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 ADDED
@@ -0,0 +1,9 @@
1
+ __version__ = "0.2.0"
2
+
3
+ from .core import *
4
+ from .j import create_j_magic
5
+
6
+ def load_ipython_extension(ipython):
7
+ "Register both the `apl` and `j` magics"
8
+ create_magic(shell=ipython)
9
+ create_j_magic(shell=ipython)
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()