klongpy 0.6.8__py3-none-any.whl → 0.7.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.
Files changed (61) hide show
  1. klongpy/__init__.py +19 -1
  2. klongpy/adverbs.py +5 -5
  3. klongpy/autograd.py +308 -0
  4. klongpy/backend.py +167 -99
  5. klongpy/backends/__init__.py +94 -0
  6. klongpy/backends/base.py +320 -0
  7. klongpy/backends/numpy_backend.py +122 -0
  8. klongpy/backends/torch_backend.py +995 -0
  9. klongpy-0.6.8.data/scripts/kgpy → klongpy/cli.py +65 -88
  10. klongpy/core.py +228 -106
  11. klongpy/db/sys_fn_db.py +4 -3
  12. klongpy/dyads.py +173 -32
  13. klongpy/interpreter.py +31 -3
  14. klongpy/lib/help.kg +2 -2
  15. klongpy/monads.py +49 -12
  16. klongpy/repl.py +91 -0
  17. klongpy/sys_fn.py +129 -18
  18. klongpy/sys_fn_autograd.py +290 -0
  19. klongpy/sys_fn_ipc.py +18 -7
  20. klongpy/sys_fn_timer.py +13 -3
  21. klongpy/web/sys_fn_web.py +28 -6
  22. klongpy-0.7.0.dist-info/METADATA +493 -0
  23. klongpy-0.7.0.dist-info/RECORD +48 -0
  24. {klongpy-0.6.8.dist-info → klongpy-0.7.0.dist-info}/WHEEL +1 -1
  25. klongpy-0.7.0.dist-info/entry_points.txt +2 -0
  26. {klongpy-0.6.8.dist-info → klongpy-0.7.0.dist-info}/top_level.txt +0 -1
  27. klongpy-0.6.8.dist-info/METADATA +0 -412
  28. klongpy-0.6.8.dist-info/RECORD +0 -72
  29. tests/__init__.py +0 -6
  30. tests/gen_join_over.py +0 -119
  31. tests/gen_py_suite.py +0 -77
  32. tests/gen_test_fn.py +0 -259
  33. tests/perf_async.py +0 -25
  34. tests/perf_avg.py +0 -18
  35. tests/perf_duckdb.py +0 -32
  36. tests/perf_gen.py +0 -38
  37. tests/perf_ipc_overhead.py +0 -34
  38. tests/perf_join.py +0 -53
  39. tests/perf_load.py +0 -17
  40. tests/perf_prog.py +0 -18
  41. tests/perf_serdes.py +0 -52
  42. tests/perf_sys_fn_db.py +0 -263
  43. tests/perf_vector.py +0 -40
  44. tests/test_accel.py +0 -227
  45. tests/test_df_cache.py +0 -85
  46. tests/test_examples.py +0 -64
  47. tests/test_extra_suite.py +0 -382
  48. tests/test_file_cache.py +0 -185
  49. tests/test_interop.py +0 -181
  50. tests/test_kgtests.py +0 -65
  51. tests/test_known_bugs.py +0 -206
  52. tests/test_prog.py +0 -107
  53. tests/test_suite.py +0 -1479
  54. tests/test_suite_file.py +0 -153
  55. tests/test_sys_fn.py +0 -420
  56. tests/test_sys_fn_db.py +0 -88
  57. tests/test_sys_fn_ipc.py +0 -587
  58. tests/test_sys_fn_timer.py +0 -133
  59. tests/test_util.py +0 -233
  60. tests/utils.py +0 -126
  61. {klongpy-0.6.8.dist-info → klongpy-0.7.0.dist-info/licenses}/LICENSE +0 -0
klongpy/sys_fn.py CHANGED
@@ -12,8 +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
+ is_dict, is_empty, is_list, kg_read, kg_write, np,
16
16
  reserved_fn_args, reserved_fn_symbol_map, safe_eq, safe_inspect)
17
+ from .backend import to_numpy, get_default_backend, kg_asarray
18
+
19
+
20
+ def _to_display_value(x):
21
+ """Convert backend tensors to numpy for cleaner display."""
22
+ backend = get_default_backend()
23
+ # Convert backend arrays (tensors) to numpy
24
+ if backend.is_backend_array(x):
25
+ return to_numpy(x)
26
+ # Handle numpy arrays with tensors inside (object arrays)
27
+ if isinstance(x, numpy.ndarray) and x.dtype == object:
28
+ return numpy.array([_to_display_value(item) for item in x], dtype=object)
29
+ # Handle lists with tensors
30
+ if isinstance(x, list):
31
+ return [_to_display_value(item) for item in x]
32
+ return x
17
33
 
18
34
 
19
35
  def eval_sys_append_channel(x):
@@ -47,10 +63,25 @@ def eval_sys_display(klong, x):
47
63
 
48
64
  .d(x) [Display]
49
65
 
50
- See [Write].
66
+ Display the object "x". Tensors are converted to numpy for cleaner output.
67
+ Use .bkd() for raw backend-specific display.
68
+
69
+ """
70
+ x = _to_display_value(x)
71
+ r = kg_write(x, klong._backend, display=True)
72
+ klong['.sys.cout'].raw.write(r)
73
+ return r
74
+
75
+
76
+ def eval_sys_backend_display(klong, x):
77
+ """
78
+
79
+ .bkd(x) [Backend-Display]
80
+
81
+ Display the object "x" in raw backend format (tensors shown as-is).
51
82
 
52
83
  """
53
- r = kg_write(x, display=True)
84
+ r = kg_write(x, klong._backend, display=True)
54
85
  klong['.sys.cout'].raw.write(r)
55
86
  return r
56
87
 
@@ -272,10 +303,26 @@ def eval_sys_print(klong, x):
272
303
  .p(x) [Print]
273
304
 
274
305
  Pretty-print the object "x" (like Display) and then print a
275
- newline sequence. .p("") will just print a newline.
306
+ newline sequence. Tensors are converted to numpy for cleaner output.
307
+ Use .bkp() for raw backend-specific print.
308
+
309
+ """
310
+ x = _to_display_value(x)
311
+ o = kg_write(x, klong._backend, display=True)
312
+ klong['.sys.cout'].raw.write(o+"\n")
313
+ return o
314
+
315
+
316
+ def eval_sys_backend_print(klong, x):
317
+ """
318
+
319
+ .bkp(x) [Backend-Print]
320
+
321
+ Pretty-print the object "x" in raw backend format (tensors shown as-is)
322
+ and then print a newline sequence.
276
323
 
277
324
  """
278
- o = kg_write(x, display=True)
325
+ o = kg_write(x, klong._backend, display=True)
279
326
  klong['.sys.cout'].raw.write(o+"\n")
280
327
  return o
281
328
 
@@ -341,20 +388,30 @@ def _handle_import(item):
341
388
  if n_args <= len(reserved_fn_args):
342
389
  item = KGLambda(item, args=reserved_fn_args[:n_args])
343
390
  else:
344
- args = safe_inspect(item, follow_wrapped=True)
345
- if 'args' in args:
391
+ sig_args = safe_inspect(item, follow_wrapped=True)
392
+ if 'args' in sig_args:
346
393
  item = KGLambda(item, args=None, wildcard=True)
347
394
  n_args = 3
348
395
  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])
396
+ # Get required args (no default)
397
+ 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)]
398
+ # Get optional args (have default)
399
+ optional_args = [k for k,v in sig_args.items() if v.kind == Parameter.POSITIONAL_OR_KEYWORD and v.default != Parameter.empty]
400
+ # Use required args count, but if there are optional args and no required args,
401
+ # use wildcard mode so the function can accept 0-3 args
402
+ if not required_args and optional_args:
403
+ item = KGLambda(item, args=None, wildcard=True)
404
+ n_args = 3
405
+ else:
406
+ args = required_args
407
+ n_args = len(args)
408
+ # if there are kwargs, then .pyc() must be used to call this function to override them
409
+ if 'klong' in args:
410
+ n_args -= 1
411
+ assert n_args <= len(reserved_fn_args)
412
+ item = KGLambda(item, args=reserved_fn_args[:n_args], provide_klong=True)
413
+ elif n_args <= len(reserved_fn_args):
414
+ item = KGLambda(item, args=reserved_fn_args[:n_args])
358
415
  except Exception:
359
416
  if hasattr(item, "__class__") and hasattr(item.__class__, '__module__') and item.__class__.__module__ == "builtins":
360
417
  # LOOK AWAY. You didn't see this.
@@ -405,7 +462,7 @@ def _import_module(klong, x, from_set=None):
405
462
  module = import_module_from_sys(x)
406
463
 
407
464
  export_items = module.__dict__.get("klongpy_exports") or module.__dict__
408
- ffn = lambda p: p[0] in from_set if from_set is not None else lambda p: not p[0].startswith("__")
465
+ ffn = (lambda p: p[0] in from_set) if from_set is not None else (lambda p: not p[0].startswith("__"))
409
466
 
410
467
  ctx = klong._context.pop()
411
468
  try:
@@ -415,6 +472,17 @@ def _import_module(klong, x, from_set=None):
415
472
  except Exception as e:
416
473
  # TODO: this should be logged
417
474
  print(f"failed to import function: {name}", e)
475
+
476
+ # For from_set imports, also check for lazy-loaded attributes not in __dict__
477
+ # (e.g., numpy.random in numpy 2.x)
478
+ if from_set is not None:
479
+ for name in from_set:
480
+ if name not in export_items and hasattr(module, name):
481
+ try:
482
+ item = getattr(module, name)
483
+ klong[name] = _handle_import(item)
484
+ except Exception as e:
485
+ print(f"failed to import function: {name}", e)
418
486
  finally:
419
487
  klong._context.push(ctx)
420
488
 
@@ -578,6 +646,49 @@ def eval_sys_python_from(klong, x, y):
578
646
  return _import_module(klong, x, from_set=set(y))
579
647
 
580
648
 
649
+ def eval_sys_backend_fn(klong, x):
650
+ """
651
+
652
+ .bkf(x) [Backend-Function]
653
+
654
+ Import functions from the current backend's array module.
655
+ This is similar to .pyf() but uses backend-aware functions that
656
+ work with both numpy and torch backends.
657
+
658
+ When using the torch backend, these functions preserve gradient
659
+ tracking for autograd.
660
+
661
+ Example:
662
+
663
+ .bkf("exp")
664
+ exp(1.0) --> 2.718...
665
+
666
+ .bkf(["exp";"sin";"cos"])
667
+ sin(1.0) --> 0.841...
668
+
669
+ Common functions available: exp, sin, cos, tan, tanh, sqrt, abs,
670
+ log, log10, floor, ceil, round
671
+
672
+ """
673
+ if isinstance(x, str):
674
+ x = [x]
675
+ if not (is_list(x) and all(map(lambda p: isinstance(p, str), x))):
676
+ raise RuntimeError("function name(s) must be a string or list of strings")
677
+
678
+ backend = klong._backend
679
+ ctx = klong._context.pop()
680
+ try:
681
+ for fn_name in x:
682
+ if hasattr(backend.np, fn_name):
683
+ fn = getattr(backend.np, fn_name)
684
+ klong[fn_name] = _handle_import(fn)
685
+ else:
686
+ raise RuntimeError(f"Backend does not have function: {fn_name}")
687
+ finally:
688
+ klong._context.push(ctx)
689
+ return None
690
+
691
+
581
692
  def eval_sys_random_number():
582
693
  """
583
694
 
@@ -722,7 +833,7 @@ def eval_sys_write(klong, x):
722
833
  sequence. Use .p (Print) to do so.
723
834
 
724
835
  """
725
- r = kg_write(x)
836
+ r = kg_write(x, klong._backend)
726
837
  klong['.sys.cout'].raw.write(r)
727
838
  return x
728
839
 
@@ -0,0 +1,290 @@
1
+ """
2
+ Autograd system functions for KlongPy.
3
+
4
+ Provides .jacobian() for Jacobian matrix computation.
5
+ Provides .compile() for function compilation and graph export (torch only).
6
+
7
+ For optimizers (SGD, Adam, etc.), see examples/autograd/optimizers.py
8
+ which can be copied to your project and customized.
9
+ """
10
+ import sys
11
+
12
+ from .autograd import jacobian_of_fn, _invoke_fn
13
+
14
+
15
+ def eval_sys_jacobian(klong, x, y):
16
+ """
17
+
18
+ .jacobian(x;y) [Jacobian]
19
+
20
+ Compute Jacobian matrix of function x at point y.
21
+
22
+ For f: R^n -> R^m, returns m x n matrix where J[i,j] = df_i/dx_j.
23
+
24
+ Examples:
25
+ f::{[x@0^2 x@1^2]}
26
+ .jacobian(f;[1 2]) --> [[2 0] [0 4]]
27
+
28
+ """
29
+ return jacobian_of_fn(klong, x, y)
30
+
31
+
32
+ def eval_sys_compile(klong, x, y):
33
+ """
34
+
35
+ .compile(x;y) [Compile]
36
+
37
+ Compile a function for optimized execution using torch.compile.
38
+ Requires PyTorch backend (USE_TORCH=1).
39
+
40
+ Arguments:
41
+ x - Function to compile
42
+ y - Example input for tracing the computation graph
43
+
44
+ Returns:
45
+ Compiled function (faster execution)
46
+
47
+ Examples:
48
+ f::{x^2}
49
+ cf::.compile(f;3.0) :" Returns compiled function
50
+ cf(5.0) :" 25.0 (optimized)
51
+
52
+ Notes:
53
+ - Only supported with PyTorch backend
54
+ - Raises error on NumPy backend
55
+ - See .export() for saving graphs to files
56
+
57
+ """
58
+ fn, example_input = x, y
59
+
60
+ backend = klong._backend
61
+ if not backend.supports_autograd():
62
+ raise RuntimeError(
63
+ ".compile() requires PyTorch backend. "
64
+ "Run with USE_TORCH=1 environment variable."
65
+ )
66
+
67
+ # Wrap the Klong function for torch
68
+ def wrapped_fn(v):
69
+ return _invoke_fn(klong, fn, [v])
70
+
71
+ return backend.compile_function(wrapped_fn, example_input, None)
72
+
73
+
74
+ def eval_sys_export(klong, x, y, z):
75
+ """
76
+
77
+ .export(x;y;z) [Export]
78
+
79
+ Export a function's computation graph to a file for inspection.
80
+ Requires PyTorch backend (USE_TORCH=1).
81
+
82
+ Arguments:
83
+ x - Function to export
84
+ y - Example input for tracing the computation graph
85
+ z - Path to save the graph (.pt2 file)
86
+
87
+ Returns:
88
+ Dictionary with:
89
+ "compiled_fn" - The compiled function
90
+ "export_path" - Path where graph was saved
91
+ "graph" - String representation of computation graph
92
+
93
+ Examples:
94
+ f::{x^2}
95
+ info::.export(f;3.0;"model.pt2")
96
+ .p(info@"graph") :" Print computation graph
97
+
98
+ Notes:
99
+ - Only supported with PyTorch backend
100
+ - The exported graph can be loaded with torch.export.load()
101
+ - Use .compile() for just compiling without export
102
+
103
+ """
104
+ fn, example_input, output_path = x, y, z
105
+
106
+ backend = klong._backend
107
+ if not backend.supports_autograd():
108
+ raise RuntimeError(
109
+ ".export() requires PyTorch backend. "
110
+ "Run with USE_TORCH=1 environment variable."
111
+ )
112
+
113
+ # Wrap the Klong function for torch
114
+ def wrapped_fn(v):
115
+ return _invoke_fn(klong, fn, [v])
116
+
117
+ return backend.compile_function(wrapped_fn, example_input, output_path)
118
+
119
+
120
+ def eval_sys_compilex(klong, x, y, z):
121
+ """
122
+
123
+ .compilex(x;y;z) [Compile-Extended]
124
+
125
+ Compile a function with extended options for mode and backend.
126
+ Requires PyTorch backend (USE_TORCH=1).
127
+
128
+ Arguments:
129
+ x - Function to compile
130
+ y - Example input for tracing the computation graph
131
+ z - Options dictionary with compile settings
132
+
133
+ Options (z):
134
+ "mode" - Compilation mode:
135
+ "default" - Balanced (default)
136
+ "reduce-overhead" - Faster compile, less optimization
137
+ "max-autotune" - Slower compile, best runtime
138
+ "backend" - Compilation backend:
139
+ "inductor" - Default with C++/Triton codegen
140
+ "eager" - No compilation (debugging)
141
+ "cudagraphs" - CUDA graphs (GPU only)
142
+ "fullgraph" - 1 to require full graph compilation
143
+ "dynamic" - 1 for dynamic shapes, 0 for static
144
+
145
+ Mode Comparison:
146
+ | Mode | Compile | Runtime | Use Case |
147
+ |-----------------|---------|---------|-------------------|
148
+ | default | Medium | Good | General use |
149
+ | reduce-overhead | Fast | OK | Development |
150
+ | max-autotune | Slow | Best | Production |
151
+
152
+ Returns:
153
+ Compiled function
154
+
155
+ Examples:
156
+ f::{x^2}
157
+
158
+ :" Fast compilation for development
159
+ cf::.compilex(f;3.0;:{["mode" "reduce-overhead"]})
160
+
161
+ :" Maximum optimization for production
162
+ cf::.compilex(f;3.0;:{["mode" "max-autotune"]})
163
+
164
+ :" Debug mode (no compilation)
165
+ cf::.compilex(f;3.0;:{["backend" "eager"]})
166
+
167
+ Notes:
168
+ - Only supported with PyTorch backend
169
+ - Requires C++ compiler for inductor backend
170
+ - Use .cmodes() to see all available options
171
+
172
+ """
173
+ fn, example_input, options = x, y, z
174
+
175
+ backend = klong._backend
176
+ if not backend.supports_autograd():
177
+ raise RuntimeError(
178
+ ".compilex() requires PyTorch backend. "
179
+ "Run with USE_TORCH=1 environment variable."
180
+ )
181
+
182
+ # Extract options from dictionary
183
+ mode = options.get("mode", "default") if isinstance(options, dict) else "default"
184
+ compile_backend = options.get("backend", "inductor") if isinstance(options, dict) else "inductor"
185
+ fullgraph = bool(options.get("fullgraph", 0)) if isinstance(options, dict) else False
186
+ dynamic = None
187
+ if isinstance(options, dict) and "dynamic" in options:
188
+ dynamic = bool(options["dynamic"])
189
+
190
+ # Wrap the Klong function for torch
191
+ def wrapped_fn(v):
192
+ return _invoke_fn(klong, fn, [v])
193
+
194
+ return backend.compile_function(
195
+ wrapped_fn, example_input, None,
196
+ mode=mode, backend=compile_backend, fullgraph=fullgraph, dynamic=dynamic
197
+ )
198
+
199
+
200
+ def eval_sys_cmodes(klong):
201
+ """
202
+
203
+ .cmodes() [Compile-Modes]
204
+
205
+ Get information about available torch.compile modes and backends.
206
+ Requires PyTorch backend (USE_TORCH=1).
207
+
208
+ Returns:
209
+ Dictionary with:
210
+ "modes" - Available compilation modes
211
+ "backends" - Available compilation backends
212
+ "recommendations" - Suggested settings for common use cases
213
+
214
+ Examples:
215
+ info::.cmodes()
216
+ .p(info@"modes") :" Print available modes
217
+ .p(info@"recommendations") :" Print recommended settings
218
+
219
+ Mode Comparison:
220
+ | Mode | Compile Time | Runtime Speed | Best For |
221
+ |-----------------|--------------|---------------|--------------|
222
+ | default | Medium | Good | General use |
223
+ | reduce-overhead | Fast | Moderate | Development |
224
+ | max-autotune | Slow | Best | Production |
225
+
226
+ Backend Comparison:
227
+ | Backend | Description |
228
+ |------------|------------------------------------------|
229
+ | inductor | Default - C++/Triton code generation |
230
+ | eager | No compilation - for debugging |
231
+ | cudagraphs | CUDA graphs - reduces GPU launch overhead|
232
+
233
+ """
234
+ backend = klong._backend
235
+ if not backend.supports_autograd():
236
+ raise RuntimeError(
237
+ ".cmodes() requires PyTorch backend. "
238
+ "Run with USE_TORCH=1 environment variable."
239
+ )
240
+
241
+ return backend.get_compile_modes()
242
+
243
+
244
+ def eval_sys_gradcheck(klong, x, y):
245
+ """
246
+
247
+ .gradcheck(x;y) [Gradcheck]
248
+
249
+ Verify that autograd gradients match numeric gradients.
250
+ Uses torch.autograd.gradcheck for rigorous verification.
251
+ Requires PyTorch backend (USE_TORCH=1).
252
+
253
+ Arguments:
254
+ x - Function to check (should return a scalar)
255
+ y - Input value or list of inputs to check
256
+
257
+ Returns:
258
+ 1 if gradients are correct
259
+ Raises error if gradients don't match
260
+
261
+ Examples:
262
+ f::{x^2}
263
+ .gradcheck(f;3.0) :" Returns 1
264
+
265
+ g::{(x@0^2)+(x@1^2)}
266
+ .gradcheck(g;[1.0 2.0]) :" Returns 1
267
+
268
+ Notes:
269
+ - Only supported with PyTorch backend
270
+ - Uses double precision (float64) when available (CPU/CUDA)
271
+ - Falls back to float32 with relaxed tolerances on MPS
272
+ - Useful for verifying custom gradient implementations
273
+
274
+ """
275
+ return klong._backend.klong_gradcheck(klong, x, y)
276
+
277
+
278
+ def create_system_functions_autograd():
279
+ """Create registry of autograd system functions."""
280
+ def _get_name(s):
281
+ i = s.index('.')
282
+ return s[i:i+s[i:].index('(')]
283
+
284
+ registry = {}
285
+ m = sys.modules[__name__]
286
+ for x in filter(lambda n: n.startswith("eval_sys_"), dir(m)):
287
+ fn = getattr(m, x)
288
+ registry[_get_name(fn.__doc__)] = fn
289
+
290
+ return registry
klongpy/sys_fn_ipc.py CHANGED
@@ -919,7 +919,6 @@ def eval_sys_fn_create_ipc_server(klong, x):
919
919
  if "x" is 0, then the server is closed and existing client connections are dropped.
920
920
 
921
921
  """
922
- global _ipc_tcp_server
923
922
  x = str(x)
924
923
  parts = x.split(":")
925
924
  bind = parts[0] if len(parts) > 1 else None
@@ -941,20 +940,31 @@ def eval_sys_fn_create_ipc_server(klong, x):
941
940
 
942
941
 
943
942
  class KGAsyncCall(KGLambda):
944
- def __init__(self, klongloop, fn, cb):
943
+ def __init__(self, klongloop, fn, cb, klong):
945
944
  self.klongloop = klongloop
946
- self.cb = cb
947
- self.fn = fn
948
- arity = fn.get_arity() if issubclass(type(self.fn), KGLambda) else fn.arity
945
+ self.klong = klong
946
+
947
+ # Wrap callbacks - KGFnWrapper now handles dynamic resolution automatically
948
+ self.fn = KGFnWrapper(klong, fn) if isinstance(fn, KGFn) else fn
949
+ self.cb = KGFnWrapper(klong, cb) if isinstance(cb, KGFn) else cb
950
+
951
+ arity = fn.get_arity() if issubclass(type(fn), KGLambda) else fn.arity
949
952
  self.args = [reserved_fn_symbol_map[x] for x in reserved_fn_args[:arity]]
950
953
 
951
954
  async def acall(self, klong, params):
955
+ # Execute the function based on its type
952
956
  if issubclass(type(self.fn), KGLambda):
953
957
  ctx = {reserved_fn_symbols[i]:params[i] for i in range(min(len(reserved_fn_args),len(params)))}
954
958
  r = self.fn(klong, ctx)
959
+ elif callable(self.fn):
960
+ r = self.fn(*params)
955
961
  else:
962
+ # Shouldn't reach here, but handle it
956
963
  r = klong.call(KGCall(self.fn.a, [*params], self.fn.arity))
957
- self.cb(r)
964
+
965
+ # Invoke callback - KGFnWrapper handles dynamic resolution automatically
966
+ if self.cb is not None:
967
+ self.cb(r)
958
968
 
959
969
  def __call__(self, klong, ctx):
960
970
  params = [ctx[x] for x in self.args]
@@ -980,7 +990,8 @@ def eval_sys_fn_create_async_wrapper(klong, x, y):
980
990
  raise KlongException("y must be a function")
981
991
  system = klong['.system']
982
992
  klongloop = system['klongloop']
983
- return KGAsyncCall(klongloop, x, KGFnWrapper(klong, y))
993
+ # KGAsyncCall will wrap the callbacks automatically
994
+ return KGAsyncCall(klongloop, x, y, klong)
984
995
 
985
996
 
986
997
  def create_system_functions_ipc():
klongpy/sys_fn_timer.py CHANGED
@@ -54,6 +54,8 @@ def eval_sys_fn_timer(klong, x, y, z):
54
54
 
55
55
  The callback function returns 1 to continue, 0 to stop time timer.
56
56
 
57
+ If "z" is a named function, the timer re-resolves it on each tick so redefinitions take effect.
58
+
57
59
  Example:
58
60
 
59
61
  cb::{.p("hello")}
@@ -76,12 +78,20 @@ def eval_sys_fn_timer(klong, x, y, z):
76
78
  y= int(y)
77
79
  if y < 0:
78
80
  return "x must be a non-negative integer"
79
- z = z if isinstance(z, KGCall) else KGFnWrapper(klong, z) if isinstance(z, KGFn) else z
80
- if not callable(z):
81
+
82
+ # Wrap the callback - KGFnWrapper now handles dynamic resolution automatically
83
+ if isinstance(z, KGCall):
84
+ return "z must be a function (not a function call)"
85
+ if isinstance(z, KGFn):
86
+ callback = KGFnWrapper(klong, z)
87
+ elif callable(z):
88
+ callback = z
89
+ else:
81
90
  return "z must be a function"
91
+
82
92
  system = klong['.system']
83
93
  klongloop = system['klongloop']
84
- return _call_periodic(klongloop, x, y, z)
94
+ return _call_periodic(klongloop, x, y, callback)
85
95
 
86
96
 
87
97
  def eval_sys_fn_cancel_timer(x):
klongpy/web/sys_fn_web.py CHANGED
@@ -1,5 +1,7 @@
1
1
  import logging
2
2
  import sys
3
+ import asyncio
4
+ import concurrent.futures
3
5
 
4
6
  from aiohttp import web
5
7
 
@@ -67,9 +69,14 @@ def eval_sys_fn_create_web_server(klong, x, y, z):
67
69
  if arity != 1:
68
70
  logging.info(f"GET route {route} handler function requires arity 1, got {arity}")
69
71
  continue
70
- fn = fn if isinstance(fn, KGCall) else KGFnWrapper(klong, fn) if isinstance(fn, KGFn) else fn
71
72
 
72
- 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):
73
80
  try:
74
81
  assert request.method == "GET"
75
82
  return web.Response(text=str(fn(dict(request.rel_url.query))))
@@ -86,9 +93,14 @@ def eval_sys_fn_create_web_server(klong, x, y, z):
86
93
  if arity != 1:
87
94
  logging.info(f"POST route {route} handler function requires arity 1, got {arity}")
88
95
  continue
89
- fn = fn if isinstance(fn, KGCall) else KGFnWrapper(klong, fn) if isinstance(fn, KGFn) else fn
90
96
 
91
- 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):
92
104
  try:
93
105
  assert request.method == "POST"
94
106
  parameters = dict(await request.post())
@@ -113,7 +125,17 @@ def eval_sys_fn_create_web_server(klong, x, y, z):
113
125
  site = web.TCPSite(runner, bind, port)
114
126
  await site.start()
115
127
 
116
- server_task = klong['.system']['ioloop'].create_task(start_server())
128
+ # create the server task in the ioloop thread and capture the task handle
129
+ server_loop = klong['.system']['ioloop']
130
+ task_future = concurrent.futures.Future()
131
+
132
+ def _start():
133
+ task = asyncio.create_task(start_server())
134
+ task_future.set_result(task)
135
+
136
+ server_loop.call_soon_threadsafe(_start)
137
+ server_task = task_future.result()
138
+
117
139
  return WebServerHandle(bind, port, runner, server_task)
118
140
 
119
141
 
@@ -129,7 +151,7 @@ def eval_sys_fn_shutdown_web_server(klong, x):
129
151
  x = x.a.fn
130
152
  if isinstance(x, WebServerHandle) and x.runner is not None:
131
153
  print("shutting down web server")
132
- klong['.system']['ioloop'].run_until_complete(x.shutdown())
154
+ asyncio.run_coroutine_threadsafe(x.shutdown(), klong['.system']['ioloop']).result()
133
155
  return 1
134
156
  return 0
135
157