# iversonnb


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

`iversonnb` brings the Iverson languages to notebooks. It adds `%%apl`
to Jupyter and IPython, which executes code in Dyalog APL, `%%j` to J,
plus persistent APL and J kernels for LLM agents (including MCP). APL
runs over the [RIDE
protocol](https://github.com/Dyalog/ride/blob/master/docs/protocol.md),
the same protocol Dyalog’s own IDE uses, so nothing needs to be loaded
into your workspace, output looks exactly like a Dyalog session, and
errors are detected reliably. J runs in-process through libj, the engine
library that ships with every J install.

See [`core`](https://answerdotai.github.io/iversonnb/core.html) and
[`j`](https://answerdotai.github.io/iversonnb/j.html) for step by step
walkthroughs of how each is built.

## Installation

Install the languages you plan to use; nothing starts until a magic
first runs, so neither is required.

- **APL**: install [Dyalog APL](https://www.dyalog.com/). Dyalog
  provides a basic license for free. iversonnb is developed against
  Dyalog 20.0; 18.2 and later may work but are untested.
- **J**: `pip install jlanguage`, or install it from
  [jsoftware.com](https://www.jsoftware.com/) – iversonnb finds either.

Then:

    pip install iversonnb

Once that’s complete, you can install the magics to all IPython and
Jupyter sessions automatically by running in your terminal:

    iversonnb_install

An interpreter only starts the first time you run its magic, so having
iversonnb installed everywhere costs nothing when you don’t use it.

## The `apl` magics

After first running an `apl` magic in a notebook, the [APL language
bar](https://abrudz.github.io/lb/apl) by Adám Brudzewsky is
automatically added to the current page. (iversonnb bundles a modified
copy of Adám’s lb.js; the file header lists the changes. The most
visible ones: type a backtick twice in a row to enter triple backticks,
get a `⋄` glyph with backtick-q, and use the `▲`/`▼` button beside the
close button to choose whether the bar pushes the page down or overlays
it, remembered per site.)

The cell magic (`%%apl`) runs APL code and displays the session’s own
output, rendered in Adám’s [SAX2](https://github.com/abrudz/SAX2) APL
font, so results look exactly as they do in Dyalog:

``` python
%%apl
m←3 3⍴⍳9
m×10
```

    Javascript(// APL language bar by Adám Brudzewsky: https://abrudz.github.io/lb (source: https://github.com/abrudz/lb)
    // MIT License, Copyright (c) 2011-2020 Nikolay G. Nikolov and Adam Brudzevski. This is a modified copy bundled with iversonnb.
    // Changes from upstream: double backtick composes ```; insertion via insertText so undo and input events work;
    // Monaco editor support (incl. EditContext mode); dark mode; overlay/push-down toggle persisted per site;
    // idempotent injection; ResizeObserver-driven layout; @font-face with dead url() removed; skipped on quarto-rendered pages.
    ; (_ => {
        if (document.querySelector('.ngn_lb')) return
        if (document.querySelector('meta[name=generator][content^=quarto]')) return //no bar on rendered docs pages
        let hc = { '<': '&lt;', '&': '&amp;', "'": '&apos;', '"': '&quot;' }, he = x => x.replace(/[<&'"]/g, c => hc[c]) //html chars and escape fn
            , tcs = '<-←xx×/\\×:-÷*O⍟[-⌹-]⌹OO○77⌈FF⌈ll⌊LL⌊T_⌶II⌶|_⊥TT⊤-|⊣|-⊢=/≠L-≠<=≤<_≤>=≥>_≥==≡=_≡7=≢Z-≢vv∨^^∧^~⍲v~⍱^|↑v|↓((⊂cc⊂(_⊆c_⊆))⊃[|⌷|]⌷A|⍋V|⍒ii⍳i_⍸ee∊e_⍷' +
                'uu∪UU∪nn∩/-⌿\\-⍀,-⍪rr⍴pp⍴O|⌽O-⊖O\\⍉::¨""¨~:⍨~"⍨*:⍣*"⍣oo∘o:⍤o"⍤O:⍥O"⍥[\'⍞\']⍞[]⎕[:⍠:]⍠[=⌸=]⌸[<⌺>]⌺o_⍎oT⍕o-⍕<>⋄^v⋄on⍝->→aa⍺ww⍵VV∇v-∇--¯0~⍬' +
                'AA∆^-∆A_⍙^=⍙[?⍰?]⍰:V⍢∇"⍢||∥ox¤)_⊇_)⊇V~⍫\'\'`'
            , lbs = ['←←\nASSIGN', ' ', '++\nconjugate\nplus', '--\nnegate\nminus', '××\ndirection\ntimes', '÷÷\nreciprocal\ndivide', '**\nexponential\npower', '⍟⍟\nnatural logarithm\nlogarithm',
                '⌹⌹\nmatrix inverse\nmatrix divide', '○○\npi times\ncircular', '!!\nfactorial\nbinomial', '??\nroll\ndeal', ' ', '||\nmagnitude\nresidue',
                '⌈⌈\nceiling\nmaximum', '⌊⌊\nfloor\nminimum', '⊥⊥\ndecode', '⊤⊤\nencode', '⊣⊣\nsame\nleft', '⊢⊢\nsame\nright', ' ', '==\nequal', '≠≠\nunique mask\nnot equal',
                '≤≤\nless than or equal to', '<<\nless than', '>>\ngreater than', '≥≥\ngreater than or equal to', '≡≡\ndepth\nmatch', '≢≢\ntally\nnot match', ' ', '∨∨\ngreatest common divisor/or',
                '∧∧\nlowest common multiple/and', '⍲⍲\nnand', '⍱⍱\nnor', ' ', '↑↑\nmix\ntake', '↓↓\nsplit\ndrop', '⊂⊂\nenclose\npartioned enclose', '⊃⊃\nfirst\npick', '⊆⊆\nnest\npartition', '⌷⌷\nmaterialise\nindex', '⍋⍋\ngrade up\ngrades up',
                '⍒⍒\ngrade down\ngrades down', ' ', '⍳⍳\nindices\nindices of', '⍸⍸\nwhere\ninterval index', '∊∊\nenlist\nmember of', '⍷⍷\nfind', '∪∪\nunique\nunion', '∩∩\nintersection', '~~\nnot\nwithout', ' ',
                '//\nreplicate\nReduce', '\\\\\n\expand\nScan', '⌿⌿\nreplicate first\nReduce First', '⍀⍀\nexpand first\nScan First', ' ', ',,\nravel\ncatenate/laminate',
                '⍪⍪\ntable\ncatenate first/laminate', '⍴⍴\nshape\nreshape', '⌽⌽\nreverse\nrotate', '⊖⊖\nreverse first\nrotate first',
                '⍉⍉\ntranspose\nreorder axes', ' ', '¨¨\nEach', '⍨⍨\nConstant\nSelf\nSwap', '⍣⍣\nRepeat\nUntil', '..\nOuter Product (∘.)\nInner Product',
                '∘∘\nOUTER PRODUCT (∘.)\nBind\nBeside', '⍤⍤\nRank\nAtop', '⍥⍥\nOver', '@@\nAt', ' ', '⍞⍞\nSTDIN\nSTDERR', '⎕⎕\nEVALUATED STDIN\nSTDOUT\nSYSTEM NAME PREFIX', '⍠⍠\nVariant',
                '⌸⌸\nIndex Key\nKey', '⌺⌺\nStencil', '⌶⌶\nI-Beam', '⍎⍎\nexecute', '⍕⍕\nformat', ' ', '⋄⋄\nSTATEMENT SEPARATOR', '⍝⍝\nCOMMENT', '→→\nABORT\nBRANCH', '⍵⍵\nRIGHT ARGUMENT\nRIGHT OPERAND (⍵⍵)', '⍺⍺\nLEFT ARGUMENT\nLEFT OPERAND (⍺⍺)',
                '∇∇\nrecursion\nRecursion (∇∇)', '&&\nSpawn', ' ', '¯¯\nNEGATIVE', '⍬⍬\nEMPTY NUMERIC VECTOR', '∆∆\nIDENTIFIER CHARACTER', '⍙⍙\nIDENTIFIER CHARACTER']
            , bqk = ' =1234567890-qwertyuiop\\asdfghjk∙l;\'zxcvbnm,./q[]+!@#$%^&*()_QWERTYUIOP|ASDFGHJKL:"ZXCVBNM<>?~{}'.replace(/∙/g, '')
            , bqv = '`÷¨¯<≤=≥>≠∨∧×⋄⍵∊⍴~↑↓⍳○*⊢∙⍺⌈⌊_∇∆∘\'⎕⍎⍕∙⊂⊃∩∪⊥⊤|⍝⍀⌿⋄←→⌹⌶⍫⍒⍋⌽⍉⊖⍟⍱⍲!⍰W⍷R⍨YU⍸⍥⍣⊣ASDF⍢H⍤⌸⌷≡≢⊆⊇CVB¤∥⍪⍙⍠⌺⍞⍬'.replace(/∙/g, '')
            , tc = {}, bqc = {} //tab completions and ` completions
        for (let i = 0; i < bqk.length; i++)bqc[bqk[i]] = bqv[i]
        for (let i = 0; i < tcs.length; i += 3)tc[tcs[i] + tcs[i + 1]] = tcs[i + 2]
        for (let i = 0; i < tcs.length; i += 3) { let k = tcs[i + 1] + tcs[i]; tc[k] = tc[k] || tcs[i + 2] }
        let lbh = ''; for (let i = 0; i < lbs.length; i++) {
            let ks = []
            for (let j = 0; j < tcs.length; j += 3)if (lbs[i][0] === tcs[j + 2]) ks.push('\n' + tcs[j] + ' ' + tcs[j + 1] + ' <tab>')
            for (let j = 0; j < bqk.length; j++)if (lbs[i][0] === bqv[j]) ks.push('\n` ' + bqk[j])
            lbh += '<b title="' + he(lbs[i].slice(1) + (ks.length ? '\n' + ks.join('') : '')) + '">' + lbs[i][0] + '</b>'
        }
        let ovl; try { ovl = localStorage.getItem('ngn_lb_overlay') === '1' } catch (e) { ovl = !1 } //overlay mode: bar covers the top instead of pushing the page down
        let d = document, el = d.createElement('div'); el.innerHTML =
            `<div class=ngn_lb><span class=ngn_x title=Close>❎</span><span class=ngn_o title="Toggle overlay/push-down">${ovl ? '▼' : '▲'}</span>${lbh}</div>
     <style>
      .ngn_lb{position:fixed;top:0;left:0;right:0;background-color:#eee;color:#000;cursor:default;z-index:2147483647;
        font-family:"DejaVu Sans Mono",monospace;border-bottom:solid #999 1px;padding:2px 2px 0 2px;word-wrap:break-word;}
      .ngn_lb b{cursor:pointer;padding:0 1px;font-weight:normal}
      .ngn_lb b:hover,.ngn_bq .ngn_lb{background-color:#777;color:#fff}
      .ngn_x,.ngn_o{float:right;color:#999;cursor:pointer;margin-top:-3px}
      .ngn_o{margin-right:6px}
      .ngn_o:hover{color:#00d}
      .ngn_x:hover{color:#f00}
      @media (prefers-color-scheme:dark){
       .ngn_lb{background-color:#222;color:#ddd;border-bottom-color:#555}
       .ngn_lb b:hover,.ngn_bq .ngn_lb{background-color:#bbb;color:#000}
       .ngn_x,.ngn_o{color:#666}
      }
     </style>`
        d.body.appendChild(el)
        let t, lb = el.firstChild, bqm = 0 //t:textarea or input, lb:language bar, bqm:backquote mode
        let pd = x => x.preventDefault()
        let ev = (x, t, f, c) => x.addEventListener(t, f, c)
        let med = _ => { try { return window.monaco?.editor?.getEditors?.().find(e => e.hasTextFocus()) } catch (e) { } } //focused Monaco editor, if any
        let ins = (t, s, del = 0) => { //insert s at caret (replacing selection, or del chars before it), keeping undo & input events
            let m = med()
            if (m) {
                if (del) {
                    let p = m.getPosition()
                    m.executeEdits('lb', [{ range: { startLineNumber: p.lineNumber, startColumn: p.column - del, endLineNumber: p.lineNumber, endColumn: p.column }, text: s }])
                } else m.trigger('keyboard', 'type', { text: s })
                return
            }
            if (!t || t.selectionStart == null) return
            if (del) t.selectionStart = t.selectionStart - del
            if (!(d.execCommand && d.execCommand('insertText', !1, s))) {
                let i = t.selectionStart
                t.value = t.value.slice(0, i) + s + t.value.slice(t.selectionEnd)
                t.selectionStart = t.selectionEnd = i + s.length
                t.dispatchEvent(new Event('input', { bubbles: !0 }))
            }
        }
        ev(lb, 'mousedown', x => {
            if (x.target.classList.contains('ngn_x')) { lb.hidden = 1; upd() }
            else if (x.target.classList.contains('ngn_o')) {
                ovl = !ovl
                x.target.textContent = ovl ? '▼' : '▲'
                try { localStorage.setItem('ngn_lb_overlay', ovl ? '1' : '0') } catch (e) { }
                upd()
            } else if (x.target.nodeName === 'B') {
                let s = x.target.textContent, m = med()
                if (m) { m.focus(); ins(t, s) }
                else if (t && t.selectionStart != null) { t.focus(); ins(t, s) }
            }
            pd(x) //always: clicking the bar must never steal focus
        })
        let fk = x => {
            let t = x.target, m = med(), i, v
            if (m) { let p = m.getPosition(); i = p.column - 1; v = m.getModel().getLineContent(p.lineNumber) }
            else { i = t.selectionStart; v = t.value }
            if (bqm) {
                let c = bqc[x.key]
                if (x.key === '`') {
                    ins(t, '```')
                    if (m) { let p = m.getPosition(); m.setPosition({ lineNumber: p.lineNumber, column: p.column - 2 }) }
                    else t.selectionStart = t.selectionEnd = i + 1
                    bqm = 0
                    d.body.classList.remove('ngn_bq')
                    pd(x)
                    return !1
                }
                if (x.which > 31) { bqm = 0; d.body.classList.remove('ngn_bq') }
                if (c) { ins(t, c); pd(x); return !1 }
            }
            if (!x.ctrlKey && !x.shiftKey && !x.altKey && !x.metaKey) {
                if ("`½²^º§ùµ°".indexOf(x.key) > -1) {
                    bqm = 1; d.body.classList.add('ngn_bq'); pd(x); // ` or other trigger symbol pressed, wait for next key
                } else if (x.key == "Tab") {
                    let c = i >= 2 && tc[v.slice(i - 2, i)]
                    if (c) { ins(t, c, 2); pd(x) }
                }
            }
        }
        let ff = x => {
            let t0 = x.target, nn = t0.nodeName.toLowerCase()
            if (nn !== 'textarea' && (nn !== 'input' || t0.type !== 'text' && t0.type !== 'search')) return
            t = t0; if (!t.ngn) { t.ngn = 1; ev(t, 'keydown', fk) }
        }
        let upd = _ => { d.body.style.paddingTop = ovl ? '' : lb.clientHeight + 'px' }
        upd(); (window.ResizeObserver ? new ResizeObserver(upd).observe(lb) : ev(window, 'resize', upd))
        ev(d, 'focus', ff, !0); let ae = d.activeElement; ae && ff({ type: 'focus', target: ae })
        ev(d, 'keydown', x => { if (!x.target.ngn && x.target.closest?.('.monaco-editor')) fk(x) }, !0) //EditContext-mode Monaco has no textarea for ff to register
    })();

    )

<style>
@font-face { font-family:'SAX2'; src: local('SAX2'), url('https://cdn.jsdelivr.net/gh/abrudz/SAX2@master/SAX2.ttf') format('truetype') }
.sax2 { font-family:'SAX2',monospace !important; line-height:1.05 !important }
</style>

<pre class="iversonnb_out sax2">10 20 30
40 50 60
70 80 90</pre>

Assignments are shy, just like in the Dyalog session, so the `m←` line
above printed nothing. The line magic (`%apl`) instead evaluates one
expression and returns it as a Python value:

``` python
%apl 3×⍳4
```

    [3, 6, 9, 12]

``` python
%apl ⎕A
```

    'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

Because the line magic returns a value, you can store it in a Python
variable. Scalars come back as numbers or strings, vectors as lists, and
higher-rank arrays as nested lists:

``` python
z = %apl m
z
```

    [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

To suppress a cell’s output, end the last line with a `;`:

``` python
%%apl
m×10;
```

`⎕←` displays a value explicitly, which is how you show something that
would otherwise be shy:

``` python
%%apl
v←2×⍳5
⎕←v
```

<pre class="iversonnb_out sax2">2 4 6 8 10</pre>

To use numpy, just pass the result of `%apl` into `np.array`:

``` python
import numpy as np
```

``` python
a = %apl m
np.array(a)
```

    array([[1, 2, 3],
           [4, 5, 6],
           [7, 8, 9]])

### Example algorithms

The fibonacci sequence:

``` python
%apl {⍵,+/¯2↑⍵}⍣15⊢1 1
```

    [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597]

Explanation:

1.  `1 1`: Initial seed (first two Fibonacci numbers)
2.  `{⍵,+/¯2↑⍵}`: Function that appends the sum of the last two elements
3.  `⍣15`: Apply the function 15 times
4.  `⊢`: Identity function, passes the initial argument (1 1) to the
    iteration

Prime number sieve:

``` python
%%apl
primes ← {⍵×2=+⌿0=⍵∘.|⍵}⍳
(primes 50)~0
```

<pre class="iversonnb_out sax2">2 3 5 7 11 13 17 19 23 29 31 37 41 43 47</pre>

Explanation:

1.  `⍳50` generates integers 1 to 50
2.  `⍵∘.|⍵` creates a 50x50 matrix of divisibility (1 if divisible, 0 if
    not)
3.  `0=` inverts the matrix (1 for non-divisible)
4.  `+⌿` sums columns, counting non-divisors for each number
5.  `2=` checks if count equals 2 (prime property)
6.  `⍵×` multiplies result with original numbers, keeping primes
7.  `~0` removes zero from the result

## The `j` magics

J works the same way: `%%j` runs code in a persistent J session and
displays its output verbatim, and `%j expr` returns an expression’s
value as a Python object. The session keeps nouns and verbs across
cells:

``` python
%%j
m =: 3 3 $ i. 9
m +/ . * m
```

    15 18  21
    42 54  66
    69 90 111

``` python
z = %j m
z
```

    [[0, 1, 2], [3, 4, 5], [6, 7, 8]]

Under the hood there’s no separate process or protocol: iversonnb loads
**libj** via ctypes, so completion, errors, and interrupts come straight
from the engine’s C API. The
[`J`](https://answerdotai.github.io/iversonnb/j.html#j) session class
mirrors [`Apl`](https://answerdotai.github.io/iversonnb/core.html#apl) –
call it to run code, move values in both directions with `[]`, lift
verbs into Python callables with `fn` – see
[`j`](https://answerdotai.github.io/iversonnb/j.html) for the full
walkthrough.

## Using iversonnb from Python

The magics are a thin layer over the
[`Apl`](https://answerdotai.github.io/iversonnb/core.html#apl) class,
which you can use directly in scripts, tests, and other tooling. Calling
the session runs code and returns the output exactly as Dyalog formats
it (or None if there’s no output); APL errors raise
[`AplError`](https://answerdotai.github.io/iversonnb/core.html#aplerror):

``` python
from iversonnb import Apl

apl = Apl()
apl('3 3⍴⍳9')
```

<pre class="iversonnb_out sax2">1 2 3
4 5 6
7 8 9</pre>

Square brackets move values between Python and the workspace, in both
directions, and take any expression:

``` python
apl['v'] = [3,1,4,1,5]
apl['{⍵[⍋⍵]}v']
```

    [1, 1, 3, 4, 5]

`fn` lifts an APL function into a Python callable (one argument applies
it monadically, two dyadically):

``` python
mean = apl.fn('{(+/⍵)÷≢⍵}')
mean([1,2,3,4])
```

    2.5

Sessions shut themselves down at process exit; use `close`, or a
`with Apl() as apl:` block, to do it sooner:

``` python
apl.close()
```

## The LLM kernels

iversonnb also ships persistent APL and J sessions for LLM agents, built
on [clikernel](https://github.com/AnswerDotAI/clikernel)’s stream
protocol. `aplkernel` and `jkernel` run the stdin/stdout workers
directly, and `aplkernel-mcp`/`jkernel-mcp` wrap them as MCP servers
with `execute`, `restart`, and `interrupt` tools. Workspace state
persists across calls, and `interrupt` stops a long computation while
keeping it; send `)OFF` (APL) or `exit 0` (J) to stop a worker. If a
session has to be replaced mid-conversation (a crash, or the APL
incomplete-input bug below), the response starts with a NOTE saying
workspace state was lost.

On startup each kernel runs `~/.config/iversonnb/startup.apl` or
`startup.ijs` (if it exists) in the session, and reports its source and
output in the banner, which the MCP form forwards as the server’s
instructions.

## Limitations

- Keyboard input through `⎕` or `⍞` can’t work in a notebook, so it
  raises an error. The session survives.
- A cell that ends inside an unfinished block, such as an unclosed
  `:If`, wedges the Dyalog interpreter with no way back
  ([Dyalog/ride#1401](https://github.com/Dyalog/ride/issues/1401)).
  iversonnb detects this and starts a fresh session, raising an error
  that says so (with `reset` set on it), but workspace state is lost
  when it happens.
- `%apl` transfers values with `⎕JSON`, so it’s limited to arrays and
  scalars that JSON can represent, serializing to at most 32767
  characters. For bigger data, write a file from APL instead.
- `%j` transfers values through the engine’s typed-array API, so it’s
  limited to booleans, integers, floats, and characters; boxed,
  extended, and rational values raise (display them with `%%j` instead).
- Windows isn’t supported yet: iversonnb configures Dyalog through
  environment variables, which is the Unix convention, and the J binding
  is untested there. macOS and Linux are supported.

## Learning APL and J

To start learning APL, follow the [17 video
series](https://forums.fast.ai/t/apl-array-programming/97188) run by
Jeremy Howard, and have a look at the [study
notes](https://fastai.github.io/apl-study/apl.html). The `]` user
commands mentioned there, such as `]Help ≠`, work in iversonnb too.

For J, start with [Learning
J](https://www.jsoftware.com/help/learning/contents.htm) and the [J
wiki](https://code.jsoftware.com/wiki/), whose
[NuVoc](https://code.jsoftware.com/wiki/NuVoc) page is the reference for
every primitive.
