iversonnb 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iversonnb/__init__.py +9 -0
- iversonnb/_modidx.py +67 -0
- iversonnb/core.py +253 -0
- iversonnb/j.py +204 -0
- iversonnb/jkernel.py +49 -0
- iversonnb/kernel.py +56 -0
- iversonnb-0.2.0.dist-info/METADATA +455 -0
- iversonnb-0.2.0.dist-info/RECORD +12 -0
- iversonnb-0.2.0.dist-info/WHEEL +5 -0
- iversonnb-0.2.0.dist-info/entry_points.txt +9 -0
- iversonnb-0.2.0.dist-info/licenses/LICENSE +201 -0
- iversonnb-0.2.0.dist-info/top_level.txt +1 -0
iversonnb/__init__.py
ADDED
iversonnb/_modidx.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Autogenerated by nbdev
|
|
2
|
+
|
|
3
|
+
d = { 'settings': { 'branch': 'main',
|
|
4
|
+
'doc_baseurl': '/iversonnb',
|
|
5
|
+
'doc_host': 'https://answerdotai.github.io',
|
|
6
|
+
'git_url': 'https://github.com/answerdotai/iversonnb',
|
|
7
|
+
'lib_path': 'iversonnb'},
|
|
8
|
+
'syms': { 'iversonnb.core': { 'iversonnb.core.APLMagic': ('core.html#aplmagic', 'iversonnb/core.py'),
|
|
9
|
+
'iversonnb.core.APLMagic.__init__': ('core.html#aplmagic.__init__', 'iversonnb/core.py'),
|
|
10
|
+
'iversonnb.core.APLMagic.apl': ('core.html#aplmagic.apl', 'iversonnb/core.py'),
|
|
11
|
+
'iversonnb.core.Apl': ('core.html#apl', 'iversonnb/core.py'),
|
|
12
|
+
'iversonnb.core.Apl.__call__': ('core.html#apl.__call__', 'iversonnb/core.py'),
|
|
13
|
+
'iversonnb.core.Apl.__enter__': ('core.html#apl.__enter__', 'iversonnb/core.py'),
|
|
14
|
+
'iversonnb.core.Apl.__exit__': ('core.html#apl.__exit__', 'iversonnb/core.py'),
|
|
15
|
+
'iversonnb.core.Apl.__getitem__': ('core.html#apl.__getitem__', 'iversonnb/core.py'),
|
|
16
|
+
'iversonnb.core.Apl.__init__': ('core.html#apl.__init__', 'iversonnb/core.py'),
|
|
17
|
+
'iversonnb.core.Apl.__setitem__': ('core.html#apl.__setitem__', 'iversonnb/core.py'),
|
|
18
|
+
'iversonnb.core.Apl._connect': ('core.html#apl._connect', 'iversonnb/core.py'),
|
|
19
|
+
'iversonnb.core.Apl.close': ('core.html#apl.close', 'iversonnb/core.py'),
|
|
20
|
+
'iversonnb.core.Apl.fn': ('core.html#apl.fn', 'iversonnb/core.py'),
|
|
21
|
+
'iversonnb.core.Apl.pyval': ('core.html#apl.pyval', 'iversonnb/core.py'),
|
|
22
|
+
'iversonnb.core.Apl.run': ('core.html#apl.run', 'iversonnb/core.py'),
|
|
23
|
+
'iversonnb.core.AplError': ('core.html#aplerror', 'iversonnb/core.py'),
|
|
24
|
+
'iversonnb.core.AplError.__init__': ('core.html#aplerror.__init__', 'iversonnb/core.py'),
|
|
25
|
+
'iversonnb.core.AplOut': ('core.html#aplout', 'iversonnb/core.py'),
|
|
26
|
+
'iversonnb.core.AplOut.__repr__': ('core.html#aplout.__repr__', 'iversonnb/core.py'),
|
|
27
|
+
'iversonnb.core.AplOut._repr_html_': ('core.html#aplout._repr_html_', 'iversonnb/core.py'),
|
|
28
|
+
'iversonnb.core.AplPrompt': ('core.html#aplprompt', 'iversonnb/core.py'),
|
|
29
|
+
'iversonnb.core.AplPrompt.__init__': ('core.html#aplprompt.__init__', 'iversonnb/core.py'),
|
|
30
|
+
'iversonnb.core._apljson': ('core.html#_apljson', 'iversonnb/core.py'),
|
|
31
|
+
'iversonnb.core._recvall': ('core.html#_recvall', 'iversonnb/core.py'),
|
|
32
|
+
'iversonnb.core.create_ipython_config': ('core.html#create_ipython_config', 'iversonnb/core.py'),
|
|
33
|
+
'iversonnb.core.create_magic': ('core.html#create_magic', 'iversonnb/core.py'),
|
|
34
|
+
'iversonnb.core.find_dyalog': ('core.html#find_dyalog', 'iversonnb/core.py'),
|
|
35
|
+
'iversonnb.core.load_ipython_extension': ('core.html#load_ipython_extension', 'iversonnb/core.py'),
|
|
36
|
+
'iversonnb.core.ride_recv': ('core.html#ride_recv', 'iversonnb/core.py'),
|
|
37
|
+
'iversonnb.core.ride_run': ('core.html#ride_run', 'iversonnb/core.py'),
|
|
38
|
+
'iversonnb.core.ride_send': ('core.html#ride_send', 'iversonnb/core.py'),
|
|
39
|
+
'iversonnb.core.start_dyalog': ('core.html#start_dyalog', 'iversonnb/core.py')},
|
|
40
|
+
'iversonnb.j': { 'iversonnb.j.J': ('j.html#j', 'iversonnb/j.py'),
|
|
41
|
+
'iversonnb.j.J.__call__': ('j.html#j.__call__', 'iversonnb/j.py'),
|
|
42
|
+
'iversonnb.j.J.__enter__': ('j.html#j.__enter__', 'iversonnb/j.py'),
|
|
43
|
+
'iversonnb.j.J.__exit__': ('j.html#j.__exit__', 'iversonnb/j.py'),
|
|
44
|
+
'iversonnb.j.J.__getitem__': ('j.html#j.__getitem__', 'iversonnb/j.py'),
|
|
45
|
+
'iversonnb.j.J.__init__': ('j.html#j.__init__', 'iversonnb/j.py'),
|
|
46
|
+
'iversonnb.j.J.__setitem__': ('j.html#j.__setitem__', 'iversonnb/j.py'),
|
|
47
|
+
'iversonnb.j.J.close': ('j.html#j.close', 'iversonnb/j.py'),
|
|
48
|
+
'iversonnb.j.J.fn': ('j.html#j.fn', 'iversonnb/j.py'),
|
|
49
|
+
'iversonnb.j.J.getm': ('j.html#j.getm', 'iversonnb/j.py'),
|
|
50
|
+
'iversonnb.j.J.interrupt': ('j.html#j.interrupt', 'iversonnb/j.py'),
|
|
51
|
+
'iversonnb.j.J.pyval': ('j.html#j.pyval', 'iversonnb/j.py'),
|
|
52
|
+
'iversonnb.j.J.run': ('j.html#j.run', 'iversonnb/j.py'),
|
|
53
|
+
'iversonnb.j.JError': ('j.html#jerror', 'iversonnb/j.py'),
|
|
54
|
+
'iversonnb.j.JMagic': ('j.html#jmagic', 'iversonnb/j.py'),
|
|
55
|
+
'iversonnb.j.JMagic.__init__': ('j.html#jmagic.__init__', 'iversonnb/j.py'),
|
|
56
|
+
'iversonnb.j.JMagic.j': ('j.html#jmagic.j', 'iversonnb/j.py'),
|
|
57
|
+
'iversonnb.j.JOut': ('j.html#jout', 'iversonnb/j.py'),
|
|
58
|
+
'iversonnb.j.JOut.__repr__': ('j.html#jout.__repr__', 'iversonnb/j.py'),
|
|
59
|
+
'iversonnb.j._flat': ('j.html#_flat', 'iversonnb/j.py'),
|
|
60
|
+
'iversonnb.j._jdat': ('j.html#_jdat', 'iversonnb/j.py'),
|
|
61
|
+
'iversonnb.j._nest': ('j.html#_nest', 'iversonnb/j.py'),
|
|
62
|
+
'iversonnb.j.create_j_magic': ('j.html#create_j_magic', 'iversonnb/j.py'),
|
|
63
|
+
'iversonnb.j.find_j': ('j.html#find_j', 'iversonnb/j.py'),
|
|
64
|
+
'iversonnb.j.load_ipython_extension': ('j.html#load_ipython_extension', 'iversonnb/j.py'),
|
|
65
|
+
'iversonnb.j.load_j': ('j.html#load_j', 'iversonnb/j.py')},
|
|
66
|
+
'iversonnb.jkernel': {},
|
|
67
|
+
'iversonnb.kernel': {}}}
|
iversonnb/core.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Driving Dyalog APL over the RIDE protocol, and `apl` magics for Jupyter and IPython
|
|
2
|
+
|
|
3
|
+
Docs: https://answerdotai.github.io/iversonnb/core.html.md"""
|
|
4
|
+
|
|
5
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../00_core.ipynb.
|
|
6
|
+
|
|
7
|
+
# %% auto #0
|
|
8
|
+
__all__ = ['find_dyalog', 'start_dyalog', 'ride_send', 'ride_recv', 'AplError', 'AplPrompt', 'ride_run', 'Apl', 'AplOut',
|
|
9
|
+
'APLMagic', 'create_magic', 'load_ipython_extension', 'create_ipython_config']
|
|
10
|
+
|
|
11
|
+
# %% ../00_core.ipynb #fe6dcafd
|
|
12
|
+
import atexit,html,json,socket,subprocess
|
|
13
|
+
from shutil import which
|
|
14
|
+
from importlib.resources import files
|
|
15
|
+
from fastcore.utils import *
|
|
16
|
+
from fastcore.test import *
|
|
17
|
+
from IPython.display import display, Javascript, HTML
|
|
18
|
+
from IPython.paths import get_ipython_dir
|
|
19
|
+
from IPython.utils.capture import capture_output
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# %% ../00_core.ipynb #0c05a3cc
|
|
23
|
+
def find_dyalog():
|
|
24
|
+
"Locate the Dyalog interpreter binary"
|
|
25
|
+
if p:=which('mapl') or which('dyalog'): return p
|
|
26
|
+
apps = sorted(Path('/Applications').glob('Dyalog-*.app'))
|
|
27
|
+
if apps: return str(apps[-1]/'Contents/Resources/Dyalog/mapl')
|
|
28
|
+
vers = sorted(Path('/opt/mdyalog').glob('*/*/*/mapl'))
|
|
29
|
+
if vers: return str(vers[-1])
|
|
30
|
+
raise FileNotFoundError('Dyalog APL not found: install it from dyalog.com')
|
|
31
|
+
|
|
32
|
+
# %% ../00_core.ipynb #ece90862
|
|
33
|
+
def start_dyalog(
|
|
34
|
+
dyalog=None, # Path to the interpreter binary; `find_dyalog()` result if None
|
|
35
|
+
timeout=10, # Socket timeout (secs), so a client bug can never hang the caller
|
|
36
|
+
):
|
|
37
|
+
"Spawn a Dyalog interpreter that connects back to us over RIDE; return `(socket,Popen)`"
|
|
38
|
+
if not dyalog: dyalog = find_dyalog()
|
|
39
|
+
lsn = socket.create_server(('127.0.0.1', 0))
|
|
40
|
+
port = lsn.getsockname()[1]
|
|
41
|
+
env = os.environ | dict(RIDE_INIT=f'CONNECT:127.0.0.1:{port}', RIDE_SPAWNED='1',
|
|
42
|
+
MAXAPLCORES=os.environ.get('MAXAPLCORES','0'),
|
|
43
|
+
DYALOGQUIETUCMDBUILD='1', DYALOG_LINEEDITOR_MODE='1', ENABLE_CEF='0', LOG_FILE_INUSE='0')
|
|
44
|
+
dn = subprocess.DEVNULL
|
|
45
|
+
proc = subprocess.Popen([dyalog], env=env, stdin=dn, stdout=dn, stderr=dn)
|
|
46
|
+
lsn.settimeout(10)
|
|
47
|
+
sock,_ = lsn.accept()
|
|
48
|
+
lsn.close()
|
|
49
|
+
sock.settimeout(timeout)
|
|
50
|
+
return sock,proc
|
|
51
|
+
|
|
52
|
+
# %% ../00_core.ipynb #3ede6ea9
|
|
53
|
+
def ride_send(sock, msg):
|
|
54
|
+
"Send one RIDE message: a handshake `str`, or a `[cmd,args]` list sent as JSON"
|
|
55
|
+
if not isinstance(msg,str): msg = json.dumps(msg, separators=(',',':'))
|
|
56
|
+
b = ('RIDE'+msg).encode()
|
|
57
|
+
sock.sendall((len(b)+4).to_bytes(4,'big')+b)
|
|
58
|
+
|
|
59
|
+
def _recvall(sock, n):
|
|
60
|
+
parts = []
|
|
61
|
+
while n:
|
|
62
|
+
b = sock.recv(n)
|
|
63
|
+
if not b: raise ConnectionError('Dyalog closed the connection')
|
|
64
|
+
parts.append(b)
|
|
65
|
+
n -= len(b)
|
|
66
|
+
return b''.join(parts)
|
|
67
|
+
|
|
68
|
+
def ride_recv(sock):
|
|
69
|
+
"Receive one RIDE message, JSON-decoded unless it's a handshake string"
|
|
70
|
+
hdr = _recvall(sock, 8)
|
|
71
|
+
assert hdr[4:8]==b'RIDE', f"Bad RIDE header: {hdr}"
|
|
72
|
+
msg = _recvall(sock, int.from_bytes(hdr[:4],'big')-8).decode()
|
|
73
|
+
msg = re.sub(r'[\x00-\x1f]', lambda m: f'\\u{ord(m[0]):04x}', msg)
|
|
74
|
+
return json.loads(msg) if msg[0]=='[' else msg
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# %% ../00_core.ipynb #8b0cdcfc
|
|
78
|
+
class AplError(Exception):
|
|
79
|
+
"An APL error, carrying the session's error display as its message; `reset` means the interpreter was replaced and workspace state lost"
|
|
80
|
+
def __init__(self, msg, reset=False):
|
|
81
|
+
super().__init__(msg)
|
|
82
|
+
self.reset = reset
|
|
83
|
+
|
|
84
|
+
class AplPrompt(Exception):
|
|
85
|
+
"The session stopped at a non-ready prompt: 2=⎕ input, 3=incomplete input, 4=⍞ input"
|
|
86
|
+
def __init__(self, ptype):
|
|
87
|
+
super().__init__(f'prompt type {ptype}')
|
|
88
|
+
self.ptype = ptype
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# %% ../00_core.ipynb #538b5ad9
|
|
92
|
+
def ride_run(sock, code):
|
|
93
|
+
"Run one or more lines of APL in the session; return `(output,errno)` once the session is ready again"
|
|
94
|
+
lines = [l for l in code.splitlines() if l.strip()]
|
|
95
|
+
ride_send(sock, ['Execute',{'text':'\n'.join(lines)+'\n','trace':0}])
|
|
96
|
+
out,err,pending = '',0,len(lines)
|
|
97
|
+
while True:
|
|
98
|
+
m,a = ride_recv(sock)
|
|
99
|
+
if m=='AppendSessionOutput':
|
|
100
|
+
if a['type']==14: pending -= 1
|
|
101
|
+
elif a['type']!=1: out += a['result']
|
|
102
|
+
elif m=='HadError': err = a['error']
|
|
103
|
+
elif m=='SetPromptType' and a['type']!=0 and not pending:
|
|
104
|
+
if a['type']!=1: raise AplPrompt(a['type'])
|
|
105
|
+
return out,err
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# %% ../00_core.ipynb #23638f61
|
|
109
|
+
class Apl:
|
|
110
|
+
"A Dyalog APL session over the RIDE protocol"
|
|
111
|
+
def __init__(self, dyalog=None, timeout=10):
|
|
112
|
+
store_attr()
|
|
113
|
+
self._connect()
|
|
114
|
+
|
|
115
|
+
def _connect(self):
|
|
116
|
+
self.sock,self.proc = start_dyalog(self.dyalog, self.timeout)
|
|
117
|
+
assert ride_recv(self.sock)=='SupportedProtocols=2'
|
|
118
|
+
ride_send(self.sock, 'SupportedProtocols=2')
|
|
119
|
+
ride_send(self.sock, 'UsingProtocol=2')
|
|
120
|
+
assert ride_recv(self.sock)=='UsingProtocol=2'
|
|
121
|
+
ride_send(self.sock, ['Identify',{'apiVersion':1,'identity':1}])
|
|
122
|
+
self.info = ride_recv(self.sock)[1]
|
|
123
|
+
ride_send(self.sock, ['SetPW',{'pw':32767}])
|
|
124
|
+
atexit.register(self.close)
|
|
125
|
+
while True:
|
|
126
|
+
m,a = ride_recv(self.sock)
|
|
127
|
+
if m=='SetPromptType' and a['type']==1: break
|
|
128
|
+
self.sock.settimeout(None) # timeout guards startup only; run() waits as long as the code takes
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# %% ../00_core.ipynb #32eb4980
|
|
132
|
+
@patch
|
|
133
|
+
def close(self:Apl):
|
|
134
|
+
"Shut down the interpreter and close the connection"
|
|
135
|
+
atexit.unregister(self.close)
|
|
136
|
+
try: ride_send(self.sock, ['Exit',{'code':0}])
|
|
137
|
+
except OSError: pass
|
|
138
|
+
self.sock.close()
|
|
139
|
+
try: self.proc.wait(3)
|
|
140
|
+
except subprocess.TimeoutExpired:
|
|
141
|
+
self.proc.kill()
|
|
142
|
+
self.proc.wait()
|
|
143
|
+
|
|
144
|
+
# %% ../00_core.ipynb #cc30073d
|
|
145
|
+
@patch
|
|
146
|
+
def run(self:Apl, code):
|
|
147
|
+
"Run `code` in the session, returning its output; raises `AplError` on APL errors"
|
|
148
|
+
try: out,err = ride_run(self.sock, code)
|
|
149
|
+
except AplPrompt as e:
|
|
150
|
+
if e.ptype in (2,4):
|
|
151
|
+
ride_run(self.sock, '→' if e.ptype==2 else '')
|
|
152
|
+
raise AplError('Input via ⎕ or ⍞ is not supported in iversonnb') from None
|
|
153
|
+
self.close()
|
|
154
|
+
self._connect()
|
|
155
|
+
raise AplError('Incomplete input wedged the Dyalog interpreter; started a fresh session (workspace lost). See Dyalog/ride#1401',
|
|
156
|
+
reset=True) from None
|
|
157
|
+
if err: raise AplError(out)
|
|
158
|
+
return out
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# %% ../00_core.ipynb #07eae6b5
|
|
162
|
+
class AplOut(str):
|
|
163
|
+
"Output text from an `Apl` call; displays verbatim, in the SAX2 APL font where HTML is available"
|
|
164
|
+
def __repr__(self): return str(self)
|
|
165
|
+
def _repr_html_(self): return f'<pre class="iversonnb_out sax2">{html.escape(self.rstrip(chr(10)))}</pre>'
|
|
166
|
+
|
|
167
|
+
@patch
|
|
168
|
+
def __call__(self:Apl, code):
|
|
169
|
+
"Run `code`, returning session output (or None if there is none)"
|
|
170
|
+
return AplOut(self.run(code)) or None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# %% ../00_core.ipynb #a82d16ca
|
|
174
|
+
@patch
|
|
175
|
+
def pyval(self:Apl, expr):
|
|
176
|
+
"Evaluate `expr` and return the result as a Python value"
|
|
177
|
+
return json.loads(self.run(f"1(⎕JSON⍠'HighRank' 'Split')({expr})"))
|
|
178
|
+
|
|
179
|
+
# %% ../00_core.ipynb #4a0c3e58
|
|
180
|
+
def _apljson(v):
|
|
181
|
+
"An APL expression that evaluates to the Python value `v`"
|
|
182
|
+
return "⎕JSON'" + json.dumps(v).replace("'", "''") + "'"
|
|
183
|
+
|
|
184
|
+
@patch
|
|
185
|
+
def __getitem__(self:Apl, expr): return self.pyval(expr)
|
|
186
|
+
|
|
187
|
+
@patch
|
|
188
|
+
def __setitem__(self:Apl, nm, v): self.run(f'{nm}←{_apljson(v)}')
|
|
189
|
+
|
|
190
|
+
# %% ../00_core.ipynb #8a0bb0fe
|
|
191
|
+
@patch
|
|
192
|
+
def fn(self:Apl, code):
|
|
193
|
+
"A Python callable applying APL function `code` monadically or dyadically"
|
|
194
|
+
def f(*args):
|
|
195
|
+
if len(args)==1: return self.pyval(f'({code}){_apljson(args[0])}')
|
|
196
|
+
a,w = args
|
|
197
|
+
return self.pyval(f'({_apljson(a)})({code}){_apljson(w)}')
|
|
198
|
+
return f
|
|
199
|
+
|
|
200
|
+
# %% ../00_core.ipynb #9bd19790
|
|
201
|
+
@patch
|
|
202
|
+
def __enter__(self:Apl): return self
|
|
203
|
+
|
|
204
|
+
@patch
|
|
205
|
+
def __exit__(self:Apl, *args): self.close()
|
|
206
|
+
|
|
207
|
+
# %% ../00_core.ipynb #04f56c87
|
|
208
|
+
_css = """<style>
|
|
209
|
+
@font-face { font-family:'SAX2'; src: local('SAX2'), url('https://cdn.jsdelivr.net/gh/abrudz/SAX2@master/SAX2.ttf') format('truetype') }
|
|
210
|
+
.sax2 { font-family:'SAX2',monospace !important; line-height:1.05 !important }
|
|
211
|
+
</style>"""
|
|
212
|
+
|
|
213
|
+
class APLMagic:
|
|
214
|
+
"IPython `%apl`/`%%apl` magics, driving a lazily-started `Apl` session"
|
|
215
|
+
def __init__(self, dyalog=None): self.dyalog,self.o,self._loaded = dyalog,None,False
|
|
216
|
+
|
|
217
|
+
def apl(self, line, cell=None):
|
|
218
|
+
"Run APL: a cell magic displays the session output; a line magic returns the expression's Python value"
|
|
219
|
+
if not self.o: self.o = Apl(self.dyalog)
|
|
220
|
+
if not self._loaded:
|
|
221
|
+
display(Javascript((files('iversonnb')/'lb.js').read_text()))
|
|
222
|
+
display(HTML(_css))
|
|
223
|
+
self._loaded = True
|
|
224
|
+
if cell is None: return self.o[line.split('⍝')[0].strip()]
|
|
225
|
+
disp,cell = True,cell.rstrip()
|
|
226
|
+
if cell.endswith(';'): disp,cell = False,cell[:-1]
|
|
227
|
+
out = self.o(cell)
|
|
228
|
+
if disp and out: display(out)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# %% ../00_core.ipynb #953c6348
|
|
232
|
+
def create_magic(shell=None):
|
|
233
|
+
"Create an `APLMagic` and register its `apl` line/cell magic with `shell`, returning it"
|
|
234
|
+
if not shell: shell = get_ipython()
|
|
235
|
+
apl_magic = APLMagic()
|
|
236
|
+
shell.register_magic_function(apl_magic.apl, 'line_cell', 'apl')
|
|
237
|
+
return apl_magic
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# %% ../00_core.ipynb #3e0cb6bc
|
|
241
|
+
def load_ipython_extension(ipython):
|
|
242
|
+
"Required function for creating magic"
|
|
243
|
+
create_magic(shell=ipython)
|
|
244
|
+
|
|
245
|
+
# %% ../00_core.ipynb #66869255
|
|
246
|
+
def create_ipython_config():
|
|
247
|
+
"Called by `iversonnb_install` to install magic"
|
|
248
|
+
ipython_dir = Path(get_ipython_dir())
|
|
249
|
+
cf = ipython_dir/'profile_default'/'ipython_config.py'
|
|
250
|
+
cf.parent.mkdir(parents=True, exist_ok=True)
|
|
251
|
+
if cf.exists() and 'iversonnb' in cf.read_text(): return print('iversonnb already installed!')
|
|
252
|
+
with cf.open(mode='a') as f: f.write("\nc.InteractiveShellApp.extensions.append('iversonnb')\n\n")
|
|
253
|
+
print(f"Jupyter config updated at {cf}")
|
iversonnb/j.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Running J in-process through libj, and `j` magics for Jupyter and IPython
|
|
2
|
+
|
|
3
|
+
Docs: https://answerdotai.github.io/iversonnb/j.html.md"""
|
|
4
|
+
|
|
5
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../01_j.ipynb.
|
|
6
|
+
|
|
7
|
+
# %% auto #0
|
|
8
|
+
__all__ = ['find_j', 'load_j', 'JError', 'J', 'JOut', 'JMagic', 'create_j_magic', 'load_ipython_extension']
|
|
9
|
+
|
|
10
|
+
# %% ../01_j.ipynb #7e555789
|
|
11
|
+
import ctypes
|
|
12
|
+
from ctypes import c_void_p,c_char_p,c_int,c_longlong,c_double,c_byte,POINTER,byref,cast,create_string_buffer
|
|
13
|
+
from shutil import which
|
|
14
|
+
from IPython.display import display
|
|
15
|
+
from IPython.utils.capture import capture_output
|
|
16
|
+
from fastcore.utils import *
|
|
17
|
+
from fastcore.test import *
|
|
18
|
+
|
|
19
|
+
# %% ../01_j.ipynb #4f9e0882
|
|
20
|
+
_LIBJ = 'j.dll' if sys.platform=='win32' else 'libj.dylib' if sys.platform=='darwin' else 'libj.so'
|
|
21
|
+
|
|
22
|
+
def find_j():
|
|
23
|
+
"Locate the J binary directory: the one containing libj and profile.ijs"
|
|
24
|
+
if (p:=which('jconsole')) and (d:=Path(p).resolve().parent/_LIBJ).exists(): return d.parent
|
|
25
|
+
try:
|
|
26
|
+
import jlang
|
|
27
|
+
return Path(jlang.path())/'bin'
|
|
28
|
+
except ImportError: pass
|
|
29
|
+
roots = [Path.home(), Path('/Applications'), Path('/opt'), Path('/usr/share')]
|
|
30
|
+
cands = sorted(c for r in roots if r.exists() for c in r.glob('j9*') if (c/'bin'/_LIBJ).exists())
|
|
31
|
+
if cands: return cands[-1]/'bin'
|
|
32
|
+
raise FileNotFoundError('J not found: pip install jlanguage, or install it from jsoftware.com')
|
|
33
|
+
|
|
34
|
+
# %% ../01_j.ipynb #ee362390
|
|
35
|
+
_OUTCB = ctypes.CFUNCTYPE(None, c_void_p, c_int, c_void_p) # Joutput(jt, type, text)
|
|
36
|
+
_INCB = ctypes.CFUNCTYPE(c_void_p, c_void_p, c_char_p) # Jinput(jt, prompt) -> line
|
|
37
|
+
_SMCON = 3 # smoptions: identify as a console front end
|
|
38
|
+
|
|
39
|
+
def load_j(jbin=None):
|
|
40
|
+
"Load libj from `jbin` (or `find_j()`), declare its C signatures, and return `(jbin,lib)`"
|
|
41
|
+
jbin = Path(jbin or find_j())
|
|
42
|
+
lib = ctypes.CDLL(str(jbin/_LIBJ))
|
|
43
|
+
lib.JInit2.restype = c_void_p
|
|
44
|
+
lib.JInit2.argtypes = [c_char_p]
|
|
45
|
+
lib.JDo.argtypes = [c_void_p,c_char_p]
|
|
46
|
+
lib.JSM.argtypes = [c_void_p,c_void_p]
|
|
47
|
+
lib.JInterrupt.argtypes = lib.JFree.argtypes = [c_void_p]
|
|
48
|
+
lib.JGetM.argtypes = lib.JSetM.argtypes = [c_void_p,c_char_p]+[POINTER(c_longlong)]*4
|
|
49
|
+
return jbin,lib
|
|
50
|
+
|
|
51
|
+
# %% ../01_j.ipynb #e8a80100
|
|
52
|
+
class JError(Exception):
|
|
53
|
+
"A J error, carrying the session's output (including the error display) as its message"
|
|
54
|
+
|
|
55
|
+
# %% ../01_j.ipynb #30e60cf7
|
|
56
|
+
class J:
|
|
57
|
+
"A J session: the libj engine loaded in-process, with the stdlib profile booted"
|
|
58
|
+
def __init__(self, jbin=None):
|
|
59
|
+
self.jbin,self._lib = load_j(jbin)
|
|
60
|
+
self.jt = self._lib.JInit2(str(self.jbin).encode())
|
|
61
|
+
self._out,self._inq,self.exited = [],[],None
|
|
62
|
+
@_OUTCB
|
|
63
|
+
def _o(j,typ,p):
|
|
64
|
+
if typ==5: self.exited = p or 0
|
|
65
|
+
else: self._out.append((typ, ctypes.string_at(p).decode('utf-8','replace') if p else ''))
|
|
66
|
+
@_INCB
|
|
67
|
+
def _i(j,prompt):
|
|
68
|
+
self._inbuf = create_string_buffer((self._inq.pop(0) if self._inq else ')').encode())
|
|
69
|
+
return ctypes.addressof(self._inbuf)
|
|
70
|
+
self._cbs = (c_void_p*5)(cast(_o,c_void_p), None, cast(_i,c_void_p), None, _SMCON)
|
|
71
|
+
self._lib.JSM(self.jt, self._cbs)
|
|
72
|
+
boot = f"(3 : '0!:0 y')<BINPATH,'/profile.ijs'[ARGV_z_=:<'jconsole'[BINPATH_z_=:'{self.jbin}'"
|
|
73
|
+
if self._lib.JDo(self.jt, boot.encode()): raise JError(''.join(t for _,t in self._out))
|
|
74
|
+
|
|
75
|
+
# %% ../01_j.ipynb #db9dc5bf
|
|
76
|
+
@patch
|
|
77
|
+
def run(self:J, code):
|
|
78
|
+
"Run `code` (one or more lines of J) in the session, returning its output; raises `JError` on J errors"
|
|
79
|
+
self._out.clear()
|
|
80
|
+
self._inq[:] = code.strip().splitlines()
|
|
81
|
+
while self._inq and self.exited is None:
|
|
82
|
+
rc = self._lib.JDo(self.jt, self._inq.pop(0).encode())
|
|
83
|
+
if rc and self.exited is None: raise JError(''.join(t for _,t in self._out))
|
|
84
|
+
if self.exited is not None: return ''.join(t for ty,t in self._out if ty!=2) # drop the exit unwind noise
|
|
85
|
+
return ''.join(t for _,t in self._out)
|
|
86
|
+
|
|
87
|
+
# %% ../01_j.ipynb #8d7be472
|
|
88
|
+
class JOut(str):
|
|
89
|
+
"Output text from a `J` call; displays verbatim"
|
|
90
|
+
def __repr__(self): return str(self)
|
|
91
|
+
|
|
92
|
+
@patch
|
|
93
|
+
def __call__(self:J, code):
|
|
94
|
+
"Run `code`, returning session output (or None if there is none)"
|
|
95
|
+
return JOut(self.run(code)) or None
|
|
96
|
+
|
|
97
|
+
# %% ../01_j.ipynb #b4ba14ce
|
|
98
|
+
def _nest(x, shape):
|
|
99
|
+
"Nest flat sequence `x` (row-major ravel) into `shape`"
|
|
100
|
+
if len(shape)<2: return x
|
|
101
|
+
n = len(x)//shape[0]
|
|
102
|
+
return [_nest(x[i*n:(i+1)*n], shape[1:]) for i in range(shape[0])]
|
|
103
|
+
|
|
104
|
+
@patch
|
|
105
|
+
def getm(self:J, name):
|
|
106
|
+
"Read noun `name` into Python: str for characters; int/float scalars and (nested) lists otherwise"
|
|
107
|
+
t,r,s,d = c_longlong(),c_longlong(),c_longlong(),c_longlong()
|
|
108
|
+
if self._lib.JGetM(self.jt, name.encode(), byref(t),byref(r),byref(s),byref(d)): raise JError(f'no noun: {name}')
|
|
109
|
+
shape = cast(s.value, POINTER(c_longlong))[:r.value]
|
|
110
|
+
n = math.prod(shape)
|
|
111
|
+
if t.value==2: return _nest(ctypes.string_at(d.value, n).decode('utf-8','replace'), shape)
|
|
112
|
+
if t.value not in (1,4,8): raise JError(f'unsupported J type {t.value} for: {name}')
|
|
113
|
+
x = cast(d.value, POINTER({1:c_byte,4:c_longlong,8:c_double}[t.value]))[:n]
|
|
114
|
+
return _nest(x, shape) if r.value else x[0]
|
|
115
|
+
|
|
116
|
+
# %% ../01_j.ipynb #86f8fb28
|
|
117
|
+
@patch
|
|
118
|
+
def pyval(self:J, expr):
|
|
119
|
+
"Evaluate `expr` and return the result as a Python value"
|
|
120
|
+
self.run(f'jnbtmp =: {expr}')
|
|
121
|
+
try: return self.getm('jnbtmp')
|
|
122
|
+
finally: self.run("4!:55 <'jnbtmp'")
|
|
123
|
+
|
|
124
|
+
@patch
|
|
125
|
+
def __getitem__(self:J, expr): return self.pyval(expr)
|
|
126
|
+
|
|
127
|
+
# %% ../01_j.ipynb #89f0e80f
|
|
128
|
+
def _flat(o): return [a for x in o for a in _flat(x)] if isinstance(o,(list,tuple)) else [o]
|
|
129
|
+
|
|
130
|
+
def _jdat(v):
|
|
131
|
+
"ctypes `(type,shape,buf)` for a Python scalar, string, or uniformly nested list"
|
|
132
|
+
if isinstance(v,str):
|
|
133
|
+
b = v.encode()
|
|
134
|
+
return 2, [len(b)], create_string_buffer(b, len(b))
|
|
135
|
+
shape,x = [],v
|
|
136
|
+
while isinstance(x,(list,tuple)): shape,x = shape+[len(x)],x[0]
|
|
137
|
+
flat = _flat(v)
|
|
138
|
+
if any(isinstance(a,float) for a in flat): return 8, shape, (c_double*len(flat))(*flat)
|
|
139
|
+
return 4, shape, (c_longlong*len(flat))(*flat)
|
|
140
|
+
|
|
141
|
+
@patch
|
|
142
|
+
def __setitem__(self:J, nm, v):
|
|
143
|
+
"Assign Python value `v` (scalar, string, or nested list) to noun `nm`"
|
|
144
|
+
t,shape,buf = _jdat(v)
|
|
145
|
+
sh = (c_longlong*len(shape))(*shape)
|
|
146
|
+
a = [c_longlong(t), c_longlong(len(shape)), c_longlong(ctypes.addressof(sh)), c_longlong(ctypes.addressof(buf))]
|
|
147
|
+
if self._lib.JSetM(self.jt, nm.encode(), *map(byref,a)): raise JError(f'JSetM failed: {nm}')
|
|
148
|
+
|
|
149
|
+
# %% ../01_j.ipynb #b3270978
|
|
150
|
+
@patch
|
|
151
|
+
def fn(self:J, code):
|
|
152
|
+
"A Python callable applying J verb `code` monadically or dyadically (left argument first)"
|
|
153
|
+
def f(*args):
|
|
154
|
+
if len(args)==1:
|
|
155
|
+
self['jnby'] = args[0]
|
|
156
|
+
return self.pyval(f'({code}) jnby')
|
|
157
|
+
self['jnbx'],self['jnby'] = args
|
|
158
|
+
return self.pyval(f'jnbx ({code}) jnby')
|
|
159
|
+
return f
|
|
160
|
+
|
|
161
|
+
# %% ../01_j.ipynb #79911134
|
|
162
|
+
@patch
|
|
163
|
+
def interrupt(self:J):
|
|
164
|
+
"Stop the currently running sentence with an attention interrupt; safe to call from another thread"
|
|
165
|
+
self._lib.JInterrupt(self.jt)
|
|
166
|
+
|
|
167
|
+
# %% ../01_j.ipynb #af540693
|
|
168
|
+
@patch
|
|
169
|
+
def close(self:J):
|
|
170
|
+
"Free the engine instance; the session is unusable afterwards"
|
|
171
|
+
self._lib.JFree(self.jt)
|
|
172
|
+
self.jt = None
|
|
173
|
+
|
|
174
|
+
@patch
|
|
175
|
+
def __enter__(self:J): return self
|
|
176
|
+
|
|
177
|
+
@patch
|
|
178
|
+
def __exit__(self:J, *args): self.close()
|
|
179
|
+
|
|
180
|
+
# %% ../01_j.ipynb #12c1ebee
|
|
181
|
+
class JMagic:
|
|
182
|
+
"IPython `%j`/`%%j` magics, driving a lazily-started `J` session"
|
|
183
|
+
def __init__(self, jbin=None): self.jbin,self.o = jbin,None
|
|
184
|
+
|
|
185
|
+
def j(self, line, cell=None):
|
|
186
|
+
"Run J: a cell magic displays the session output; a line magic returns the expression's Python value"
|
|
187
|
+
if not self.o: self.o = J(self.jbin)
|
|
188
|
+
if cell is None: return self.o[line.strip()]
|
|
189
|
+
disp,cell = True,cell.rstrip()
|
|
190
|
+
if cell.endswith(';'): disp,cell = False,cell[:-1]
|
|
191
|
+
out = self.o(cell)
|
|
192
|
+
if disp and out: display(out)
|
|
193
|
+
|
|
194
|
+
# %% ../01_j.ipynb #00b65478
|
|
195
|
+
def create_j_magic(shell=None):
|
|
196
|
+
"Create a `JMagic` and register its `j` line/cell magic with `shell`, returning it"
|
|
197
|
+
if not shell: shell = get_ipython()
|
|
198
|
+
jm = JMagic()
|
|
199
|
+
shell.register_magic_function(jm.j, 'line_cell', 'j')
|
|
200
|
+
return jm
|
|
201
|
+
|
|
202
|
+
def load_ipython_extension(ipython):
|
|
203
|
+
"Required function for creating magic"
|
|
204
|
+
create_j_magic(shell=ipython)
|
iversonnb/jkernel.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"J clikernel: a stream-protocol worker and MCP supervisor driving J in-process via `J` (libj). Hand-written (not nbdev-exported); see `clikernel.base` for the protocol."
|
|
2
|
+
import signal,sys,threading,traceback
|
|
3
|
+
from fastcore.xdg import xdg_config_home
|
|
4
|
+
from clikernel.base import fmt_error,init_worker,run_mcp,run_startup,serve_stream
|
|
5
|
+
from iversonnb.j import J,JError
|
|
6
|
+
|
|
7
|
+
INSTRUCTIONS = "jkernel is a persistent J session: `execute` runs J code and keeps workspace state (nouns, verbs) across calls; `restart` gives a clear workspace. If a response starts with a NOTE saying the session was replaced, all workspace state is gone."
|
|
8
|
+
|
|
9
|
+
_DOCS = dict(
|
|
10
|
+
execute="Run J `code` in the persistent J session, keeping workspace state (nouns, verbs) across calls.",
|
|
11
|
+
restart="Kill the worker and start a fresh J session with a clear workspace; all state is discarded. Also works when `execute` is stuck: the stuck call returns an error and the session comes back fresh.",
|
|
12
|
+
interrupt="Interrupt the J code the kernel is currently running (a J attention interrupt): the in-flight `execute` call returns with an attention interrupt error, and workspace state survives. Prefer this over `restart` when a call is merely taking too long. Only meaningful while an `execute` call is running.",
|
|
13
|
+
died="NOTE: the kernel process had died; a fresh one was started, and all previous workspace state (nouns, verbs) is gone.\n")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _execute(j, code):
|
|
17
|
+
try: return j.run(code)
|
|
18
|
+
except JError as e: return fmt_error("error", str(e))
|
|
19
|
+
except Exception: return fmt_error("error", traceback.format_exc())
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main():
|
|
23
|
+
init_worker()
|
|
24
|
+
signal.signal(signal.SIGTERM, lambda s,f: sys.exit(0))
|
|
25
|
+
j = J()
|
|
26
|
+
state = dict(busy=False)
|
|
27
|
+
# JDo blocks its calling thread at the C level, so each request runs on a worker thread and the main
|
|
28
|
+
# thread joins in slices, staying free to run the SIGINT handler: JInterrupt is just a break-flag
|
|
29
|
+
# write (safe cross-thread), and the engine polls it between sentences, ending the in-flight JDo.
|
|
30
|
+
def _sigint(s,f):
|
|
31
|
+
if state['busy']: j.interrupt()
|
|
32
|
+
signal.signal(signal.SIGINT, _sigint)
|
|
33
|
+
def execute(code):
|
|
34
|
+
res = {}
|
|
35
|
+
th = threading.Thread(target=lambda: res.update(out=_execute(j, code)), daemon=True)
|
|
36
|
+
state['busy'] = True
|
|
37
|
+
try:
|
|
38
|
+
th.start()
|
|
39
|
+
while th.is_alive(): th.join(0.1)
|
|
40
|
+
finally: state['busy'] = False
|
|
41
|
+
return res['out']
|
|
42
|
+
block = run_startup(xdg_config_home()/"iversonnb"/"startup.ijs", lambda src: (j.run(src), None))
|
|
43
|
+
serve_stream(execute, INSTRUCTIONS + ("\n\n" + block if block else ""), should_exit=lambda: j.exited is not None)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main_mcp(): run_mcp([sys.executable, "-m", "iversonnb.jkernel"], name="jkernel", docs=_DOCS, instructions=INSTRUCTIONS)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__": main()
|
iversonnb/kernel.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"APL clikernel: a stream-protocol worker and MCP supervisor driving Dyalog over RIDE via `Apl`. Hand-written (not nbdev-exported); see `clikernel.base` for the protocol."
|
|
2
|
+
import signal,sys,traceback
|
|
3
|
+
from fastcore.xdg import xdg_config_home
|
|
4
|
+
from clikernel.base import fmt_error,init_worker,run_mcp,run_startup,serve_stream
|
|
5
|
+
from iversonnb.core import Apl,AplError,ride_send
|
|
6
|
+
|
|
7
|
+
INSTRUCTIONS = "aplkernel is a persistent Dyalog APL session: `execute` runs APL code and keeps workspace state (variables, functions) across calls; `restart` gives a clear workspace. If a response starts with a NOTE saying the session was replaced, all workspace state is gone."
|
|
8
|
+
|
|
9
|
+
_RESET = "NOTE: the Dyalog interpreter was replaced with a fresh session; all previous workspace state (variables, functions) is gone.\n"
|
|
10
|
+
|
|
11
|
+
_DOCS = dict(
|
|
12
|
+
execute="Run APL `code` in the persistent Dyalog session, keeping workspace state (variables, functions) across calls. If the session had to be replaced (the interpreter died, or incomplete input hit Dyalog/ride#1401), the response starts with a NOTE saying workspace state was lost.",
|
|
13
|
+
restart="Kill the worker and start a fresh Dyalog session with a clear workspace; all state is discarded. Also works when `execute` is stuck: the stuck call returns an error and the session comes back fresh.",
|
|
14
|
+
interrupt="Interrupt the APL code the kernel is currently running (a RIDE StrongInterrupt): the in-flight `execute` call returns with an INTERRUPT error, and workspace state survives. Prefer this over `restart` when a call is merely taking too long. Only meaningful while an `execute` call is running.",
|
|
15
|
+
died="NOTE: the kernel process had died; a fresh one was started, and all previous workspace state (variables, functions) is gone.\n")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _execute(apl, code):
|
|
19
|
+
try: return apl.run(code)
|
|
20
|
+
except AplError as e:
|
|
21
|
+
body = fmt_error("error", str(e))
|
|
22
|
+
return _RESET + body if e.reset else body
|
|
23
|
+
except Exception:
|
|
24
|
+
apl.close()
|
|
25
|
+
apl._connect()
|
|
26
|
+
return _RESET + fmt_error("error",
|
|
27
|
+
f"the Dyalog session died while executing this request; a fresh session was started\n{traceback.format_exc()}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main():
|
|
31
|
+
init_worker()
|
|
32
|
+
# SIGTERM (the supervisor's graceful kill) becomes SystemExit, so atexit shuts Dyalog down.
|
|
33
|
+
# SIGINT becomes a RIDE StrongInterrupt when a request is running (see the probe notes in tests):
|
|
34
|
+
# the interpreter answers with an INTERRUPT error and the session survives; when idle it's a no-op,
|
|
35
|
+
# since an idle StrongInterrupt is silent but a stray KeyboardInterrupt would desync the stream.
|
|
36
|
+
signal.signal(signal.SIGTERM, lambda s,f: sys.exit(0))
|
|
37
|
+
apl = Apl()
|
|
38
|
+
state = dict(exit=False, busy=False)
|
|
39
|
+
def _sigint(s,f):
|
|
40
|
+
if state['busy']: ride_send(apl.sock, ['StrongInterrupt',{}])
|
|
41
|
+
signal.signal(signal.SIGINT, _sigint)
|
|
42
|
+
def execute(code):
|
|
43
|
+
if code.strip().upper() == ')OFF':
|
|
44
|
+
state['exit'] = True
|
|
45
|
+
return ""
|
|
46
|
+
state['busy'] = True
|
|
47
|
+
try: return _execute(apl, code)
|
|
48
|
+
finally: state['busy'] = False
|
|
49
|
+
block = run_startup(xdg_config_home()/"iversonnb"/"startup.apl", lambda src: (apl.run(src), None))
|
|
50
|
+
serve_stream(execute, INSTRUCTIONS + ("\n\n" + block if block else ""), should_exit=lambda: state['exit'])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main_mcp(): run_mcp([sys.executable, "-m", "iversonnb.kernel"], name="aplkernel", docs=_DOCS, instructions=INSTRUCTIONS)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__": main()
|