# iversonnb core


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

iversonnb talks to Dyalog using the [RIDE
protocol](https://github.com/Dyalog/ride/blob/master/docs/protocol.md),
the same TCP protocol used by Dyalog’s own IDE and by the official
[Jupyter kernel](https://github.com/Dyalog/dyalog-jupyter-kernel). Any
Dyalog interpreter can serve it, nothing extra needs to be loaded into
the workspace, and the message framing tells us exactly when output
ends, whether it was an error, and when the interpreter is ready for
more. This notebook builds up the client one step at a time, showing
what actually goes over the wire.

## Finding and starting Dyalog

------------------------------------------------------------------------

<a
href="https://github.com/answerdotai/iversonnb/blob/main/iversonnb/core.py#L23"
target="_blank" style="float:right; font-size:smaller">source</a>

### find_dyalog

``` python
def find_dyalog():
```

*Locate the Dyalog interpreter binary*

On macOS the installer symlinks the interpreter into `/usr/local/bin`,
so `which` normally finds it. The fallbacks scan the standard install
locations on macOS and Linux.

``` python
find_dyalog()
```

    '/usr/local/bin/dyalog'

RIDE connections can go in either direction. Dyalog’s Jupyter kernel
uses `RIDE_INIT=SERVE:*:port`, where the interpreter listens and the
client polls until the port opens, which means guessing a free port and
racing to connect. `CONNECT` mode reverses the roles: we listen on a
port the OS just gave us, tell the interpreter to dial back, and
`accept()` blocks until it does. No race, no polling. `RIDE_SPAWNED=1`
tells the interpreter it belongs to this connection, so it exits when
the connection drops.

------------------------------------------------------------------------

<a
href="https://github.com/answerdotai/iversonnb/blob/main/iversonnb/core.py#L33"
target="_blank" style="float:right; font-size:smaller">source</a>

### start_dyalog

``` python
def start_dyalog(
    dyalog:NoneType=None, # Path to the interpreter binary; `find_dyalog()` result if None
    timeout:int=10, # Socket timeout (secs), so a client bug can never hang the caller
):
```

*Spawn a Dyalog interpreter that connects back to us over RIDE; return
`(socket,Popen)`*

``` python
sock,proc = start_dyalog()
```

## Message framing

The interpreter speaks first. Here are the raw bytes:

``` python
raw = sock.recv(64)
raw
```

    b'\x00\x00\x00\x1cRIDESupportedProtocols=2'

Every message is framed as a 4-byte big-endian total length, the literal
`RIDE`, then a UTF-8 payload. Here `0x1c` is 28: 4 length bytes, 4 for
`RIDE`, and the 20 characters of `SupportedProtocols=2`. The first two
messages in each direction are plain strings negotiating the protocol
version; every payload after that is JSON, a 2-element array of command
name and arguments. One quirk: the interpreter sometimes sends raw
control characters inside JSON strings, which strict parsers reject, so
[`ride_recv`](https://answerdotai.github.io/iversonnb/core.html#ride_recv)
escapes them before decoding (the official Jupyter kernel does the
same).

------------------------------------------------------------------------

<a
href="https://github.com/answerdotai/iversonnb/blob/main/iversonnb/core.py#L68"
target="_blank" style="float:right; font-size:smaller">source</a>

### ride_recv

``` python
def ride_recv(
    sock
):
```

*Receive one RIDE message, JSON-decoded unless it’s a handshake string*

------------------------------------------------------------------------

<a
href="https://github.com/answerdotai/iversonnb/blob/main/iversonnb/core.py#L53"
target="_blank" style="float:right; font-size:smaller">source</a>

### ride_send

``` python
def ride_send(
    sock, msg
):
```

*Send one RIDE message: a handshake `str`, or a `[cmd,args]` list sent
as JSON*

The handshake is symmetric: each side sends `SupportedProtocols=2` and
`UsingProtocol=2`, then we identify ourselves and the interpreter
replies with its details:

``` python
ride_send(sock, 'SupportedProtocols=2')
ride_send(sock, 'UsingProtocol=2')
ride_recv(sock)
```

    'UsingProtocol=2'

``` python
ride_send(sock, ['Identify',{'apiVersion':1,'identity':1}])
info = ride_recv(sock)[1]
{k:info[k] for k in ('Vendor','Language','version','arch','platform')}
```

    {'Vendor': 'Dyalog Limited',
     'Language': 'APL',
     'version': '20.0.53963',
     'arch': 'Unicode/64',
     'platform': 'Mac-64'}

The interpreter then sends a series of status messages on its own.
`SetPromptType` with `type` 1 means the session is ready for input, so
read until that arrives:

``` python
msgs = []
while True:
    m = ride_recv(sock)
    msgs.append(m)
    if m[0]=='SetPromptType' and m[1]['type']==1: break
msgs
```

    [['UpdateDisplayName', {'displayName': 'CLEAR WS'}],
     ['SetPromptType', {'type': 1}]]

## Executing code

`Execute` takes a line of session input (the trailing newline is
required). The interpreter echoes the input, streams back session output
as `AppendSessionOutput` messages, and finally sends `SetPromptType` 1
to say it’s ready for the next line. That final message is what makes
RIDE reliable to drive: there’s never a question of whether output has
finished.

``` python
ride_send(sock, ['Execute',{'text':'3×⍳4\n','trace':0}])
msgs = []
while True:
    m = ride_recv(sock)
    msgs.append(m)
    if m[0]=='SetPromptType' and m[1]['type']==1: break
msgs
```

    [['UpdateSessionCaption', {'text': 'CLEAR WS - Dyalog APL'}],
     ['AppendSessionOutput', {'result': '3×⍳4\n', 'type': 14, 'group': 0}],
     ['SetPromptType', {'type': 0}],
     ['AppendSessionOutput', {'result': '3 6 9 12\n', 'type': 2, 'group': 0}],
     ['SetPromptType', {'type': 1}]]

The `type` field distinguishes kinds of output: 14 is the echo of our
own input (clients normally drop it), and the rest are genuine session
output. Errors are flagged out-of-band with a `HadError` message, so
there’s no scraping of output text to detect failure:

``` python
ride_send(sock, ['Execute',{'text':'1÷0\n','trace':0}])
msgs = []
while True:
    m = ride_recv(sock)
    msgs.append(m)
    if m[0]=='SetPromptType' and m[1]['type']==1: break
msgs
```

    [['AppendSessionOutput', {'result': '1÷0\n', 'type': 14, 'group': 0}],
     ['SetPromptType', {'type': 0}],
     ['HadError', {'error': 11, 'dmx': 1}],
     ['AppendSessionOutput',
      {'result': 'DOMAIN ERROR: Divide by zero\n', 'type': 5, 'group': 0}],
     ['AppendSessionOutput', {'result': '      1÷0\n', 'type': 5, 'group': 0}],
     ['AppendSessionOutput', {'result': '       ∧\n', 'type': 5, 'group': 0}],
     ['SetPromptType', {'type': 1}]]

APL errors and unusual prompt states get their own exceptions:
[`AplError`](https://answerdotai.github.io/iversonnb/core.html#aplerror)
for ordinary APL errors (carrying the session’s error display, plus a
`reset` flag for when the interpreter had to be replaced), and
[`AplPrompt`](https://answerdotai.github.io/iversonnb/core.html#aplprompt),
used internally when the session stops at a prompt other than ready.

------------------------------------------------------------------------

<a
href="https://github.com/answerdotai/iversonnb/blob/main/iversonnb/core.py#L84"
target="_blank" style="float:right; font-size:smaller">source</a>

### AplPrompt

``` python
def AplPrompt(
    ptype
):
```

*The session stopped at a non-ready prompt: 2=⎕ input, 3=incomplete
input, 4=⍞ input*

------------------------------------------------------------------------

<a
href="https://github.com/answerdotai/iversonnb/blob/main/iversonnb/core.py#L78"
target="_blank" style="float:right; font-size:smaller">source</a>

### AplError

``` python
def AplError(
    msg, reset:bool=False
):
```

*An APL error, carrying the session’s error display as its message;
`reset` means the interpreter was replaced and workspace state lost*

[`ride_run`](https://answerdotai.github.io/iversonnb/core.html#ride_run)
wraps the whole exchange: normalize the code to non-blank lines, send
them all as one `Execute`, drop the echoes and prompt strings, collect
everything else, and return the output plus the error number (0 if none)
once the session is ready again. The echo counting is the subtle part:
each input line is echoed back as a type 14 message, and a prompt change
only counts as final once every line we sent has been echoed.

------------------------------------------------------------------------

<a
href="https://github.com/answerdotai/iversonnb/blob/main/iversonnb/core.py#L92"
target="_blank" style="float:right; font-size:smaller">source</a>

### ride_run

``` python
def ride_run(
    sock, code
):
```

*Run one or more lines of APL in the session; return `(output,errno)`
once the session is ready again*

## Multi-line input

Since 20.0 the session has multiline input on by default, so a complete
block construct can go in a single `Execute`. The interpreter echoes
each line as it consumes it, switches the prompt to type 3 while a block
is open, then runs the block once it’s complete. This is why
[`ride_run`](https://answerdotai.github.io/iversonnb/core.html#ride_run)
counts echoes: a type 3 prompt seen before all our input has been echoed
just means “still reading the block”, while one seen after means the
input really was incomplete. (One thing to avoid: sending a fresh
`Execute` message while the prompt is type 3 crashes the interpreter, so
a batch of lines must go in one message.)

``` python
ride_run(sock, '''
:If 1
    ⎕←6×7
:EndIf''')
```

    ('42\n', 0)

Plain multi-statement input works the same way, and session state
persists across calls:

``` python
test_eq(ride_run(sock, 'x←⍳3\nz←x×x\n⎕←z'), ('1 4 9\n', 0))
```

Errors come back with the `HadError` number and the session error
display:

``` python
out,err = ride_run(sock, '1÷0')
print(out)
test_eq(err, 11)
```

    DOMAIN ERROR: Divide by zero
          1÷0
           ∧

## A session object

[`Apl`](https://answerdotai.github.io/iversonnb/core.html#apl) packages
all of the above: spawn the interpreter, shake hands, identify, widen
the print width so long results aren’t wrapped (the same 32767 the
official kernel uses), and wait for the first ready prompt.

------------------------------------------------------------------------

<a
href="https://github.com/answerdotai/iversonnb/blob/main/iversonnb/core.py#L109"
target="_blank" style="float:right; font-size:smaller">source</a>

### Apl

``` python
def Apl(
    dyalog:NoneType=None, timeout:int=10
):
```

*A Dyalog APL session over the RIDE protocol*

`close` asks the interpreter to exit with an `Exit` message, then falls
back to killing the process: a wedged interpreter ignores both `Exit`
and SIGTERM. The interpreter doesn’t exit just because the connection
drops, so `_connect` registers `close` to run at process exit; without
that, every notebook kernel restart would leak a dyalog process.

------------------------------------------------------------------------

<a
href="https://github.com/answerdotai/iversonnb/blob/main/iversonnb/core.py#L133"
target="_blank" style="float:right; font-size:smaller">source</a>

### Apl.close

``` python
def close():
```

*Shut down the interpreter and close the connection*

``` python
apl = Apl()
apl.info['version']
```

    '20.0.53963'

`run` sends the code and interprets what comes back. APL errors raise
[`AplError`](https://answerdotai.github.io/iversonnb/core.html#aplerror)
with the session’s error display. A `⎕` or `⍞` input request is
cancelled (the session survives) and raises. Incomplete input, such as
an unclosed `:If`, is the nasty case: it wedges the interpreter beyond
recovery
([Dyalog/ride#1401](https://github.com/Dyalog/ride/issues/1401)), so the
only honest move is to replace the interpreter with a fresh one and
raise with `reset` set, so callers know workspace state was lost.

------------------------------------------------------------------------

<a
href="https://github.com/answerdotai/iversonnb/blob/main/iversonnb/core.py#L146"
target="_blank" style="float:right; font-size:smaller">source</a>

### Apl.run

``` python
def run(
    code
):
```

*Run `code` in the session, returning its output; raises
[`AplError`](https://answerdotai.github.io/iversonnb/core.html#aplerror)
on APL errors*

``` python
print(apl.run('m←2 3⍴⍳6\n⎕←m'))
```

    1 2 3
    4 5 6

Output comes back exactly as the session formats it, so matrices look
like APL. State persists for the life of the session, and errors raise
[`AplError`](https://answerdotai.github.io/iversonnb/core.html#aplerror)
carrying the session’s error display:

``` python
test_eq(apl.run('+/,m'), '21\n')
test_fail(lambda: apl.run('m+\'x\''), contains='DOMAIN ERROR')
```

Wrapping every use in `print(apl.run(...))` is clunky. Defining
`__call__` lets us treat the session itself as a function, and giving
the output a verbatim `__repr__` means bare expressions in a notebook
display exactly as they do in a Dyalog session.
[`AplOut`](https://answerdotai.github.io/iversonnb/core.html#aplout)
subclasses `str`, so results still compare, slice, and print like
ordinary strings. A cell with no output returns None, so nothing
displays at all:

------------------------------------------------------------------------

<a
href="https://github.com/answerdotai/iversonnb/blob/main/iversonnb/core.py#L168"
target="_blank" style="float:right; font-size:smaller">source</a>

### Apl.\_\_call\_\_

``` python
def __call__(
    code
):
```

*Run `code`, returning session output (or None if there is none)*

------------------------------------------------------------------------

<a
href="https://github.com/answerdotai/iversonnb/blob/main/iversonnb/core.py#L162"
target="_blank" style="float:right; font-size:smaller">source</a>

### AplOut

``` python
def AplOut(
    *args, **kwargs
):
```

*Output text from an
[`Apl`](https://answerdotai.github.io/iversonnb/core.html#apl) call;
displays verbatim, in the SAX2 APL font where HTML is available*

``` python
apl('m ∘.× ⍳4')
```

<pre class="iversonnb_out sax2">1  2  3  4
2  4  6  8
3  6  9 12
          &#10;4  8 12 16
5 10 15 20
6 12 18 24</pre>

``` python
test_eq(apl('+/,m'), '21\n')
test_is(apl('m2←m×10'), None)
```

## Getting values into Python

`run` returns display text, which is what you want to look at, but not
what you want to compute with. For that, ask the interpreter to
serialize the value with `⎕JSON` and parse it on our side. The
`HighRank` variant option makes rank≥2 arrays serialize as nested lists
(by default `⎕JSON` refuses them). The `1` left argument forces export:
monadic `⎕JSON` on a character vector would parse it as JSON instead of
serializing it.

------------------------------------------------------------------------

<a
href="https://github.com/answerdotai/iversonnb/blob/main/iversonnb/core.py#L175"
target="_blank" style="float:right; font-size:smaller">source</a>

### Apl.pyval

``` python
def pyval(
    expr
):
```

*Evaluate `expr` and return the result as a Python value*

``` python
test_eq(apl.pyval('m'), [[1,2,3],[4,5,6]])
test_eq(apl.pyval('3×⍳4'), [3,6,9,12])
test_eq(apl.pyval('⎕A'), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
```

`pyval` is wordy for the most common case, grabbing a variable. Square
brackets read better, and `⎕JSON` works in both directions, so we can
assign Python values into the workspace too:

------------------------------------------------------------------------

<a
href="https://github.com/answerdotai/iversonnb/blob/main/iversonnb/core.py#L188"
target="_blank" style="float:right; font-size:smaller">source</a>

### Apl.\_\_setitem\_\_

``` python
def __setitem__(
    nm, v
):
```

*Call self as a function.*

------------------------------------------------------------------------

<a
href="https://github.com/answerdotai/iversonnb/blob/main/iversonnb/core.py#L185"
target="_blank" style="float:right; font-size:smaller">source</a>

### Apl.\_\_getitem\_\_

``` python
def __getitem__(
    expr
):
```

*Call self as a function.*

``` python
apl['q'] = [[1,2],[3,4.5]]
test_eq(apl['q'], [[1,2],[3,4.5]])
test_eq(apl["'nested: ',⍕⎕NC'q'"], 'nested: 2')
apl['s'] = "it's"
test_eq(apl['s'], "it's")
```

Note that `⎕JSON` imports nested lists as vectors of vectors, not as
rank-2 arrays. When you want a proper matrix, mix with `↑`:

``` python
apl('↑q')
```

<pre class="iversonnb_out sax2">1 2  
3 4.5</pre>

`fn` goes one step further: it turns any APL function into a Python
callable. One argument applies it monadically; two applies it
dyadically, in APL’s left-argument-first order. Arguments travel in as
`⎕JSON` and the result comes back through `pyval`:

------------------------------------------------------------------------

<a
href="https://github.com/answerdotai/iversonnb/blob/main/iversonnb/core.py#L192"
target="_blank" style="float:right; font-size:smaller">source</a>

### Apl.fn

``` python
def fn(
    code
):
```

*A Python callable applying APL function `code` monadically or
dyadically*

``` python
sq = apl.fn('{⍵*2}')
test_eq(sq([1,2,3]), [1,4,9])
test_eq(apl.fn('+/')([1,2,3]), 6)
test_eq(apl.fn('↑')(2, [5,6,7]), [5,6])
```

Sessions close themselves at process exit, but a context manager is
tidier for short-lived ones:

------------------------------------------------------------------------

<a
href="https://github.com/answerdotai/iversonnb/blob/main/iversonnb/core.py#L205"
target="_blank" style="float:right; font-size:smaller">source</a>

### Apl.\_\_exit\_\_

``` python
def __exit__(
    *args
):
```

*Call self as a function.*

------------------------------------------------------------------------

<a
href="https://github.com/answerdotai/iversonnb/blob/main/iversonnb/core.py#L202"
target="_blank" style="float:right; font-size:smaller">source</a>

### Apl.\_\_enter\_\_

``` python
def __enter__():
```

*Call self as a function.*

``` python
with Apl() as a2: test_eq(a2('2+2'), '4\n')
```

## Input requests and incomplete input

A cell that asks for keyboard input can’t be satisfied in a notebook, so
`run` cancels the request and raises, leaving the session usable:

``` python
test_fail(lambda: apl('x←⎕'), contains='not supported')
test_eq(apl('2+2'), '4\n')
```

``` python
test_fail(lambda: apl('x←⍞'), contains='not supported')
test_eq(apl('2+2'), '4\n')
```

Incomplete input is unrecoverable, so `run` replaces the wedged
interpreter with a fresh workspace and raises with `reset` set:

``` python
try: apl(':If 1')
except AplError as ex: err = ex
test_eq(err.reset, True)
assert 'wedged' in str(err)
test_eq(apl('2+2'), '4\n')
```

Long computations must not be cut off: the socket timeout guards startup
only, and is lifted once the session is ready, so `run` waits as long as
the code takes.

``` python
with Apl(timeout=2) as a2: assert float(a2.run('⎕←⎕DL 3')) >= 3
```

## The `apl` magics

`%%apl` runs a cell and displays the session output, rendered in Adám
Brudzewsky’s [SAX2](https://github.com/abrudz/SAX2) APL font (loaded
from a CDN, falling back to plain monospace offline), so results look
exactly as they do in a Dyalog session. `%apl expr` returns the
expression’s value as a Python object (it’s just `apl[expr]`), so it
works on the right of an assignment: `z = %apl z`. A trailing `;` on a
cell suppresses its output, and the interpreter only starts on first
use, not when the magic is registered. The [APL language
bar](https://abrudz.github.io/lb/apl) is injected into the page the
first time a magic runs.

------------------------------------------------------------------------

<a
href="https://github.com/answerdotai/iversonnb/blob/main/iversonnb/core.py#L213"
target="_blank" style="float:right; font-size:smaller">source</a>

### APLMagic

``` python
def APLMagic(
    dyalog:NoneType=None
):
```

*IPython `%apl`/`%%apl` magics, driving a lazily-started
[`Apl`](https://answerdotai.github.io/iversonnb/core.html#apl) session*

------------------------------------------------------------------------

<a
href="https://github.com/answerdotai/iversonnb/blob/main/iversonnb/core.py#L232"
target="_blank" style="float:right; font-size:smaller">source</a>

### create_magic

``` python
def create_magic(
    shell:NoneType=None
):
```

*Create an
[`APLMagic`](https://answerdotai.github.io/iversonnb/core.html#aplmagic)
and register its `apl` line/cell magic with `shell`, returning it*

``` python
# Only required if you don't load the extension
magic = create_magic()
```

``` python
%%apl
m2←3 3⍴⍳9
⎕←m2
```

    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">1 2 3
4 5 6
7 8 9</pre>

The line magic brings values back into Python:

``` python
z = %apl m2  ⍝ comments are fine here too
test_eq(z, [[1,2,3],[4,5,6],[7,8,9]])
```

`]` user commands work, and a trailing `;` suppresses cell output
entirely:

``` python
%%apl
]display 2 2⍴'ab' 'cd' 1 2
```

<pre class="iversonnb_out sax2">┌→──────────┐
↓ ┌→─┐ ┌→─┐ │
│ │ab│ │cd│ │
│ └──┘ └──┘ │
│           │
│ 1    2    │
│           │
└∊──────────┘</pre>

``` python
%%apl
big←1000 1000⍴⍳12;
```

------------------------------------------------------------------------

<a
href="https://github.com/answerdotai/iversonnb/blob/main/iversonnb/core.py#L241"
target="_blank" style="float:right; font-size:smaller">source</a>

### load_ipython_extension

``` python
def load_ipython_extension(
    ipython
):
```

*Required function for creating magic*

------------------------------------------------------------------------

<a
href="https://github.com/answerdotai/iversonnb/blob/main/iversonnb/core.py#L246"
target="_blank" style="float:right; font-size:smaller">source</a>

### create_ipython_config

``` python
def create_ipython_config():
```

*Called by `iversonnb_install` to install magic*

## Cleanup

Shut down the sessions this notebook started: the magic’s, the
[`Apl`](https://answerdotai.github.io/iversonnb/core.html#apl) object’s,
and the raw-socket walkthrough one.

``` python
magic.o.close()
apl.close()
ride_send(sock, ['Exit',{'code':0}])
sock.close()
```
