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.
Files changed (70) hide show
  1. klongpy/__init__.py +17 -1
  2. klongpy/adverbs.py +84 -82
  3. klongpy/autograd.py +299 -0
  4. klongpy/backend.py +38 -103
  5. klongpy/backends/__init__.py +26 -0
  6. klongpy/backends/base.py +469 -0
  7. klongpy/backends/numpy_backend.py +123 -0
  8. klongpy/backends/registry.py +76 -0
  9. klongpy/backends/torch_backend.py +1047 -0
  10. klongpy-0.6.9.data/scripts/kgpy → klongpy/cli.py +110 -90
  11. klongpy/core.py +113 -974
  12. klongpy/db/sys_fn_db.py +7 -6
  13. klongpy/db/sys_fn_kvs.py +2 -4
  14. klongpy/dyads.py +332 -160
  15. klongpy/interpreter.py +60 -15
  16. klongpy/monads.py +121 -75
  17. klongpy/parser.py +328 -0
  18. klongpy/repl.py +23 -5
  19. klongpy/sys_fn.py +170 -21
  20. klongpy/sys_fn_autograd.py +290 -0
  21. klongpy/sys_fn_ipc.py +22 -15
  22. klongpy/sys_fn_timer.py +13 -3
  23. klongpy/types.py +503 -0
  24. klongpy/web/sys_fn_web.py +14 -4
  25. klongpy/writer.py +122 -0
  26. klongpy/ws/sys_fn_ws.py +5 -8
  27. klongpy-0.7.1.dist-info/METADATA +544 -0
  28. klongpy-0.7.1.dist-info/RECORD +52 -0
  29. {klongpy-0.6.9.dist-info → klongpy-0.7.1.dist-info}/WHEEL +1 -1
  30. klongpy-0.7.1.dist-info/entry_points.txt +2 -0
  31. {klongpy-0.6.9.dist-info → klongpy-0.7.1.dist-info}/top_level.txt +0 -1
  32. klongpy-0.6.9.dist-info/METADATA +0 -448
  33. klongpy-0.6.9.dist-info/RECORD +0 -77
  34. tests/__init__.py +0 -6
  35. tests/gen_join_over.py +0 -119
  36. tests/gen_py_suite.py +0 -77
  37. tests/gen_test_fn.py +0 -259
  38. tests/perf_async.py +0 -25
  39. tests/perf_avg.py +0 -18
  40. tests/perf_duckdb.py +0 -32
  41. tests/perf_gen.py +0 -38
  42. tests/perf_ipc_overhead.py +0 -34
  43. tests/perf_join.py +0 -53
  44. tests/perf_load.py +0 -17
  45. tests/perf_prog.py +0 -18
  46. tests/perf_serdes.py +0 -52
  47. tests/perf_sys_fn_db.py +0 -263
  48. tests/perf_vector.py +0 -40
  49. tests/test_accel.py +0 -227
  50. tests/test_df_cache.py +0 -85
  51. tests/test_eval_monad_list.py +0 -34
  52. tests/test_examples.py +0 -64
  53. tests/test_extra_suite.py +0 -382
  54. tests/test_file_cache.py +0 -185
  55. tests/test_interop.py +0 -180
  56. tests/test_kg_asarray.py +0 -94
  57. tests/test_kgtests.py +0 -65
  58. tests/test_known_bugs.py +0 -206
  59. tests/test_prog.py +0 -107
  60. tests/test_reshape_strings.py +0 -33
  61. tests/test_suite.py +0 -1480
  62. tests/test_suite_file.py +0 -153
  63. tests/test_sys_fn.py +0 -420
  64. tests/test_sys_fn_db.py +0 -88
  65. tests/test_sys_fn_ipc.py +0 -587
  66. tests/test_sys_fn_timer.py +0 -133
  67. tests/test_sys_fn_web.py +0 -50
  68. tests/test_util.py +0 -233
  69. tests/utils.py +0 -126
  70. {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
- async def _get(request: web.Request, fn=fn, route=route):
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
- async def _post(request: web.Request, fn=fn):
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 != np.inf:
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": np.inf,
554
- ".srv.c": np.inf,
555
- ".srv.e": np.inf,
552
+ ".srv.o": KLONG_UNDEFINED,
553
+ ".srv.c": KLONG_UNDEFINED,
554
+ ".srv.e": KLONG_UNDEFINED,
556
555
  }
557
556
  return registry
558
-
559
-