klongpy 0.6.9__py3-none-any.whl → 0.7.1__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.
- klongpy/__init__.py +17 -1
- klongpy/adverbs.py +84 -82
- klongpy/autograd.py +299 -0
- klongpy/backend.py +38 -103
- klongpy/backends/__init__.py +26 -0
- klongpy/backends/base.py +469 -0
- klongpy/backends/numpy_backend.py +123 -0
- klongpy/backends/registry.py +76 -0
- klongpy/backends/torch_backend.py +1047 -0
- klongpy-0.6.9.data/scripts/kgpy → klongpy/cli.py +110 -90
- klongpy/core.py +113 -974
- klongpy/db/sys_fn_db.py +7 -6
- klongpy/db/sys_fn_kvs.py +2 -4
- klongpy/dyads.py +332 -160
- klongpy/interpreter.py +60 -15
- klongpy/monads.py +121 -75
- klongpy/parser.py +328 -0
- klongpy/repl.py +23 -5
- klongpy/sys_fn.py +170 -21
- klongpy/sys_fn_autograd.py +290 -0
- klongpy/sys_fn_ipc.py +22 -15
- klongpy/sys_fn_timer.py +13 -3
- klongpy/types.py +503 -0
- klongpy/web/sys_fn_web.py +14 -4
- klongpy/writer.py +122 -0
- klongpy/ws/sys_fn_ws.py +5 -8
- klongpy-0.7.1.dist-info/METADATA +544 -0
- klongpy-0.7.1.dist-info/RECORD +52 -0
- {klongpy-0.6.9.dist-info → klongpy-0.7.1.dist-info}/WHEEL +1 -1
- klongpy-0.7.1.dist-info/entry_points.txt +2 -0
- {klongpy-0.6.9.dist-info → klongpy-0.7.1.dist-info}/top_level.txt +0 -1
- klongpy-0.6.9.dist-info/METADATA +0 -448
- klongpy-0.6.9.dist-info/RECORD +0 -77
- tests/__init__.py +0 -6
- tests/gen_join_over.py +0 -119
- tests/gen_py_suite.py +0 -77
- tests/gen_test_fn.py +0 -259
- tests/perf_async.py +0 -25
- tests/perf_avg.py +0 -18
- tests/perf_duckdb.py +0 -32
- tests/perf_gen.py +0 -38
- tests/perf_ipc_overhead.py +0 -34
- tests/perf_join.py +0 -53
- tests/perf_load.py +0 -17
- tests/perf_prog.py +0 -18
- tests/perf_serdes.py +0 -52
- tests/perf_sys_fn_db.py +0 -263
- tests/perf_vector.py +0 -40
- tests/test_accel.py +0 -227
- tests/test_df_cache.py +0 -85
- tests/test_eval_monad_list.py +0 -34
- tests/test_examples.py +0 -64
- tests/test_extra_suite.py +0 -382
- tests/test_file_cache.py +0 -185
- tests/test_interop.py +0 -180
- tests/test_kg_asarray.py +0 -94
- tests/test_kgtests.py +0 -65
- tests/test_known_bugs.py +0 -206
- tests/test_prog.py +0 -107
- tests/test_reshape_strings.py +0 -33
- tests/test_suite.py +0 -1480
- tests/test_suite_file.py +0 -153
- tests/test_sys_fn.py +0 -420
- tests/test_sys_fn_db.py +0 -88
- tests/test_sys_fn_ipc.py +0 -587
- tests/test_sys_fn_timer.py +0 -133
- tests/test_sys_fn_web.py +0 -50
- tests/test_util.py +0 -233
- tests/utils.py +0 -126
- {klongpy-0.6.9.dist-info → klongpy-0.7.1.dist-info}/licenses/LICENSE +0 -0
klongpy/types.py
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
"""
|
|
2
|
+
KlongPy type definitions and type checking functions.
|
|
3
|
+
|
|
4
|
+
This module contains all the core types used by the KlongPy interpreter:
|
|
5
|
+
- KGSym: Symbol type
|
|
6
|
+
- KGChar: Character type
|
|
7
|
+
- KGFn, KGCall: Function types
|
|
8
|
+
- KGLambda: Lambda wrapper
|
|
9
|
+
- KGOp, KGAdverb: Operator types
|
|
10
|
+
- KGChannel: I/O channel type
|
|
11
|
+
- Type checking utilities
|
|
12
|
+
"""
|
|
13
|
+
import inspect
|
|
14
|
+
import weakref
|
|
15
|
+
from enum import Enum
|
|
16
|
+
import numpy
|
|
17
|
+
|
|
18
|
+
from .backend import np
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# python3.11 support
|
|
22
|
+
if not hasattr(inspect, 'getargspec'):
|
|
23
|
+
inspect.getargspec = inspect.getfullargspec
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class KlongException(Exception):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class KGSym(str):
|
|
31
|
+
def __repr__(self):
|
|
32
|
+
return f":{super().__str__()}"
|
|
33
|
+
def __eq__(self, o):
|
|
34
|
+
return isinstance(o,KGSym) and self.__str__() == o.__str__()
|
|
35
|
+
def __hash__(self):
|
|
36
|
+
return super().__hash__()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_fn_arity_str(arity):
|
|
40
|
+
if arity == 0:
|
|
41
|
+
return ":nilad"
|
|
42
|
+
elif arity == 1:
|
|
43
|
+
return ":monad"
|
|
44
|
+
elif arity == 2:
|
|
45
|
+
return ":dyad"
|
|
46
|
+
return ":triad"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class KGFn:
|
|
50
|
+
def __init__(self, a, args, arity, global_params=None):
|
|
51
|
+
self.a = a
|
|
52
|
+
self.args = args
|
|
53
|
+
self.arity = arity
|
|
54
|
+
self.global_params = global_params or set()
|
|
55
|
+
|
|
56
|
+
def __str__(self):
|
|
57
|
+
return get_fn_arity_str(self.arity)
|
|
58
|
+
|
|
59
|
+
def is_op(self):
|
|
60
|
+
return isinstance(self.a, KGOp)
|
|
61
|
+
|
|
62
|
+
def is_adverb_chain(self):
|
|
63
|
+
return isinstance(self.a, list) and isinstance(self.a[0], KGAdverb)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class KGFnWrapper:
|
|
67
|
+
"""
|
|
68
|
+
Wrapper for KGFn that enables calling from Python with dynamic symbol resolution.
|
|
69
|
+
|
|
70
|
+
When a KGFn is stored and later invoked, this wrapper automatically re-resolves
|
|
71
|
+
the symbol to use the current function definition. This matches k4 behavior and
|
|
72
|
+
provides REPL-friendly semantics where function redefinitions take effect immediately.
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
fn = klong['callback'] # Returns KGFnWrapper
|
|
76
|
+
klong('callback::{new implementation}')
|
|
77
|
+
fn(args) # Uses the NEW implementation
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, klong, fn, sym=None):
|
|
81
|
+
self.klong = klong
|
|
82
|
+
self.fn = fn
|
|
83
|
+
# Use provided symbol name (cached) or search for it
|
|
84
|
+
self._sym = sym if sym is not None else self._find_symbol(fn)
|
|
85
|
+
|
|
86
|
+
def _find_symbol(self, fn):
|
|
87
|
+
"""Find which symbol this function is currently bound to"""
|
|
88
|
+
if not isinstance(fn, KGFn) or isinstance(fn, KGCall):
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
# Search the context for this function
|
|
92
|
+
# Skip reserved symbols (x, y, z, .f) which are function parameters, not stored callbacks
|
|
93
|
+
for sym, value in self.klong._context:
|
|
94
|
+
# Skip reserved symbols - use the module constants
|
|
95
|
+
if sym in reserved_fn_symbols or sym == reserved_dot_f_symbol:
|
|
96
|
+
continue
|
|
97
|
+
if value is fn:
|
|
98
|
+
return sym
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
def __call__(self, *args, **kwargs):
|
|
102
|
+
# Try to resolve dynamically first if we have a symbol
|
|
103
|
+
if self._sym is not None:
|
|
104
|
+
try:
|
|
105
|
+
current = self.klong._context[self._sym]
|
|
106
|
+
if isinstance(current, KGFn) and not isinstance(current, KGCall):
|
|
107
|
+
# Use the current definition
|
|
108
|
+
if len(args) != current.arity:
|
|
109
|
+
raise RuntimeError(f"Klong function called with {len(args)} but expected {current.arity}")
|
|
110
|
+
fn_args = [np.asarray(x) if isinstance(x, list) else x for x in args]
|
|
111
|
+
return self.klong.call(KGCall(current.a, [*fn_args], current.arity))
|
|
112
|
+
except KeyError:
|
|
113
|
+
# Symbol was deleted, fall through to original function
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
if len(args) != self.fn.arity:
|
|
117
|
+
raise RuntimeError(f"Klong function called with {len(args)} but expected {self.fn.arity}")
|
|
118
|
+
fn_args = [np.asarray(x) if isinstance(x, list) else x for x in args]
|
|
119
|
+
return self.klong.call(KGCall(self.fn.a, [*fn_args], self.fn.arity))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class KGCall(KGFn):
|
|
123
|
+
def __str__(self):
|
|
124
|
+
return self.a.__str__() if issubclass(type(self.a), KGLambda) else super().__str__()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class KGOp:
|
|
128
|
+
def __init__(self, a, arity):
|
|
129
|
+
self.a = a
|
|
130
|
+
self.arity = arity
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class KGAdverb:
|
|
134
|
+
def __init__(self, a, arity):
|
|
135
|
+
self.a = a
|
|
136
|
+
self.arity = arity
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class KGChar(str):
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class KGCond(list):
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class KGUndefined:
|
|
148
|
+
def __repr__(self):
|
|
149
|
+
return ":undefined"
|
|
150
|
+
|
|
151
|
+
def __str__(self):
|
|
152
|
+
return ":undefined"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
KLONG_UNDEFINED = KGUndefined()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def safe_inspect(fn, follow_wrapped=True):
|
|
159
|
+
try:
|
|
160
|
+
return inspect.signature(fn, follow_wrapped=follow_wrapped).parameters
|
|
161
|
+
except ValueError:
|
|
162
|
+
return {"args":[]}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class KGLambda:
|
|
166
|
+
"""
|
|
167
|
+
KGLambda wraps a lambda and make it available to Klong, allowing for direct
|
|
168
|
+
integration of python functions in Klong.
|
|
169
|
+
|
|
170
|
+
Introspection is used to infer which parameters should be collected from the
|
|
171
|
+
current context and passed to the lambda. Parameter names must be x,y, or z
|
|
172
|
+
according to the klong convention. The value for these parameters are
|
|
173
|
+
extracted directly from the currentcontext.
|
|
174
|
+
|
|
175
|
+
If a lambda requires access to klong itself, that must be the first parameter.
|
|
176
|
+
|
|
177
|
+
Function arity is computed by examining the arguments.
|
|
178
|
+
|
|
179
|
+
e.g.
|
|
180
|
+
|
|
181
|
+
lambda x,y: x + y
|
|
182
|
+
lambda klong, x: klong(x)
|
|
183
|
+
|
|
184
|
+
"""
|
|
185
|
+
def __init__(self, fn, args=None, provide_klong=False, wildcard=False):
|
|
186
|
+
self.fn = fn
|
|
187
|
+
params = args or safe_inspect(fn)
|
|
188
|
+
self.args = [reserved_fn_symbol_map[x] for x in reserved_fn_args if x in params]
|
|
189
|
+
self._provide_klong = provide_klong or 'klong' in params
|
|
190
|
+
self._wildcard = wildcard
|
|
191
|
+
|
|
192
|
+
def _get_pos_args(self, ctx):
|
|
193
|
+
if self._wildcard:
|
|
194
|
+
pos_args = []
|
|
195
|
+
for sym in reserved_fn_symbols:
|
|
196
|
+
try:
|
|
197
|
+
pos_args.append(ctx[sym])
|
|
198
|
+
except KeyError:
|
|
199
|
+
break
|
|
200
|
+
else:
|
|
201
|
+
pos_args = [ctx[x] for x in self.args]
|
|
202
|
+
return pos_args
|
|
203
|
+
|
|
204
|
+
def __call__(self, klong, ctx):
|
|
205
|
+
pos_args = self._get_pos_args(ctx)
|
|
206
|
+
return self.fn(klong, *pos_args) if self._provide_klong else self.fn(*pos_args)
|
|
207
|
+
|
|
208
|
+
def call_with_kwargs(self, klong, ctx, kwargs):
|
|
209
|
+
pos_args = self._get_pos_args(ctx)
|
|
210
|
+
return self.fn(klong, *pos_args, **kwargs) if self._provide_klong else self.fn(*pos_args, **kwargs)
|
|
211
|
+
|
|
212
|
+
def get_arity(self):
|
|
213
|
+
return len(self.args)
|
|
214
|
+
|
|
215
|
+
def __str__(self):
|
|
216
|
+
return get_fn_arity_str(self.get_arity())
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class KGChannelDir(Enum):
|
|
220
|
+
INPUT=1
|
|
221
|
+
OUTPUT=2
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class KGChannel:
|
|
225
|
+
class FileHandler:
|
|
226
|
+
def __init__(self, raw, parent):
|
|
227
|
+
self._raw = raw
|
|
228
|
+
self._ref = weakref.ref(parent, self.close)
|
|
229
|
+
|
|
230
|
+
def close(self, *args):
|
|
231
|
+
self._raw.close()
|
|
232
|
+
|
|
233
|
+
def __init__(self, raw, channel_dir):
|
|
234
|
+
self.channel_dir = channel_dir
|
|
235
|
+
self.raw = raw
|
|
236
|
+
self._fh = KGChannel.FileHandler(raw, self)
|
|
237
|
+
self.at_eof = False
|
|
238
|
+
|
|
239
|
+
def __enter__(self):
|
|
240
|
+
return self
|
|
241
|
+
|
|
242
|
+
def __exit__(self, ext_type, exc_value, traceback):
|
|
243
|
+
self._fh.close()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class RangeError(Exception):
|
|
247
|
+
def __init__(self, i):
|
|
248
|
+
self.i = i
|
|
249
|
+
super().__init__()
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# Reserved function argument names and symbols
|
|
253
|
+
reserved_fn_args = ['x','y','z']
|
|
254
|
+
reserved_fn_symbols = [KGSym(n) for n in reserved_fn_args]
|
|
255
|
+
reserved_fn_symbol_map = {n:KGSym(n) for n in reserved_fn_args}
|
|
256
|
+
reserved_dot_f_symbol = KGSym('.f')
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# Type checking functions
|
|
260
|
+
|
|
261
|
+
def is_list(x):
|
|
262
|
+
# Check for list or any array-like with ndim > 0 (works for numpy and torch)
|
|
263
|
+
return isinstance(x, list) or (hasattr(x, 'ndim') and x.ndim > 0)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def is_iterable(x):
|
|
267
|
+
return is_list(x) or (isinstance(x, str) and not isinstance(x, (KGSym, KGChar)))
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def is_empty(a):
|
|
271
|
+
return is_iterable(a) and len(a) == 0
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def is_dict(x):
|
|
275
|
+
return isinstance(x, dict)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def to_list(a):
|
|
279
|
+
return a if isinstance(a, list) else a.tolist() if np.isarray(a) else [a]
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def is_integer(x, backend):
|
|
283
|
+
if issubclass(type(x), (int, numpy.integer)):
|
|
284
|
+
return True
|
|
285
|
+
# Handle 0-dim numpy arrays
|
|
286
|
+
if isinstance(x, numpy.ndarray) and x.ndim == 0:
|
|
287
|
+
return numpy.issubdtype(x.dtype, numpy.integer)
|
|
288
|
+
# Handle backend-specific scalar integers (e.g., torch tensors)
|
|
289
|
+
return backend.is_scalar_integer(x)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def is_float(x, backend):
|
|
293
|
+
if issubclass(type(x), (float, numpy.floating, int)):
|
|
294
|
+
return True
|
|
295
|
+
# Handle 0-dim numpy arrays
|
|
296
|
+
if isinstance(x, numpy.ndarray) and x.ndim == 0:
|
|
297
|
+
return numpy.issubdtype(x.dtype, numpy.floating)
|
|
298
|
+
# Handle backend-specific scalar floats (e.g., torch tensors)
|
|
299
|
+
return backend.is_scalar_float(x)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def is_number(a, backend):
|
|
303
|
+
if is_float(a, backend) or is_integer(a, backend):
|
|
304
|
+
return True
|
|
305
|
+
# Handle 0-dim numpy arrays
|
|
306
|
+
if isinstance(a, numpy.ndarray) and a.ndim == 0:
|
|
307
|
+
return numpy.issubdtype(a.dtype, numpy.number)
|
|
308
|
+
# Handle 0-dim backend tensors as numbers
|
|
309
|
+
if backend.is_backend_array(a) and hasattr(a, 'ndim') and a.ndim == 0:
|
|
310
|
+
return True
|
|
311
|
+
return False
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def str_is_float(b):
|
|
315
|
+
try:
|
|
316
|
+
float(b)
|
|
317
|
+
return True
|
|
318
|
+
except ValueError:
|
|
319
|
+
return False
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def is_symbolic(c):
|
|
323
|
+
return isinstance(c, str) and (c.isalpha() or c.isdigit() or c == '.')
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def is_char(x):
|
|
327
|
+
# Check for both core and backend KGChar classes
|
|
328
|
+
if isinstance(x, KGChar):
|
|
329
|
+
return True
|
|
330
|
+
# Also check for backend KGChar (in case they're different classes)
|
|
331
|
+
return type(x).__name__ == 'KGChar' and isinstance(x, str)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def is_atom(x):
|
|
335
|
+
""" All objects except for non-empty lists and non-empty strings are atoms. """
|
|
336
|
+
return is_empty(x) if is_iterable(x) else True
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def kg_truth(x):
|
|
340
|
+
return x*1
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def str_to_chr_arr(s, backend):
|
|
344
|
+
"""
|
|
345
|
+
Convert string to character array.
|
|
346
|
+
|
|
347
|
+
Parameters
|
|
348
|
+
----------
|
|
349
|
+
s : str
|
|
350
|
+
The string to convert.
|
|
351
|
+
backend : BackendProvider
|
|
352
|
+
The backend to use.
|
|
353
|
+
|
|
354
|
+
Returns
|
|
355
|
+
-------
|
|
356
|
+
array
|
|
357
|
+
Array of KGChar objects.
|
|
358
|
+
|
|
359
|
+
Raises
|
|
360
|
+
------
|
|
361
|
+
UnsupportedDtypeError
|
|
362
|
+
If the backend doesn't support string operations.
|
|
363
|
+
"""
|
|
364
|
+
return backend.str_to_char_array(s)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def get_dtype_kind(arr, backend):
|
|
368
|
+
"""
|
|
369
|
+
Get the dtype 'kind' character for an array (numpy or torch).
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
'O' for object dtype
|
|
373
|
+
'i' for integer types
|
|
374
|
+
'f' for float types
|
|
375
|
+
'u' for unsigned integer
|
|
376
|
+
'b' for boolean
|
|
377
|
+
'c' for complex
|
|
378
|
+
"""
|
|
379
|
+
return backend.get_dtype_kind(arr)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# Utility functions
|
|
383
|
+
|
|
384
|
+
def safe_eq(a, b):
|
|
385
|
+
return isinstance(a, type(b)) and a == b
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def in_map(x, v):
|
|
389
|
+
try:
|
|
390
|
+
return x in v
|
|
391
|
+
except Exception:
|
|
392
|
+
return False
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def has_none(a):
|
|
396
|
+
if isinstance(a, list):
|
|
397
|
+
for q in a:
|
|
398
|
+
if q is None:
|
|
399
|
+
return True
|
|
400
|
+
return False
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def rec_flatten(a):
|
|
404
|
+
if is_list(a) and len(a) > 0:
|
|
405
|
+
return np.concatenate([rec_flatten(x) if is_list(x) else np.array([x]) for x in a]).ravel()
|
|
406
|
+
return a
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# Adverb utilities
|
|
410
|
+
|
|
411
|
+
def is_adverb(s):
|
|
412
|
+
return s in {
|
|
413
|
+
"'",
|
|
414
|
+
':\\',
|
|
415
|
+
":'",
|
|
416
|
+
':/',
|
|
417
|
+
'/',
|
|
418
|
+
':~',
|
|
419
|
+
':*',
|
|
420
|
+
'\\',
|
|
421
|
+
'\\~',
|
|
422
|
+
'\\*'
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def get_adverb_arity(s, ctx):
|
|
427
|
+
if s == "'":
|
|
428
|
+
return ctx
|
|
429
|
+
elif s == ':\\':
|
|
430
|
+
return 2
|
|
431
|
+
elif s == ':\'':
|
|
432
|
+
return 2
|
|
433
|
+
elif s == ':/':
|
|
434
|
+
return 2
|
|
435
|
+
elif s == '/':
|
|
436
|
+
return 2
|
|
437
|
+
elif s == ':~':
|
|
438
|
+
return 1
|
|
439
|
+
elif s == ':*':
|
|
440
|
+
return 1
|
|
441
|
+
elif s == '\\':
|
|
442
|
+
return 2
|
|
443
|
+
elif s == '\\~':
|
|
444
|
+
return 1
|
|
445
|
+
elif s == '\\*':
|
|
446
|
+
return 1
|
|
447
|
+
raise RuntimeError(f"unknown adverb: {s}")
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
# Function utilities
|
|
451
|
+
|
|
452
|
+
def merge_projections(arr):
|
|
453
|
+
"""
|
|
454
|
+
A projection is a new function that is created by projecting an
|
|
455
|
+
existing function onto at least one of its arguments, resulting
|
|
456
|
+
in the partial application of the original function.
|
|
457
|
+
"""
|
|
458
|
+
if len(arr) == 0:
|
|
459
|
+
return arr
|
|
460
|
+
if len(arr) == 1 or not has_none(arr[0]):
|
|
461
|
+
return arr[0]
|
|
462
|
+
sparse_fa = np.copy(arr[0])
|
|
463
|
+
i = 0
|
|
464
|
+
k = 1
|
|
465
|
+
while i < len(sparse_fa) and k < len(arr):
|
|
466
|
+
fa = arr[k]
|
|
467
|
+
j = 0
|
|
468
|
+
while i < len(sparse_fa) and j < len(fa):
|
|
469
|
+
if sparse_fa[i] is None:
|
|
470
|
+
sparse_fa[i] = fa[j]
|
|
471
|
+
j += 1
|
|
472
|
+
while j < len(fa) and safe_eq(fa[j], None):
|
|
473
|
+
j += 1
|
|
474
|
+
i += 1
|
|
475
|
+
k += 1
|
|
476
|
+
return sparse_fa
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def get_fn_arity(f):
|
|
480
|
+
"""
|
|
481
|
+
Examine a function AST and infer arity by looking for x,y and z.
|
|
482
|
+
This arity is needed to populate the KGFn.
|
|
483
|
+
|
|
484
|
+
NOTE: TODO: it maybe easier / better to do this at parse time vs late.
|
|
485
|
+
"""
|
|
486
|
+
if isinstance(f, KGFn) and isinstance(f.a, KGSym) and not in_map(f.a, reserved_fn_symbols):
|
|
487
|
+
return sum(1 for x in set(f.args) if in_map(x, reserved_fn_symbols) or (x is None))
|
|
488
|
+
def _e(f, level=0):
|
|
489
|
+
if isinstance(f, KGFn):
|
|
490
|
+
x = _e(f.a, level=1)
|
|
491
|
+
if isinstance(f.args, list):
|
|
492
|
+
for q in f.args:
|
|
493
|
+
x.update(_e(q, level=1))
|
|
494
|
+
elif isinstance(f, list):
|
|
495
|
+
x = set()
|
|
496
|
+
for q in f:
|
|
497
|
+
x.update(_e(q, level=1))
|
|
498
|
+
elif isinstance(f, KGSym):
|
|
499
|
+
x = set([f]) if f in reserved_fn_symbols else set()
|
|
500
|
+
else:
|
|
501
|
+
x = set()
|
|
502
|
+
return x if level else len(x)
|
|
503
|
+
return _e(f)
|
klongpy/web/sys_fn_web.py
CHANGED
|
@@ -69,9 +69,14 @@ def eval_sys_fn_create_web_server(klong, x, y, z):
|
|
|
69
69
|
if arity != 1:
|
|
70
70
|
logging.info(f"GET route {route} handler function requires arity 1, got {arity}")
|
|
71
71
|
continue
|
|
72
|
-
fn = fn if isinstance(fn, KGCall) else KGFnWrapper(klong, fn) if isinstance(fn, KGFn) else fn
|
|
73
72
|
|
|
74
|
-
|
|
73
|
+
# Wrap function - KGFnWrapper now handles dynamic resolution automatically
|
|
74
|
+
if isinstance(fn, KGCall):
|
|
75
|
+
logging.info(f"GET route {route} handler cannot be a function call")
|
|
76
|
+
continue
|
|
77
|
+
fn_wrapped = KGFnWrapper(klong, fn) if isinstance(fn, KGFn) else fn
|
|
78
|
+
|
|
79
|
+
async def _get(request: web.Request, fn=fn_wrapped, route=route):
|
|
75
80
|
try:
|
|
76
81
|
assert request.method == "GET"
|
|
77
82
|
return web.Response(text=str(fn(dict(request.rel_url.query))))
|
|
@@ -88,9 +93,14 @@ def eval_sys_fn_create_web_server(klong, x, y, z):
|
|
|
88
93
|
if arity != 1:
|
|
89
94
|
logging.info(f"POST route {route} handler function requires arity 1, got {arity}")
|
|
90
95
|
continue
|
|
91
|
-
fn = fn if isinstance(fn, KGCall) else KGFnWrapper(klong, fn) if isinstance(fn, KGFn) else fn
|
|
92
96
|
|
|
93
|
-
|
|
97
|
+
# Wrap function - KGFnWrapper now handles dynamic resolution automatically
|
|
98
|
+
if isinstance(fn, KGCall):
|
|
99
|
+
logging.info(f"POST route {route} handler cannot be a function call")
|
|
100
|
+
continue
|
|
101
|
+
fn_wrapped = KGFnWrapper(klong, fn) if isinstance(fn, KGFn) else fn
|
|
102
|
+
|
|
103
|
+
async def _post(request: web.Request, fn=fn_wrapped, route=route):
|
|
94
104
|
try:
|
|
95
105
|
assert request.method == "POST"
|
|
96
106
|
parameters = dict(await request.post())
|
klongpy/writer.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
KlongPy output formatting and writing functions.
|
|
3
|
+
|
|
4
|
+
This module contains all the functions for converting Klong values to strings:
|
|
5
|
+
- Symbol, number, character, string formatting
|
|
6
|
+
- List and dictionary formatting
|
|
7
|
+
- Array sorting utilities
|
|
8
|
+
"""
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from .backend import np
|
|
12
|
+
from .types import (
|
|
13
|
+
KGSym, KGChar, KGFn, KGChannel, KGChannelDir, KLONG_UNDEFINED,
|
|
14
|
+
is_integer, is_float, is_list, is_iterable, is_empty,
|
|
15
|
+
get_dtype_kind
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def kg_write_symbol(x, display=False):
|
|
20
|
+
return str(x) if display else f":{x}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def kg_write_integer(x, display=False):
|
|
24
|
+
return str(int(x))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def kg_write_float(x, display=False):
|
|
28
|
+
return str(x)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def kg_write_char(c, display=False):
|
|
32
|
+
return c if display else f"0c{c}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def kg_write_string(s, display=False):
|
|
36
|
+
if display:
|
|
37
|
+
return s
|
|
38
|
+
arr = ['"']
|
|
39
|
+
for c in s:
|
|
40
|
+
if c == '"':
|
|
41
|
+
arr.append('"')
|
|
42
|
+
arr.append(c)
|
|
43
|
+
arr.append('"')
|
|
44
|
+
return ''.join(arr)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def kg_write_dict(d, backend, display=False):
|
|
48
|
+
# determine if the object d has overwritten the default __str__ and call it
|
|
49
|
+
# if so, otherwise use the default dict str
|
|
50
|
+
if d.__class__.__name__ != 'dict':
|
|
51
|
+
return str(d)
|
|
52
|
+
return ''.join([':{', ' '.join([kg_write(list(e), backend, display=display) for e in d.items()]), '}'])
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def kg_write_list(x, backend, display=False):
|
|
56
|
+
return ''.join(['[', ' '.join([kg_write(q, backend, display=display) for q in x]), ']'])
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def kg_write_fn(x, display=False):
|
|
60
|
+
return str(x)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def kg_write_channel(x, display=False):
|
|
64
|
+
if x.channel_dir == KGChannelDir.INPUT:
|
|
65
|
+
return ":inchan.0"
|
|
66
|
+
return f":outchan.{2 if x.raw == sys.stderr else 1}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def kg_write(a, backend, display=False):
|
|
70
|
+
_backend = backend
|
|
71
|
+
# Convert backend arrays (e.g., torch tensors) to display-friendly format
|
|
72
|
+
a = _backend.to_display(a)
|
|
73
|
+
if a is KLONG_UNDEFINED:
|
|
74
|
+
return ":undefined"
|
|
75
|
+
if isinstance(a, KGSym):
|
|
76
|
+
return kg_write_symbol(a, display=display)
|
|
77
|
+
elif is_integer(a, _backend):
|
|
78
|
+
return kg_write_integer(a, display=display)
|
|
79
|
+
elif is_float(a, _backend):
|
|
80
|
+
return kg_write_float(a, display=display)
|
|
81
|
+
elif isinstance(a, KGChar):
|
|
82
|
+
return kg_write_char(a, display=display)
|
|
83
|
+
elif isinstance(a, str):
|
|
84
|
+
return kg_write_string(a, display=display)
|
|
85
|
+
elif isinstance(a, dict):
|
|
86
|
+
return kg_write_dict(a, _backend, display=display)
|
|
87
|
+
elif is_list(a):
|
|
88
|
+
return kg_write_list(a, _backend, display=display)
|
|
89
|
+
elif isinstance(a, KGFn):
|
|
90
|
+
return kg_write_fn(a, display=display)
|
|
91
|
+
elif isinstance(a, KGChannel):
|
|
92
|
+
return kg_write_channel(a, display=display)
|
|
93
|
+
elif hasattr(a, '__str__'):
|
|
94
|
+
return str(a)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def kg_argsort(a, backend, descending=False):
|
|
98
|
+
"""
|
|
99
|
+
Return the indices of the sorted array (may be nested) or a string.
|
|
100
|
+
Duplicate elements are disambiguated by their position in the array.
|
|
101
|
+
|
|
102
|
+
argsort("foobar") => [4 3 0 1 2 5]
|
|
103
|
+
^ ^
|
|
104
|
+
arbitrary ordering resolved by index position
|
|
105
|
+
|
|
106
|
+
argsort("foobar",descending=True) => [5 2 1 0 3 4]
|
|
107
|
+
^ ^
|
|
108
|
+
arbitrary ordering resolved by index position
|
|
109
|
+
"""
|
|
110
|
+
if not is_iterable(a) or len(a) == 0:
|
|
111
|
+
return a
|
|
112
|
+
|
|
113
|
+
# Fast path: for simple 1D numeric arrays, use native argsort
|
|
114
|
+
if hasattr(a, 'ndim') and a.ndim == 1:
|
|
115
|
+
dtype_kind = get_dtype_kind(a, backend)
|
|
116
|
+
if dtype_kind in ('i', 'f', 'u'):
|
|
117
|
+
return backend.argsort(a, descending=descending)
|
|
118
|
+
|
|
119
|
+
# Slow path: nested arrays or strings need element-by-element comparison
|
|
120
|
+
def _e(x):
|
|
121
|
+
return (-np.inf, x) if is_empty(a[x]) else (np.max(a[x]), x) if is_list(a[x]) else (a[x], x)
|
|
122
|
+
return np.asarray(sorted(range(len(a)), key=_e, reverse=descending))
|
klongpy/ws/sys_fn_ws.py
CHANGED
|
@@ -7,7 +7,7 @@ import threading
|
|
|
7
7
|
import numpy as np
|
|
8
8
|
import websockets
|
|
9
9
|
|
|
10
|
-
from klongpy.core import (KGCall, KGLambda, KGSym, KlongException,
|
|
10
|
+
from klongpy.core import (KGCall, KGLambda, KGSym, KlongException, KLONG_UNDEFINED,
|
|
11
11
|
reserved_fn_args, reserved_fn_symbol_map)
|
|
12
12
|
|
|
13
13
|
|
|
@@ -243,7 +243,7 @@ class NetworkClient(KGLambda):
|
|
|
243
243
|
except Exception as e:
|
|
244
244
|
logging.warning(f"error while running on_message handler: {e}")
|
|
245
245
|
await run_command_on_klongloop(self.klongloop, self.klong, ".ws.m", msg, self)
|
|
246
|
-
# if response is not None and response
|
|
246
|
+
# if response is not None and response is not KLONG_UNDEFINED:
|
|
247
247
|
# await self.websocket.send(encode_message(response))
|
|
248
248
|
except websockets.exceptions.ConnectionClosed:
|
|
249
249
|
logging.info("Connection error")
|
|
@@ -548,12 +548,9 @@ def create_system_functions_websockets():
|
|
|
548
548
|
|
|
549
549
|
def create_system_var_websockets():
|
|
550
550
|
# populate the .srv.* handlers with undefined values
|
|
551
|
-
# TODO: use real undefined value instead of np.inf
|
|
552
551
|
registry = {
|
|
553
|
-
".srv.o":
|
|
554
|
-
".srv.c":
|
|
555
|
-
".srv.e":
|
|
552
|
+
".srv.o": KLONG_UNDEFINED,
|
|
553
|
+
".srv.c": KLONG_UNDEFINED,
|
|
554
|
+
".srv.e": KLONG_UNDEFINED,
|
|
556
555
|
}
|
|
557
556
|
return registry
|
|
558
|
-
|
|
559
|
-
|