klongpy 0.7.0__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/interpreter.py CHANGED
@@ -4,7 +4,6 @@ from collections import deque
4
4
  from .adverbs import get_adverb_fn
5
5
  from .backends import get_backend
6
6
  from .core import *
7
- from .backend import is_number
8
7
  from .dyads import create_dyad_functions
9
8
  from .monads import create_monad_functions
10
9
  from .sys_fn import create_system_functions
@@ -55,9 +54,10 @@ class KlongContext():
55
54
 
56
55
  """
57
56
 
58
- def __init__(self, system_contexts):
57
+ def __init__(self, system_contexts, strict_mode=1):
59
58
  self._context = deque([{}, *system_contexts])
60
59
  self._min_ctx_count = len(system_contexts)
60
+ self._strict_mode = strict_mode
61
61
 
62
62
  def start_module(self, name):
63
63
  self.push(KGModule(name))
@@ -71,11 +71,28 @@ class KlongContext():
71
71
 
72
72
  def __setitem__(self, k, v):
73
73
  assert isinstance(k, KGSym)
74
+
74
75
  if k not in reserved_fn_symbols:
76
+ # Check if variable exists in any scope
75
77
  for d in self._context:
76
78
  if in_map(k, d):
77
79
  d[k] = v
78
80
  return k
81
+
82
+ # Variable doesn't exist - check strict mode
83
+ if self._strict_mode >= 1:
84
+ # Check if we're inside a function (more than just global scope)
85
+ in_function = len(self._context) > self._min_ctx_count + 1
86
+
87
+ if in_function:
88
+ # Inside function - disallow creating new variables
89
+ raise KlongException(
90
+ f"undefined variable: {k}\n"
91
+ f" To create a local variable, declare it in the parameter list: {{[{k}]; ...}}\n"
92
+ f" To modify an existing global, ensure it exists before calling the function"
93
+ )
94
+
95
+ # Create new variable in current scope
79
96
  set_context_var(self._context[0], k, v)
80
97
  return k
81
98
 
@@ -221,14 +238,14 @@ class KlongInterpreter():
221
238
  Parameters
222
239
  ----------
223
240
  backend : str, optional
224
- Backend name ('numpy' or 'torch'). If None, uses the default
225
- backend (numpy, unless KLONGPY_BACKEND or USE_TORCH env vars are set).
241
+ Backend name ('numpy' or 'torch'). Defaults to 'numpy'.
226
242
  device : str, optional
227
243
  Device for torch backend ('cpu', 'cuda', 'mps'). Only applies
228
244
  when backend='torch'. If None, auto-selects best available device.
229
245
  """
230
246
  self._backend = get_backend(backend, device=device)
231
- self._context = KlongContext(create_system_contexts())
247
+ strict_mode = 0 # 0=unsafe (default for backward compat), 1=strict, 2=pedantic
248
+ self._context = KlongContext(create_system_contexts(), strict_mode=strict_mode)
232
249
  self._vd = create_dyad_functions(self)
233
250
  self._vm = create_monad_functions(self)
234
251
  self._start_time = time.time()
@@ -332,7 +349,7 @@ class KlongInterpreter():
332
349
  return i+1,arr
333
350
  k = i
334
351
  while True:
335
- ii,c = kg_read(t,i,ignore_newline=True,module=self.current_module())
352
+ ii,c = kg_read(t, i, ignore_newline=True, module=self.current_module())
336
353
  if safe_eq(c, ';'):
337
354
  i = ii
338
355
  if k == i - 1:
@@ -380,7 +397,7 @@ class KlongInterpreter():
380
397
  | V P
381
398
 
382
399
  """
383
- i,a = kg_read(t, i, ignore_newline=ignore_newline, module=self.current_module())
400
+ i,a = kg_read_array(t, i, self._backend, ignore_newline=ignore_newline, module=self.current_module())
384
401
  if a is None:
385
402
  return i,a
386
403
  if safe_eq(a, '{'): # read fn
@@ -597,14 +614,14 @@ class KlongInterpreter():
597
614
  ctx = {} if f_args is None else {reserved_fn_symbol_map[p]: self.call(q) for p,q in zip(reserved_fn_args,f_args)}
598
615
 
599
616
  if is_list(f) and len(f) > 1 and is_list(f[0]) and len(f[0]) > 0:
600
- have_locals = True
601
- for q in f[0]:
602
- if not isinstance(q, KGSym):
603
- have_locals = False
604
- break
617
+ # Filter out semicolons and check if all remaining elements are symbols
618
+ params = [q for q in f[0] if isinstance(q, KGSym)]
619
+ have_locals = len(params) > 0 and all(isinstance(q, KGSym) for q in params)
605
620
  if have_locals:
606
- for q in f[0]:
607
- ctx[q] = q
621
+ for q in params:
622
+ # Don't overwrite function parameters (x, y, z)
623
+ if q not in ctx:
624
+ ctx[q] = q
608
625
  f = f[1:]
609
626
 
610
627
  ctx[reserved_dot_f_symbol] = f
@@ -654,7 +671,7 @@ class KlongInterpreter():
654
671
  return self._eval_fn(x)
655
672
  elif isinstance(x, KGCond):
656
673
  q = self.call(x[0])
657
- p = not ((is_number(q) and q == 0) or is_empty(q))
674
+ p = not ((self._backend.is_number(q) and q == 0) or is_empty(q))
658
675
  return self.call(x[1]) if p else self.call(x[2])
659
676
  elif isinstance(x,list) and len(x) > 0:
660
677
  return [self.call(y) for y in x][-1]
klongpy/monads.py CHANGED
@@ -1,9 +1,6 @@
1
1
  from .core import *
2
2
  from .autograd import grad_of_fn
3
- from .backend import (
4
- kg_asarray, is_integer, is_number, str_to_chr_arr, kg_argsort, array_size, vec_fn
5
- )
6
- import sys
3
+
7
4
 
8
5
  def eval_monad_atom(a):
9
6
  """
@@ -22,7 +19,7 @@ def eval_monad_atom(a):
22
19
  return kg_truth(is_atom(a))
23
20
 
24
21
 
25
- def eval_monad_char(a):
22
+ def eval_monad_char(a, backend):
26
23
  """
27
24
 
28
25
  :#a [Char]
@@ -35,10 +32,10 @@ def eval_monad_char(a):
35
32
  :#10 --> :"newline character"
36
33
 
37
34
  """
38
- return rec_fn(a, lambda x: KGChar(chr(x))) if is_list(a) else KGChar(chr(a))
35
+ return backend.rec_fn(a, lambda x: KGChar(chr(x))) if is_list(a) else KGChar(chr(a))
39
36
 
40
37
 
41
- def eval_monad_enumerate(a):
38
+ def eval_monad_enumerate(a, backend):
42
39
  """
43
40
 
44
41
  !a [Enumerate]
@@ -50,9 +47,9 @@ def eval_monad_enumerate(a):
50
47
  !10 --> [0 1 2 3 4 5 6 7 8 9]
51
48
 
52
49
  """
53
- if not is_integer(a):
50
+ if not backend.is_integer(a):
54
51
  raise RuntimeError(f"enumerate: invalid type error: {a}")
55
- return np.arange(int(a))
52
+ return bknp.arange(int(a))
56
53
 
57
54
 
58
55
  def eval_monad_expand_where(a):
@@ -79,7 +76,7 @@ def eval_monad_expand_where(a):
79
76
 
80
77
  """
81
78
  arr = a if is_list(a) else [a]
82
- return np.repeat(np.arange(len(arr)), arr)
79
+ return bknp.repeat(bknp.arange(len(arr)), arr)
83
80
 
84
81
 
85
82
  def eval_monad_first(a):
@@ -101,7 +98,7 @@ def eval_monad_first(a):
101
98
  return a if is_empty(a) or not is_iterable(a) else a[0]
102
99
 
103
100
 
104
- def eval_monad_floor(a):
101
+ def eval_monad_floor(a, backend):
105
102
  """
106
103
 
107
104
  _a [Floor]
@@ -119,18 +116,10 @@ def eval_monad_floor(a):
119
116
  _1e100 --> 1.0e+100 :"if precision < 100 digits"
120
117
 
121
118
  """
122
- def _floor_to_int(x):
123
- result = np.floor(np.asarray(x, dtype=float))
124
- # Handle both numpy arrays and torch tensors
125
- if hasattr(result, 'astype'):
126
- return result.astype(int)
127
- elif hasattr(result, 'to'): # torch tensor - .to(int) works
128
- return result.to(int)
129
- return int(result)
130
- return vec_fn(a, _floor_to_int)
119
+ return backend.vec_fn(a, backend.floor_to_int)
131
120
 
132
121
 
133
- def eval_monad_format(a):
122
+ def eval_monad_format(a, backend):
134
123
  """
135
124
 
136
125
  $a [Format]
@@ -148,10 +137,10 @@ def eval_monad_format(a):
148
137
  $:foo --> ":foo"
149
138
 
150
139
  """
151
- return f":{a}" if isinstance(a, KGSym) else vec_fn(a, eval_monad_format) if is_list(a) else str(a)
140
+ return f":{a}" if isinstance(a, KGSym) else backend.vec_fn(a, lambda x: eval_monad_format(x, backend)) if is_list(a) else str(a)
152
141
 
153
142
 
154
- def eval_monad_grade_up(a):
143
+ def eval_monad_grade_up(a, backend):
155
144
  """
156
145
 
157
146
  <a [Grade-Up]
@@ -176,10 +165,10 @@ def eval_monad_grade_up(a):
176
165
  >[[1] [2] [3]] --> [2 1 0]
177
166
 
178
167
  """
179
- return kg_argsort(kg_asarray(a))
168
+ return kg_argsort(backend.kg_asarray(a), backend)
180
169
 
181
170
 
182
- def eval_monad_grade_down(a):
171
+ def eval_monad_grade_down(a, backend):
183
172
  """
184
173
 
185
174
  >a [Grade-Down]
@@ -187,10 +176,10 @@ def eval_monad_grade_down(a):
187
176
  See [Grade-Up].
188
177
 
189
178
  """
190
- return kg_argsort(kg_asarray(a), descending=True)
179
+ return kg_argsort(backend.kg_asarray(a), backend, descending=True)
191
180
 
192
181
 
193
- def eval_monad_groupby(a):
182
+ def eval_monad_groupby(a, backend):
194
183
  """
195
184
 
196
185
  =a [Group]
@@ -207,15 +196,15 @@ def eval_monad_groupby(a):
207
196
  ="hello foo" --> [[0] [1] [2 3] [4 7 8] [5] [6]]
208
197
 
209
198
  """
210
- arr = kg_asarray(a)
211
- if array_size(arr) == 0:
199
+ arr = backend.kg_asarray(a)
200
+ if backend.array_size(arr) == 0:
212
201
  return arr
213
- vals, inverse = np.unique(arr, return_inverse=True)
214
- groups = [np.where(inverse == i)[0] for i in range(len(vals))]
215
- return kg_asarray(groups)
202
+ vals, inverse = bknp.unique(arr, return_inverse=True)
203
+ groups = [bknp.where(inverse == i)[0] for i in range(len(vals))]
204
+ return backend.kg_asarray(groups)
216
205
 
217
206
 
218
- def eval_monad_list(a):
207
+ def eval_monad_list(a, backend):
219
208
  """
220
209
 
221
210
  ,a [List]
@@ -229,12 +218,10 @@ def eval_monad_list(a):
229
218
  """
230
219
  if is_char(a):
231
220
  return str(a)
232
- if isinstance(a, KGSym):
233
- return np.asarray([a],dtype=object) # np interprets ':foo" as ':fo"
234
- return np.asarray([a])
221
+ return backend.kg_asarray([a])
235
222
 
236
223
 
237
- def eval_monad_negate(a):
224
+ def eval_monad_negate(a, backend):
238
225
  """
239
226
 
240
227
  -a [Negate]
@@ -247,10 +234,10 @@ def eval_monad_negate(a):
247
234
  -1.23 --> -1.23
248
235
 
249
236
  """
250
- return vec_fn(a, lambda x: np.negative(kg_asarray(x)))
237
+ return backend.vec_fn(a, lambda x: backend.np.negative(backend.kg_asarray(x)))
251
238
 
252
239
 
253
- def eval_monad_not(a):
240
+ def eval_monad_not(a, backend):
254
241
  """
255
242
 
256
243
  ~a [Not]
@@ -266,11 +253,11 @@ def eval_monad_not(a):
266
253
 
267
254
  """
268
255
  def _neg(x):
269
- return 1 if is_empty(x) else 0 if is_dict(x) or isinstance(x, (KGFn, KGSym)) else kg_truth(np.logical_not(np.asarray(x, dtype=object)))
270
- return vec_fn(a, _neg) if not is_empty(a) else _neg(a)
256
+ return 1 if is_empty(x) else 0 if is_dict(x) or isinstance(x, (KGFn, KGSym)) else kg_truth(bknp.logical_not(bknp.asarray(x, dtype=object)))
257
+ return backend.vec_fn(a, _neg) if not is_empty(a) else _neg(a)
271
258
 
272
259
 
273
- def eval_monad_range(a):
260
+ def eval_monad_range(a, backend):
274
261
  """
275
262
 
276
263
  ?a [Range]
@@ -283,28 +270,19 @@ def eval_monad_range(a):
283
270
  ?"aaabbcccd" --> "abcd"
284
271
 
285
272
  """
273
+ np_backend = backend.np
286
274
  if isinstance(a, str):
287
- return ''.join(np.unique(str_to_chr_arr(a)))
288
- elif np.isarray(a):
289
- if a.dtype != 'O' and a.ndim > 1:
290
- _,ids = np.unique(a,axis=0,return_index=True)
275
+ return ''.join(bknp.unique(backend.str_to_chr_arr(a)))
276
+ elif np_backend.isarray(a):
277
+ dtype_kind = backend.get_dtype_kind(a)
278
+ if dtype_kind != 'O' and a.ndim > 1:
279
+ # Use numpy for unique with return_index across backends
280
+ a_np = backend.to_numpy(a) if backend.is_backend_array(a) else a
281
+ _, ids = bknp.unique(a_np, axis=0, return_index=True)
282
+ ids.sort()
283
+ return a[ids]
291
284
  else:
292
285
  # handle the jagged / mixed array case
293
- # from functools import total_ordering
294
- # @total_ordering
295
- # class Wrapper:
296
- # def __init__(self, x):
297
- # self.x = x
298
- # def __eq__(self,o):
299
- # print("eq")
300
- # return array_equal(self.x, o.x)
301
- # def __ne__(self,o):
302
- # return not array_equal(self.x, o.x)
303
- # def __lt__(self, o):
304
- # u = np.sort(np.asarray([self.x, o.x]))
305
- # return u[0] == self.x
306
- # # return u[0] if isinstance(u,np.ndarray) else u
307
- # _,ids = np.unique([Wrapper(x) for x in a], return_index=True)
308
286
  # TODO: Make UNIQUE work. this feels so dirty.
309
287
  s = set()
310
288
  arr = []
@@ -313,13 +291,11 @@ def eval_monad_range(a):
313
291
  if sx not in s:
314
292
  s.add(sx)
315
293
  arr.append(x)
316
- return np.asarray(arr, dtype=object)
317
- ids.sort()
318
- a = a[ids]
294
+ return backend.kg_asarray(arr)
319
295
  return a
320
296
 
321
297
 
322
- def eval_monad_reciprocal(a):
298
+ def eval_monad_reciprocal(a, backend):
323
299
  """
324
300
 
325
301
  %a [Reciprocal]
@@ -333,7 +309,11 @@ def eval_monad_reciprocal(a):
333
309
  %0.1 --> 10.0
334
310
 
335
311
  """
336
- return vec_fn(a, lambda x: np.reciprocal(np.asarray(x,dtype=float)))
312
+ if not is_list(a) and backend.is_number(a):
313
+ a_val = backend.scalar_to_python(a) if backend.is_backend_array(a) else a
314
+ if a_val == 0:
315
+ return KLONG_UNDEFINED
316
+ return backend.vec_fn(a, lambda x: bknp.reciprocal(bknp.asarray(x, dtype=float)))
337
317
 
338
318
 
339
319
  def eval_monad_reverse(a):
@@ -353,7 +333,7 @@ def eval_monad_reverse(a):
353
333
  return a[::-1]
354
334
 
355
335
 
356
- def eval_monad_shape(a):
336
+ def eval_monad_shape(a, backend):
357
337
  """
358
338
 
359
339
  ^a [Shape]
@@ -405,12 +385,20 @@ def eval_monad_shape(a):
405
385
 
406
386
  """
407
387
 
388
+ def _normalize_backend_array(x):
389
+ return backend.to_numpy(x) if backend.is_backend_array(x) else x
390
+
408
391
  def _a(x): # use numpy's natural shape by replacing all strings with arrays
409
- return np.asarray([np.empty(len(y)) if isinstance(y,str) else (_a(y) if is_list(y) else y) for y in x])
410
- return 0 if is_atom(a) else np.asarray([len(a)]) if isinstance(a,str) else np.asarray(_a(a).shape)
392
+ x = _normalize_backend_array(x)
393
+ return bknp.asarray([
394
+ bknp.empty(len(y)) if isinstance(y, str) else (_a(y) if is_list(y) else _normalize_backend_array(y))
395
+ for y in x
396
+ ])
397
+ a = _normalize_backend_array(a)
398
+ return 0 if is_atom(a) else bknp.asarray([len(a)]) if isinstance(a, str) else bknp.asarray(_a(a).shape)
411
399
 
412
400
 
413
- def eval_monad_size(a):
401
+ def eval_monad_size(a, backend):
414
402
  """
415
403
 
416
404
  #a [Size]
@@ -429,7 +417,7 @@ def eval_monad_size(a):
429
417
  #0cA --> 65
430
418
 
431
419
  """
432
- return np.abs(a) if is_number(a) else ord(a) if is_char(a) else len(a)
420
+ return backend.np.abs(a) if backend.is_number(a) else ord(a) if is_char(a) else len(a)
433
421
 
434
422
 
435
423
  def eval_monad_transpose(a):
@@ -444,10 +432,10 @@ def eval_monad_transpose(a):
444
432
  +[] --> []
445
433
 
446
434
  """
447
- return np.transpose(np.asarray(a))
435
+ return bknp.transpose(bknp.asarray(a))
448
436
 
449
437
 
450
- def eval_monad_undefined(a):
438
+ def eval_monad_undefined(a, backend):
451
439
  """
452
440
 
453
441
  :_a [Undefined]
@@ -462,7 +450,7 @@ def eval_monad_undefined(a):
462
450
  :_:valid --> 0
463
451
 
464
452
  """
465
- return kg_truth(a is None or (np.isinf(a) if is_number(a) else False))
453
+ return kg_truth(a is None or a is KLONG_UNDEFINED)
466
454
 
467
455
 
468
456
  def eval_monad_track(a):
@@ -488,18 +476,40 @@ def eval_monad_grad(klong, a):
488
476
 
489
477
 
490
478
  def create_monad_functions(klong):
491
- def _get_name(s):
492
- s = s.strip()
493
- return s[:s.index('a')]
494
-
495
- registry = {}
496
-
497
- m = sys.modules[__name__]
498
- for x in filter(lambda n: n.startswith("eval_monad_"), dir(m)):
499
- fn = getattr(m,x)
500
- name = _get_name(fn.__doc__)
501
- if fn.__code__.co_argcount == 2 and 'klong' in fn.__code__.co_varnames:
502
- fn = lambda a,f=fn,klong=klong: f(klong, a)
503
- registry[name] = fn
504
-
505
- return registry
479
+ backend = klong._backend
480
+
481
+ # Simple monads that don't need backend or klong
482
+ simple = {
483
+ '@': eval_monad_atom,
484
+ '&': eval_monad_expand_where,
485
+ '*': eval_monad_first,
486
+ '|': eval_monad_reverse,
487
+ '+': eval_monad_transpose,
488
+ '˙': eval_monad_track,
489
+ }
490
+
491
+ # Monads needing backend
492
+ backend_monads = {
493
+ ',': lambda a: eval_monad_list(a, backend),
494
+ ':#': lambda a: eval_monad_char(a, backend),
495
+ '!': lambda a: eval_monad_enumerate(a, backend),
496
+ '_': lambda a: eval_monad_floor(a, backend),
497
+ '$': lambda a: eval_monad_format(a, backend),
498
+ '<': lambda a: eval_monad_grade_up(a, backend),
499
+ '>': lambda a: eval_monad_grade_down(a, backend),
500
+ '=': lambda a: eval_monad_groupby(a, backend),
501
+ '-': lambda a: eval_monad_negate(a, backend),
502
+ '~': lambda a: eval_monad_not(a, backend),
503
+ '?': lambda a: eval_monad_range(a, backend),
504
+ '%': lambda a: eval_monad_reciprocal(a, backend),
505
+ '#': lambda a: eval_monad_size(a, backend),
506
+ ':_': lambda a: eval_monad_undefined(a, backend),
507
+ '^': lambda a: eval_monad_shape(a, backend),
508
+ }
509
+
510
+ # Monads needing klong
511
+ klong_monads = {
512
+ '∇': lambda a: eval_monad_grad(klong, a),
513
+ }
514
+
515
+ return {**simple, **backend_monads, **klong_monads}