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/parser.py ADDED
@@ -0,0 +1,328 @@
1
+ """
2
+ KlongPy parser and lexer functions.
3
+
4
+ This module contains all the parsing functions for the Klong language:
5
+ - Lexeme reading (numbers, strings, symbols, operators)
6
+ - List parsing
7
+ - Conditional expression parsing
8
+ - Comment handling
9
+ """
10
+ import copy
11
+
12
+ from .types import (
13
+ KGSym, KGChar, KGOp, KGCond, KGCall, KGLambda,
14
+ reserved_fn_symbol_map,
15
+ safe_eq, is_symbolic, is_adverb
16
+ )
17
+
18
+
19
+ # Character matching utilities
20
+
21
+ def cmatch(t, i, c):
22
+ return i < len(t) and t[i] == c
23
+
24
+
25
+ def cmatch2(t, i, a, b):
26
+ return cmatch(t, i, a) and cmatch(t, i+1, b)
27
+
28
+
29
+ def cpeek(t, i):
30
+ return t[i] if i < len(t) else None
31
+
32
+
33
+ def cpeek2(t, i):
34
+ return t[i:i+2] if i < (len(t)-1) else None
35
+
36
+
37
+ class UnexpectedChar(Exception):
38
+ def __init__(self, t, i, c):
39
+ super().__init__(f"t: {t[i-10:i+10]} pos: {i} char: {c}")
40
+
41
+
42
+ class UnexpectedEOF(Exception):
43
+ def __init__(self, t, i):
44
+ self.t = t
45
+ self.i = i
46
+ super().__init__(f"t: {t[i-10:]} pos: {i}")
47
+
48
+
49
+ def cexpect(t, i, c):
50
+ if cmatch(t, i, c):
51
+ return i + 1
52
+ raise UnexpectedChar(t, i, c)
53
+
54
+
55
+ def cexpect2(t, i, a, b):
56
+ if cmatch(t, i, a) and cmatch(t, i+1, b):
57
+ return i + 2
58
+ raise UnexpectedChar(t, i, b)
59
+
60
+
61
+ # Comment handling
62
+
63
+ def read_shifted_comment(t, i=0):
64
+ while i < len(t):
65
+ c = t[i]
66
+ if c == '"':
67
+ i += 1
68
+ if not cmatch(t, i, '"'):
69
+ break
70
+ i += 1
71
+ return i
72
+
73
+
74
+ def read_sys_comment(t, i, a):
75
+ """
76
+ .comment(x) [Comment]
77
+
78
+ Read and discard lines until the current line starts with the
79
+ string specified in "x". Also discard the line containing the
80
+ end-of-comment marker and return "x".
81
+
82
+ Example: .comment("end-of-comment")
83
+ this will be ignored
84
+ this, too: *%(*^#)&(#
85
+ end-of-comment
86
+
87
+ NOTE: this is handled in the parsing phase and is not a runtime function
88
+ """
89
+ try:
90
+ j = t[i:].index(a)
91
+ while t[i+j+1:].startswith(a):
92
+ j += 1
93
+ return i + j + len(a)
94
+ except ValueError:
95
+ return RuntimeError("end of comment not found")
96
+
97
+
98
+ # Whitespace handling
99
+
100
+ def skip_space(t, i=0, ignore_newline=False):
101
+ """
102
+ NOTE: a newline character translates to a semicolon in Klong,
103
+ except in functions, dictionaries, conditional expressions,
104
+ and lists. So
105
+ """
106
+ while i < len(t) and (t[i].isspace() and (ignore_newline or t[i] != '\n')):
107
+ i += 1
108
+ return i
109
+
110
+
111
+ def skip(t, i=0, ignore_newline=False):
112
+ i = skip_space(t, i, ignore_newline=ignore_newline)
113
+ if cmatch2(t, i, ':', '"'):
114
+ i = read_shifted_comment(t, i+2)
115
+ i = skip(t, i)
116
+ return i
117
+
118
+
119
+ # Lexeme readers
120
+
121
+ def read_num(t, i=0):
122
+ p = i
123
+ use_float = False
124
+ if t[i] == '-':
125
+ i += 1
126
+ while i < len(t):
127
+ if t[i] == '.':
128
+ use_float = True
129
+ elif t[i] == 'e':
130
+ use_float = True
131
+ if cmatch(t, i+1, '-'):
132
+ i += 2
133
+ elif not t[i].isnumeric():
134
+ break
135
+ i += 1
136
+ return i, float(t[p:i]) if use_float else int(t[p:i])
137
+
138
+
139
+ def read_char(t, i):
140
+ i = cexpect2(t, i, '0', 'c')
141
+ if i >= len(t):
142
+ raise UnexpectedEOF(t, i)
143
+ return i+1, KGChar(t[i])
144
+
145
+
146
+ def read_sym(t, i=0, module=None):
147
+ p = i
148
+ while i < len(t) and is_symbolic(t[i]):
149
+ i += 1
150
+ x = t[p:i]
151
+ return i, reserved_fn_symbol_map.get(x) or KGSym(x if x.startswith('.') or module is None else f"{x}`{module}")
152
+
153
+
154
+ def read_op(t, i=0):
155
+ if cmatch2(t, i, '\\', '~') or cmatch2(t, i, '\\', '*'):
156
+ return i+2, KGOp(t[i:i+2], arity=0)
157
+ return i+1, KGOp(t[i:i+1], arity=0)
158
+
159
+
160
+ def read_string(t, i=0):
161
+ """
162
+ ".*" [String]
163
+
164
+ A string is (almost) any sequence of characters enclosed by
165
+ double quote characters. To include a double quote character in
166
+ a string, it has to be duplicated, so the above regex is not
167
+ entirely correct. A comment is a shifted string (see below).
168
+ Examples: ""
169
+ "hello, world"
170
+ "say ""hello""!"
171
+
172
+ Note: this comforms to the KG read_string impl.
173
+ perf tests show that the final join is fast for short strings
174
+ """
175
+ r = []
176
+ while i < len(t):
177
+ c = t[i]
178
+ if c == '"':
179
+ i += 1
180
+ if not cmatch(t, i, '"'):
181
+ break
182
+ r.append(c)
183
+ i += 1
184
+ return i, "".join(r)
185
+
186
+
187
+ # Dictionary helper
188
+
189
+ def list_to_dict(a):
190
+ return {x[0]:x[1] for x in a}
191
+
192
+
193
+ # Lambda for copy operations (used in dict parsing)
194
+ copy_lambda = KGLambda(lambda x: copy.deepcopy(x))
195
+
196
+
197
+ def read_list(t, delim, i=0, module=None):
198
+ """
199
+ Parse a list from string t starting at position i.
200
+ Returns a Python list (caller converts to array if needed).
201
+
202
+ L := '[' (C|L)* ']'
203
+ """
204
+ arr = []
205
+ i = skip(t, i, ignore_newline=True)
206
+ while not cmatch(t, i, delim) and i < len(t):
207
+ i, q = kg_read(t, i, read_neg=True, ignore_newline=True, module=module)
208
+ if q is None:
209
+ break
210
+ if safe_eq(q, '['):
211
+ i, q = read_list(t, ']', i=i, module=module)
212
+ arr.append(q)
213
+ i = skip(t, i, ignore_newline=True)
214
+ if cmatch(t, i, delim):
215
+ i += 1
216
+ return i, arr
217
+
218
+
219
+ def kg_read(t, i, read_neg=False, ignore_newline=False, module=None):
220
+ """
221
+ Read a Klong lexeme from string t starting at position i.
222
+
223
+ C := I | H | R | S | V | Y
224
+ """
225
+ i = skip(t, i, ignore_newline=ignore_newline)
226
+ if i >= len(t):
227
+ return i, None
228
+ a = t[i]
229
+ if a == '\n':
230
+ a = ';'
231
+ if a in [';', '(', ')', '{', '}', ']']:
232
+ return i+1, a
233
+ elif cmatch2(t, i, '0', 'c'):
234
+ return read_char(t, i)
235
+ elif a.isnumeric() or (read_neg and (a == '-' and (i+1) < len(t) and t[i+1].isnumeric())):
236
+ return read_num(t, i)
237
+ elif a == '"':
238
+ return read_string(t, i+1)
239
+ elif a == ':' and (i+1 < len(t)):
240
+ aa = t[i+1]
241
+ if aa.isalpha() or aa == '.':
242
+ return read_sym(t, i=i+1, module=module)
243
+ elif aa.isnumeric() or aa == '"':
244
+ return kg_read(t, i+1, ignore_newline=ignore_newline, module=module)
245
+ elif aa == '{':
246
+ i, d = read_list(t, '}', i=i+2, module=module)
247
+ d = list_to_dict(d)
248
+ return i, KGCall(copy_lambda, args=d, arity=0)
249
+ elif aa == '[':
250
+ return i+2, ':['
251
+ elif aa == '|':
252
+ return i+2, ':|'
253
+ return i+2, KGOp(f":{aa}", arity=0)
254
+ elif safe_eq(a, '['):
255
+ return read_list(t, ']', i=i+1, module=module)
256
+ elif is_symbolic(a):
257
+ return read_sym(t, i, module=module)
258
+ return read_op(t, i)
259
+
260
+
261
+ def kg_read_array(t, i, backend, **kwargs):
262
+ """
263
+ Read a value and convert lists to arrays using the provided backend.
264
+
265
+ This is a helper that wraps kg_read and handles list-to-array conversion,
266
+ centralizing the pattern used by the interpreter and eval_sys.
267
+
268
+ Parameters
269
+ ----------
270
+ t : str
271
+ The string to read from.
272
+ i : int
273
+ Starting position in the string.
274
+ backend : BackendProvider
275
+ The backend to use for array conversion.
276
+ **kwargs
277
+ Additional arguments passed to kg_read (read_neg, ignore_newline, module).
278
+
279
+ Returns
280
+ -------
281
+ tuple
282
+ (new_position, value) where value is converted to an array if it was a list.
283
+ """
284
+ i, a = kg_read(t, i, **kwargs)
285
+ if isinstance(a, list):
286
+ a = backend.kg_asarray(a)
287
+ return i, a
288
+
289
+
290
+ def read_cond(klong, t, i=0):
291
+ """
292
+ # A conditional expression has two forms: :[e1;e2;e3] means "if
293
+ # e1 is true, evaluate to e2, else evaluate to e3".
294
+ # :[e1;e2:|e3;e4;e5] is short for :[e1;e2:[e3;e4;e5]], i.e. the
295
+ # ":|" acts as an "else-if" operator. There may be any number of
296
+ # ":|" operators in a conditional.
297
+
298
+ c := ':[' ( e ';' e ':|' )* e ';' e ';' e ']'
299
+ """
300
+ r = []
301
+ i, n = klong._expr(t, i, ignore_newline=True)
302
+ r.append(n)
303
+ i = cexpect(t, i, ';')
304
+ i, n = klong._expr(t, i, ignore_newline=True)
305
+ r.append(n)
306
+ i = skip(t, i, ignore_newline=True)
307
+ if cmatch2(t, i, ':', '|'):
308
+ i, n = read_cond(klong, t, i+2)
309
+ r.append(n)
310
+ else:
311
+ i = cexpect(t, i, ';')
312
+ i, n = klong._expr(t, i, ignore_newline=True)
313
+ r.append(n)
314
+ i = skip(t, i, ignore_newline=True)
315
+ i = cexpect(t, i, ']')
316
+ return i, KGCond(r)
317
+
318
+
319
+ # Adverb peeking
320
+
321
+ def peek_adverb(t, i=0):
322
+ x = cpeek2(t, i)
323
+ if is_adverb(x):
324
+ return i+2, x
325
+ x = cpeek(t, i)
326
+ if is_adverb(x):
327
+ return i+1, x
328
+ return i, None
klongpy/repl.py CHANGED
@@ -3,12 +3,30 @@ import threading
3
3
  import time
4
4
  import os
5
5
  import importlib.resources
6
+ from typing import Optional
6
7
 
7
8
  from . import KlongInterpreter
8
9
  from .utils import CallbackEvent
9
10
 
10
11
 
11
- def start_loop(loop: asyncio.AbstractEventLoop, stop_event: asyncio.Event) -> None:
12
+ class LoopStopper:
13
+ def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
14
+ self._loop = loop
15
+ self._future = loop.create_future()
16
+
17
+ def set(self) -> None:
18
+ if self._future.done():
19
+ return
20
+ if self._loop.is_running():
21
+ self._loop.call_soon_threadsafe(self._future.set_result, None)
22
+ else:
23
+ self._future.set_result(None)
24
+
25
+ async def wait(self) -> None:
26
+ await self._future
27
+
28
+
29
+ def start_loop(loop: asyncio.AbstractEventLoop, stop_event: LoopStopper) -> None:
12
30
  asyncio.set_event_loop(loop)
13
31
  loop.run_until_complete(stop_event.wait())
14
32
 
@@ -18,13 +36,13 @@ def setup_async_loop(debug: bool = False, slow_callback_duration: float = 86400.
18
36
  loop.slow_callback_duration = slow_callback_duration
19
37
  if debug:
20
38
  loop.set_debug(True)
21
- stop_event = asyncio.Event()
39
+ stop_event = LoopStopper(loop)
22
40
  thread = threading.Thread(target=start_loop, args=(loop, stop_event), daemon=True)
23
41
  thread.start()
24
42
  return loop, thread, stop_event
25
43
 
26
44
 
27
- def cleanup_async_loop(loop: asyncio.AbstractEventLoop, loop_thread: threading.Thread, stop_event: asyncio.Event, debug: bool = False, name: str | None = None) -> None:
45
+ def cleanup_async_loop(loop: asyncio.AbstractEventLoop, loop_thread: threading.Thread, stop_event: LoopStopper, debug: bool = False, name: Optional[str] = None) -> None:
28
46
  if loop.is_closed():
29
47
  return
30
48
 
@@ -54,13 +72,13 @@ def append_pkg_resource_path_KLONGPATH() -> None:
54
72
  os.environ['KLONGPATH'] = klongpath
55
73
 
56
74
 
57
- def create_repl(debug: bool = False):
75
+ def create_repl(debug: bool = False, backend: Optional[str] = None, device: Optional[str] = None):
58
76
  io_loop, io_thread, io_stop = setup_async_loop(debug=debug)
59
77
  klong_loop, klong_thread, klong_stop = setup_async_loop(debug=debug)
60
78
 
61
79
  append_pkg_resource_path_KLONGPATH()
62
80
 
63
- klong = KlongInterpreter()
81
+ klong = KlongInterpreter(backend=backend, device=device)
64
82
  shutdown_event = CallbackEvent()
65
83
  klong['.system'] = {'ioloop': io_loop, 'klongloop': klong_loop, 'closeEvent': shutdown_event}
66
84
 
klongpy/sys_fn.py CHANGED
@@ -12,10 +12,24 @@ from inspect import Parameter
12
12
  import numpy
13
13
 
14
14
  from .core import (KGChannel, KGChannelDir, KGLambda, KGSym, KlongException,
15
- is_dict, is_empty, is_list, kg_asarray, kg_read, kg_write, np,
15
+ bknp, is_dict, is_empty, is_list, kg_read_array, kg_write,
16
16
  reserved_fn_args, reserved_fn_symbol_map, safe_eq, safe_inspect)
17
17
 
18
18
 
19
+ def _to_display_value(x, backend):
20
+ """Convert backend tensors to numpy for cleaner display."""
21
+ # Convert backend arrays (tensors) to numpy
22
+ if backend.is_backend_array(x):
23
+ return backend.to_numpy(x)
24
+ # Handle numpy arrays with tensors inside (object arrays)
25
+ if isinstance(x, numpy.ndarray) and x.dtype == object:
26
+ return numpy.array([_to_display_value(item, backend) for item in x], dtype=object)
27
+ # Handle lists with tensors
28
+ if isinstance(x, list):
29
+ return [_to_display_value(item, backend) for item in x]
30
+ return x
31
+
32
+
19
33
  def eval_sys_append_channel(x):
20
34
  """
21
35
 
@@ -47,10 +61,25 @@ def eval_sys_display(klong, x):
47
61
 
48
62
  .d(x) [Display]
49
63
 
50
- See [Write].
64
+ Display the object "x". Backend arrays are converted to numpy for cleaner output.
65
+ Use .bkd() for raw backend-specific display.
51
66
 
52
67
  """
53
- r = kg_write(x, display=True)
68
+ x = _to_display_value(x, klong._backend)
69
+ r = kg_write(x, klong._backend, display=True)
70
+ klong['.sys.cout'].raw.write(r)
71
+ return r
72
+
73
+
74
+ def eval_sys_backend_display(klong, x):
75
+ """
76
+
77
+ .bkd(x) [Backend-Display]
78
+
79
+ Display the object "x" in raw backend format (tensors shown as-is).
80
+
81
+ """
82
+ r = kg_write(x, klong._backend, display=True)
54
83
  klong['.sys.cout'].raw.write(r)
55
84
  return r
56
85
 
@@ -272,10 +301,26 @@ def eval_sys_print(klong, x):
272
301
  .p(x) [Print]
273
302
 
274
303
  Pretty-print the object "x" (like Display) and then print a
275
- newline sequence. .p("") will just print a newline.
304
+ newline sequence. Backend arrays are converted to numpy for cleaner output.
305
+ Use .bkp() for raw backend-specific print.
276
306
 
277
307
  """
278
- o = kg_write(x, display=True)
308
+ x = _to_display_value(x, klong._backend)
309
+ o = kg_write(x, klong._backend, display=True)
310
+ klong['.sys.cout'].raw.write(o+"\n")
311
+ return o
312
+
313
+
314
+ def eval_sys_backend_print(klong, x):
315
+ """
316
+
317
+ .bkp(x) [Backend-Print]
318
+
319
+ Pretty-print the object "x" in raw backend format (tensors shown as-is)
320
+ and then print a newline sequence.
321
+
322
+ """
323
+ o = kg_write(x, klong._backend, display=True)
279
324
  klong['.sys.cout'].raw.write(o+"\n")
280
325
  return o
281
326
 
@@ -341,20 +386,30 @@ def _handle_import(item):
341
386
  if n_args <= len(reserved_fn_args):
342
387
  item = KGLambda(item, args=reserved_fn_args[:n_args])
343
388
  else:
344
- args = safe_inspect(item, follow_wrapped=True)
345
- if 'args' in args:
389
+ sig_args = safe_inspect(item, follow_wrapped=True)
390
+ if 'args' in sig_args:
346
391
  item = KGLambda(item, args=None, wildcard=True)
347
392
  n_args = 3
348
393
  else:
349
- args = [k for k,v in args.items() if (v.kind == Parameter.POSITIONAL_OR_KEYWORD and v.default == Parameter.empty) or (v.kind == Parameter.POSITIONAL_ONLY)]
350
- n_args = len(args)
351
- # if there are kwargs, then .pyc() must be used to call this function to override them
352
- if 'klong' in args:
353
- n_args -= 1
354
- assert n_args <= len(reserved_fn_args)
355
- item = KGLambda(item, args=reserved_fn_args[:n_args], provide_klong=True)
356
- elif n_args <= len(reserved_fn_args):
357
- item = KGLambda(item, args=reserved_fn_args[:n_args])
394
+ # Get required args (no default)
395
+ required_args = [k for k,v in sig_args.items() if (v.kind == Parameter.POSITIONAL_OR_KEYWORD and v.default == Parameter.empty) or (v.kind == Parameter.POSITIONAL_ONLY)]
396
+ # Get optional args (have default)
397
+ optional_args = [k for k,v in sig_args.items() if v.kind == Parameter.POSITIONAL_OR_KEYWORD and v.default != Parameter.empty]
398
+ # Use required args count, but if there are optional args and no required args,
399
+ # use wildcard mode so the function can accept 0-3 args
400
+ if not required_args and optional_args:
401
+ item = KGLambda(item, args=None, wildcard=True)
402
+ n_args = 3
403
+ else:
404
+ args = required_args
405
+ n_args = len(args)
406
+ # if there are kwargs, then .pyc() must be used to call this function to override them
407
+ if 'klong' in args:
408
+ n_args -= 1
409
+ assert n_args <= len(reserved_fn_args)
410
+ item = KGLambda(item, args=reserved_fn_args[:n_args], provide_klong=True)
411
+ elif n_args <= len(reserved_fn_args):
412
+ item = KGLambda(item, args=reserved_fn_args[:n_args])
358
413
  except Exception:
359
414
  if hasattr(item, "__class__") and hasattr(item.__class__, '__module__') and item.__class__.__module__ == "builtins":
360
415
  # LOOK AWAY. You didn't see this.
@@ -415,6 +470,17 @@ def _import_module(klong, x, from_set=None):
415
470
  except Exception as e:
416
471
  # TODO: this should be logged
417
472
  print(f"failed to import function: {name}", e)
473
+
474
+ # For from_set imports, also check for lazy-loaded attributes not in __dict__
475
+ # (e.g., numpy.random in numpy 2.x)
476
+ if from_set is not None:
477
+ for name in from_set:
478
+ if name not in export_items and hasattr(module, name):
479
+ try:
480
+ item = getattr(module, name)
481
+ klong[name] = _handle_import(item)
482
+ except Exception as e:
483
+ print(f"failed to import function: {name}", e)
418
484
  finally:
419
485
  klong._context.push(ctx)
420
486
 
@@ -578,6 +644,49 @@ def eval_sys_python_from(klong, x, y):
578
644
  return _import_module(klong, x, from_set=set(y))
579
645
 
580
646
 
647
+ def eval_sys_backend_fn(klong, x):
648
+ """
649
+
650
+ .bkf(x) [Backend-Function]
651
+
652
+ Import functions from the current backend's array module.
653
+ This is similar to .pyf() but uses backend-aware functions that
654
+ work with both numpy and torch backends.
655
+
656
+ When using the torch backend, these functions preserve gradient
657
+ tracking for autograd.
658
+
659
+ Example:
660
+
661
+ .bkf("exp")
662
+ exp(1.0) --> 2.718...
663
+
664
+ .bkf(["exp";"sin";"cos"])
665
+ sin(1.0) --> 0.841...
666
+
667
+ Common functions available: exp, sin, cos, tan, tanh, sqrt, abs,
668
+ log, log10, floor, ceil, round
669
+
670
+ """
671
+ if isinstance(x, str):
672
+ x = [x]
673
+ if not (is_list(x) and all(map(lambda p: isinstance(p, str), x))):
674
+ raise RuntimeError("function name(s) must be a string or list of strings")
675
+
676
+ backend = klong._backend
677
+ ctx = klong._context.pop()
678
+ try:
679
+ for fn_name in x:
680
+ if hasattr(backend.np, fn_name):
681
+ fn = getattr(backend.np, fn_name)
682
+ klong[fn_name] = _handle_import(fn)
683
+ else:
684
+ raise RuntimeError(f"Backend does not have function: {fn_name}")
685
+ finally:
686
+ klong._context.push(ctx)
687
+ return None
688
+
689
+
581
690
  def eval_sys_random_number():
582
691
  """
583
692
 
@@ -586,7 +695,7 @@ def eval_sys_random_number():
586
695
  Return a random number x, such that 0 <= x < 1.
587
696
 
588
697
  """
589
- return np.random.random()
698
+ return bknp.random.random()
590
699
 
591
700
 
592
701
  def eval_sys_read(klong):
@@ -607,7 +716,7 @@ def eval_sys_read(klong):
607
716
  f.at_eof = True
608
717
  return None
609
718
  else:
610
- i,a = kg_read(r,i=0,module=klong.current_module())
719
+ i,a = kg_read_array(r, 0, klong._backend, module=klong.current_module())
611
720
  f.raw.seek(k+i,0)
612
721
  return a
613
722
 
@@ -642,7 +751,7 @@ def eval_sys_read_lines(klong):
642
751
  f = klong['.sys.cin']
643
752
  r = f.raw.readlines()
644
753
  f.at_eof = True
645
- return kg_asarray(r)
754
+ return klong._backend.kg_asarray(r)
646
755
 
647
756
 
648
757
  def eval_sys_read_string(klong, x):
@@ -656,7 +765,8 @@ def eval_sys_read_string(klong, x):
656
765
  forms.
657
766
 
658
767
  """
659
- return kg_read(x, i=0, module=klong.current_module(), read_neg=True)[1]
768
+ _, a = kg_read_array(x, 0, klong._backend, module=klong.current_module(), read_neg=True)
769
+ return a
660
770
 
661
771
 
662
772
  def eval_sys_system(x):
@@ -722,7 +832,7 @@ def eval_sys_write(klong, x):
722
832
  sequence. Use .p (Print) to do so.
723
833
 
724
834
  """
725
- r = kg_write(x)
835
+ r = kg_write(x, klong._backend)
726
836
  klong['.sys.cout'].raw.write(r)
727
837
  return x
728
838
 
@@ -742,6 +852,45 @@ def eval_sys_exit(x):
742
852
  sys.exit(1)
743
853
 
744
854
 
855
+ def eval_sys_strict(klong):
856
+ """
857
+
858
+ .strict() [Strict]
859
+
860
+ Enable strict mode for variable assignment (level 1).
861
+ In strict mode, functions cannot create new global variables
862
+ unless they are explicitly declared with the global: prefix.
863
+
864
+ Example:
865
+ .strict()
866
+ counter::0
867
+ increment::{counter::counter+1} :" ERROR"
868
+ increment::{[global:counter];counter::counter+1} :" OK"
869
+
870
+ """
871
+ klong._context._strict_mode = 1
872
+ return 1
873
+
874
+
875
+ def eval_sys_unsafe(klong):
876
+ """
877
+
878
+ .unsafe() [Unsafe]
879
+
880
+ Disable strict mode for variable assignment (level 0).
881
+ In unsafe mode, functions can create new global variables
882
+ without declaration (legacy behavior).
883
+
884
+ Example:
885
+ .unsafe()
886
+ f::{newvar::42} :" OK - creates global newvar"
887
+ f()
888
+
889
+ """
890
+ klong._context._strict_mode = 0
891
+ return 0
892
+
893
+
745
894
  def create_system_functions():
746
895
  def _get_name(s):
747
896
  i = s.index('.')