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/core.py CHANGED
@@ -1,1094 +1,113 @@
1
- import copy
2
- import inspect
3
- import weakref
4
- from enum import Enum
5
- import sys
6
- import numpy
7
-
8
- from .backend import np, TorchUnsupportedDtypeError, get_default_backend, to_numpy
9
-
10
- # python3.11 support
11
- if not hasattr(inspect, 'getargspec'):
12
- inspect.getargspec = inspect.getfullargspec
13
-
14
-
15
- def get_dtype_kind(arr, backend):
16
- """
17
- Get the dtype 'kind' character for an array (numpy or torch).
18
-
19
- Returns:
20
- 'O' for object dtype
21
- 'i' for integer types
22
- 'f' for float types
23
- 'u' for unsigned integer
24
- 'b' for boolean
25
- 'c' for complex
26
- """
27
- return backend.get_dtype_kind(arr)
28
-
29
-
30
- class KlongException(Exception):
31
- pass
32
-
33
-
34
- class KGSym(str):
35
- def __repr__(self):
36
- return f":{super().__str__()}"
37
- def __eq__(self, o):
38
- return isinstance(o,KGSym) and self.__str__() == o.__str__()
39
- def __hash__(self):
40
- return super().__hash__()
41
-
42
-
43
- def get_fn_arity_str(arity):
44
- if arity == 0:
45
- return ":nilad"
46
- elif arity == 1:
47
- return ":monad"
48
- elif arity == 2:
49
- return ":dyad"
50
- return ":triad"
51
-
52
-
53
- class KGFn:
54
- def __init__(self, a, args, arity):
55
- self.a = a
56
- self.args = args
57
- self.arity = arity
58
-
59
- def __str__(self):
60
- return get_fn_arity_str(self.arity)
61
-
62
- def is_op(self):
63
- return isinstance(self.a,KGOp)
64
-
65
- def is_adverb_chain(self):
66
- return isinstance(self.a,list) and isinstance(self.a[0],KGAdverb)
67
-
68
-
69
- class KGFnWrapper:
70
- """
71
- Wrapper for KGFn that enables calling from Python with dynamic symbol resolution.
72
-
73
- When a KGFn is stored and later invoked, this wrapper automatically re-resolves
74
- the symbol to use the current function definition. This matches k4 behavior and
75
- provides REPL-friendly semantics where function redefinitions take effect immediately.
76
-
77
- Example:
78
- fn = klong['callback'] # Returns KGFnWrapper
79
- klong('callback::{new implementation}')
80
- fn(args) # Uses the NEW implementation
81
- """
82
-
83
- def __init__(self, klong, fn, sym=None):
84
- self.klong = klong
85
- self.fn = fn
86
- # Use provided symbol name (cached) or search for it
87
- self._sym = sym if sym is not None else self._find_symbol(fn)
88
-
89
- def _find_symbol(self, fn):
90
- """Find which symbol this function is currently bound to"""
91
- if not isinstance(fn, KGFn) or isinstance(fn, KGCall):
92
- return None
93
-
94
- # Search the context for this function
95
- # Skip reserved symbols (x, y, z, .f) which are function parameters, not stored callbacks
96
- for sym, value in self.klong._context:
97
- # Skip reserved symbols - use the module constants
98
- if sym in reserved_fn_symbols or sym == reserved_dot_f_symbol:
99
- continue
100
- if value is fn:
101
- return sym
102
- return None
103
-
104
- def __call__(self, *args, **kwargs):
105
- # Try to resolve dynamically first if we have a symbol
106
- if self._sym is not None:
107
- try:
108
- current = self.klong._context[self._sym]
109
- if isinstance(current, KGFn) and not isinstance(current, KGCall):
110
- # Use the current definition
111
- if len(args) != current.arity:
112
- raise RuntimeError(f"Klong function called with {len(args)} but expected {current.arity}")
113
- fn_args = [np.asarray(x) if isinstance(x, list) else x for x in args]
114
- return self.klong.call(KGCall(current.a, [*fn_args], current.arity))
115
- except KeyError:
116
- # Symbol was deleted, fall through to original function
117
- pass
118
-
119
- if len(args) != self.fn.arity:
120
- raise RuntimeError(f"Klong function called with {len(args)} but expected {self.fn.arity}")
121
- fn_args = [np.asarray(x) if isinstance(x, list) else x for x in args]
122
- return self.klong.call(KGCall(self.fn.a, [*fn_args], self.fn.arity))
123
-
124
-
125
- class KGCall(KGFn):
126
- def __str__(self):
127
- return self.a.__str__() if issubclass(type(self.a), KGLambda) else super().__str__()
128
-
129
-
130
- class KGOp:
131
- def __init__(self, a, arity):
132
- self.a = a
133
- self.arity = arity
134
-
135
-
136
- class KGAdverb:
137
- def __init__(self, a, arity):
138
- self.a = a
139
- self.arity = arity
140
-
141
-
142
- class KGChar(str):
143
- pass
144
-
145
-
146
- class KGCond(list):
147
- pass
148
-
149
-
150
- def safe_inspect(fn, follow_wrapped=True):
151
- try:
152
- return inspect.signature(fn, follow_wrapped=follow_wrapped).parameters
153
- except ValueError:
154
- return {"args":[]}
155
-
156
- class KGLambda:
157
- """
158
- KGLambda wraps a lambda and make it available to Klong, allowing for direct
159
- integration of python functions in Klong.
160
-
161
- Introspection is used to infer which parameters should be collected from the
162
- current context and passed to the lambda. Parameter names must be x,y, or z
163
- according to the klong convention. The value for these parameters are
164
- extracted directly from the currentcontext.
165
-
166
- If a lambda requires access to klong itself, that must be the first parameter.
167
-
168
- Function arity is computed by examining the arguments.
169
-
170
- e.g.
171
-
172
- lambda x,y: x + y
173
- lambda klong, x: klong(x)
174
-
175
- """
176
- def __init__(self, fn, args=None, provide_klong=False, wildcard=False):
177
- self.fn = fn
178
- params = args or safe_inspect(fn)
179
- self.args = [reserved_fn_symbol_map[x] for x in reserved_fn_args if x in params]
180
- self._provide_klong = provide_klong or 'klong' in params
181
- self._wildcard = wildcard
182
-
183
- def _get_pos_args(self, ctx):
184
- if self._wildcard:
185
- pos_args = []
186
- for sym in reserved_fn_symbols:
187
- try:
188
- pos_args.append(ctx[sym])
189
- except KeyError:
190
- break
191
- else:
192
- pos_args = [ctx[x] for x in self.args]
193
- return pos_args
194
-
195
- def __call__(self, klong, ctx):
196
- pos_args = self._get_pos_args(ctx)
197
- return self.fn(klong, *pos_args) if self._provide_klong else self.fn(*pos_args)
198
-
199
- def call_with_kwargs(self, klong, ctx, kwargs):
200
- pos_args = self._get_pos_args(ctx)
201
- return self.fn(klong, *pos_args, **kwargs) if self._provide_klong else self.fn(*pos_args, **kwargs)
202
-
203
- def get_arity(self):
204
- return len(self.args)
205
-
206
- def __str__(self):
207
- return get_fn_arity_str(self.get_arity())
208
-
209
-
210
- class KGChannelDir(Enum):
211
- INPUT=1
212
- OUTPUT=2
213
-
214
-
215
- class KGChannel:
216
- class FileHandler:
217
- def __init__(self, raw, parent):
218
- self._raw = raw
219
- self._ref = weakref.ref(parent, self.close)
220
-
221
- def close(self, *args):
222
- self._raw.close()
223
-
224
- def __init__(self, raw, channel_dir):
225
- self.channel_dir = channel_dir
226
- self.raw = raw
227
- self._fh = KGChannel.FileHandler(raw, self)
228
- self.at_eof = False
229
-
230
- def __enter__(self):
231
- return self
232
-
233
- def __exit__(self, ext_type, exc_value, traceback):
234
- self._fh.close()
235
-
236
-
237
- class RangeError(Exception):
238
- def __init__(self, i):
239
- self.i = i
240
- super().__init__()
241
-
242
-
243
- reserved_fn_args = ['x','y','z']
244
- reserved_fn_symbols = [KGSym(n) for n in reserved_fn_args]
245
- reserved_fn_symbol_map = {n:KGSym(n) for n in reserved_fn_args}
246
- reserved_dot_f_symbol = KGSym('.f')
247
-
248
-
249
- def is_list(x):
250
- return isinstance(x,list) or (np.isarray(x) and x.ndim > 0)
251
-
252
-
253
- def is_iterable(x):
254
- return is_list(x) or (isinstance(x,str) and not isinstance(x, (KGSym, KGChar)))
255
-
256
-
257
- def is_empty(a):
258
- return is_iterable(a) and len(a) == 0
259
-
260
-
261
- def is_dict(x):
262
- return isinstance(x, dict)
263
-
264
-
265
- def to_list(a):
266
- return a if isinstance(a, list) else a.tolist() if np.isarray(a) else [a]
267
-
268
-
269
- def is_integer(x, backend):
270
- if issubclass(type(x), (int, numpy.integer)):
271
- return True
272
- # Handle 0-dim numpy arrays
273
- if isinstance(x, numpy.ndarray) and x.ndim == 0:
274
- return numpy.issubdtype(x.dtype, numpy.integer)
275
- # Handle backend-specific scalar integers (e.g., torch tensors)
276
- return backend.is_scalar_integer(x)
277
-
278
-
279
- def is_float(x, backend):
280
- if issubclass(type(x), (float, numpy.floating, int)):
281
- return True
282
- # Handle 0-dim numpy arrays
283
- if isinstance(x, numpy.ndarray) and x.ndim == 0:
284
- return numpy.issubdtype(x.dtype, numpy.floating)
285
- # Handle backend-specific scalar floats (e.g., torch tensors)
286
- return backend.is_scalar_float(x)
287
-
288
-
289
- def is_number(a, backend):
290
- if is_float(a, backend) or is_integer(a, backend):
291
- return True
292
- # Handle 0-dim numpy arrays
293
- if isinstance(a, numpy.ndarray) and a.ndim == 0:
294
- return numpy.issubdtype(a.dtype, numpy.number)
295
- # Handle 0-dim backend tensors as numbers
296
- if backend.is_backend_array(a) and hasattr(a, 'ndim') and a.ndim == 0:
297
- return True
298
- return False
299
-
300
-
301
- def str_is_float(b):
302
- try:
303
- float(b)
304
- return True
305
- except ValueError:
306
- return False
307
-
308
- def in_map(x, v):
309
- try:
310
- return x in v
311
- except Exception:
312
- return False
313
-
314
-
315
- def kg_asarray(a, backend):
316
- """Convert input to array using the backend's kg_asarray method."""
317
- return backend.kg_asarray(a)
318
-
319
-
320
- def kg_equal(a, b, backend):
321
- """Compare two values or arrays for equality, handling nested arrays and tensors."""
322
- if a is b:
323
- return True
324
-
325
- # Check for arrays (numpy or backend-specific)
326
- is_numpy_a = isinstance(a, numpy.ndarray)
327
- is_numpy_b = isinstance(b, numpy.ndarray)
328
- is_backend_a = backend.is_backend_array(a)
329
- is_backend_b = backend.is_backend_array(b)
330
-
331
- na, nb = is_numpy_a or is_backend_a, is_numpy_b or is_backend_b
332
-
333
- # Handle arrays with same dtype
334
- if na and nb:
335
- a_dtype = get_dtype_kind(a, backend)
336
- b_dtype = get_dtype_kind(b, backend)
337
- if a_dtype == b_dtype and a_dtype != 'O':
338
- return bool(np.array_equal(a, b))
339
-
340
- na, nb = na or isinstance(a, list), nb or isinstance(b, list)
341
-
342
- if na != nb:
343
- # One is array/list, the other is not - could be scalar tensor/array vs scalar
344
- # Handle comparing 0-dim arrays/tensors with scalars
345
- if is_numpy_a and a.ndim == 0 and not nb:
346
- return kg_equal(a.item(), b, backend)
347
- if is_numpy_b and b.ndim == 0 and not na:
348
- return kg_equal(a, b.item(), backend)
349
- if is_backend_a and hasattr(a, 'ndim') and a.ndim == 0 and not nb:
350
- return kg_equal(backend.scalar_to_python(a), b, backend)
351
- if is_backend_b and hasattr(b, 'ndim') and b.ndim == 0 and not na:
352
- return kg_equal(a, backend.scalar_to_python(b), backend)
353
- return False
354
-
355
- if na:
356
- # Handle 0-dim arrays/tensors - compare as scalars
357
- a_is_0d = hasattr(a, 'ndim') and a.ndim == 0
358
- b_is_0d = hasattr(b, 'ndim') and b.ndim == 0
359
- if a_is_0d or b_is_0d:
360
- a_val = backend.scalar_to_python(a) if a_is_0d else a
361
- b_val = backend.scalar_to_python(b) if b_is_0d else b
362
- return kg_equal(a_val, b_val, backend)
363
- return len(a) == len(b) and all(kg_equal(x, y, backend) for x, y in zip(a, b))
364
-
365
- if is_number(a, backend) and is_number(b, backend):
366
- # Convert tensors to Python scalars for comparison
367
- if backend.is_backend_array(a):
368
- a = backend.scalar_to_python(a)
369
- if backend.is_backend_array(b):
370
- b = backend.scalar_to_python(b)
371
- result = np.isclose(a, b)
372
- # np.isclose might return an array/tensor, ensure we return bool
373
- if hasattr(result, 'item'):
374
- return bool(result.item())
375
- return bool(result)
376
-
377
- result = a == b
378
- # Handle tensor/array result from comparison
379
- if hasattr(result, 'all'):
380
- # For arrays, check if all elements are equal
381
- return bool(result.all())
382
- if hasattr(result, 'item'):
383
- return bool(result.item())
384
- return bool(result)
385
-
386
- def has_none(a):
387
- if isinstance(a,list):
388
- for q in a:
389
- if q is None:
390
- return True
391
- return False
392
-
393
-
394
- def cmatch(t, i, c):
395
- return i < len(t) and t[i] == c
396
-
397
-
398
- def cmatch2(t, i, a, b):
399
- return cmatch(t, i, a) and cmatch(t, i+1, b)
400
-
401
-
402
- def cpeek(t,i):
403
- return t[i] if i < len(t) else None
404
-
405
-
406
- def cpeek2(t,i):
407
- return t[i:i+2] if i < (len(t)-1) else None
408
-
409
-
410
- class UnexpectedChar(Exception):
411
- def __init__(self, t, i, c):
412
- super().__init__(f"t: {t[i-10:i+10]} pos: {i} char: {c}")
413
-
414
-
415
- class UnexpectedEOF(Exception):
416
- def __init__(self, t, i):
417
- self.t = t
418
- self.i = i
419
- super().__init__(f"t: {t[i-10:]} pos: {i}")
420
-
421
-
422
- def cexpect(t, i, c):
423
- if cmatch(t, i, c):
424
- return i + 1
425
- raise UnexpectedChar(t, i, c)
426
-
427
-
428
- def cexpect2(t, i, a, b):
429
- if cmatch(t, i, a) and cmatch(t, i+1, b):
430
- return i + 2
431
- raise UnexpectedChar(t, i, b)
432
-
433
-
434
- def safe_eq(a,b):
435
- return isinstance(a,type(b)) and a == b
436
-
437
-
438
- def rec_flatten(a):
439
- if is_list(a) and len(a) > 0:
440
- return np.concatenate([rec_flatten(x) if is_list(x) else np.array([x]) for x in a]).ravel()
441
- return a
442
-
443
-
444
- def rec_fn(a,f):
445
- _backend = get_default_backend()
446
- return _backend.kg_asarray([rec_fn(x, f) for x in a]) if is_list(a) else f(a)
447
-
448
-
449
- def vec_fn(a, f, backend):
450
- """
451
- Apply a function `f` to an array `a`, with support for both nested arrays and direct vectorized operation.
452
- """
453
- if np.isarray(a) and a.dtype == 'O':
454
- # For object arrays, process each element and preserve structure
455
- result = [((vec_fn(x, f, backend)) if is_list(x) else f(x)) for x in a]
456
- return numpy.asarray(result, dtype=object)
457
- return f(a)
458
-
459
-
460
- def vec_fn2(a, b, f):
461
- """
462
- Apply function `f` recursively to the elements of `a` and `b`, which can be scalar values, vectors, or nested vectors.
463
-
464
- This function distinguishes 8 cases based on the types and dimensions of `a` and `b`:
465
-
466
- 1. vec[A],vec[B]: `f` is applied directly to `a` and `b`.
467
- 2. vec[A],obj_vec[B]: `f` is applied recursively to pairs of elements in `a` and `b`.
468
- 3. vec[A],scalar[B]: `f` is applied directly to `a` and `b`.
469
- 4. obj_vec[A],vec[B]: `f` is applied recursively to pairs of elements in `a` and `b`.
470
- 5. obj_vec[A],scalar[B]: `f` is applied recursively to the elements in `a` and the scalar `b`.
471
- 6. scalar[A],vec[B]: `f` is applied directly to `a` and `b`.
472
- 7. scalar[A],obj_vec[B]: `f` is applied recursively to the scalar `a` and the elements in `b`.
473
- 8. scalar[A],scalar[B]: `f` is applied directly to `a` and `b`.
474
-
475
- Parameters
476
- ----------
477
- a, b : numpy.array or any type
478
- The inputs to `f`. They can be numpy arrays of any data type. If they are arrays, they should have the same shape.
479
- Non-array inputs can be of any type that `f` can accept.
480
-
481
- f : callable
482
- A function that takes two arguments and can handle the types and dimensions of `a` and `b`.
483
-
484
- Returns
485
- -------
486
- numpy.array or any type
487
- The result of applying `f` to `a` and `b`, which can be a scalar, a vector, or a nested vector depending on
488
- the inputs and `f`.
489
-
490
- Notes
491
- -----
492
- This function assumes that `f` can handle the types and dimensions of `a` and `b`, and that `a` and `b` have the same
493
- shape if they are arrays. It does not check these conditions, so unexpected results or errors may occur if they are
494
- not satisfied.
495
-
496
- """
497
- _backend = get_default_backend()
498
- _kg_asarray = _backend.kg_asarray
499
- if np.isarray(a):
500
- if a.dtype == 'O':
501
- if np.isarray(b):
502
- assert len(a) == len(b)
503
- return _kg_asarray([vec_fn2(x, y, f) for x,y in zip(a,b)])
504
- else:
505
- return _kg_asarray([vec_fn2(x, b, f) for x in a])
506
- elif np.isarray(b) and b.dtype == 'O':
507
- assert len(a) == len(b)
508
- return _kg_asarray([vec_fn2(x, y, f) for x,y in zip(a,b)])
509
- elif np.isarray(b) and b.dtype == 'O':
510
- return _kg_asarray([vec_fn2(a, x, f) for x in b])
511
- return f(a,b)
512
-
513
-
514
- def is_symbolic(c):
515
- return isinstance(c, str) and (c.isalpha() or c.isdigit() or c == '.')
516
-
517
-
518
- def is_char(x):
519
- # Check for both core and backend KGChar classes
520
- if isinstance(x, KGChar):
521
- return True
522
- # Also check for backend KGChar (in case they're different classes)
523
- return type(x).__name__ == 'KGChar' and isinstance(x, str)
524
-
525
-
526
- def is_atom(x):
527
- """ All objects except for non-empty lists and non-empty strings are atoms. """
528
- return is_empty(x) if is_iterable(x) else True
529
-
530
-
531
- def kg_truth(x):
532
- return x*1
533
-
534
-
535
- def str_to_chr_arr(s, backend):
536
- """
537
- Convert string to character array.
538
-
539
- Parameters
540
- ----------
541
- s : str
542
- The string to convert.
543
- backend : BackendProvider
544
- The backend to use.
545
-
546
- Returns
547
- -------
548
- array
549
- Array of KGChar objects.
550
-
551
- Raises
552
- ------
553
- UnsupportedDtypeError
554
- If the backend doesn't support string operations.
555
- """
556
- return backend.str_to_char_array(s)
557
-
558
-
559
- def read_num(t, i=0):
560
- p = i
561
- use_float = False
562
- if t[i] == '-':
563
- i += 1
564
- while i < len(t):
565
- if t[i] == '.':
566
- use_float = True
567
- elif t[i] == 'e':
568
- use_float = True
569
- if cmatch(t,i+1,'-'):
570
- i += 2
571
- elif not t[i].isnumeric():
572
- break
573
- i += 1
574
- return i, float(t[p:i]) if use_float else int(t[p:i])
575
-
576
-
577
- def read_char(t, i):
578
- i = cexpect2(t, i, '0', 'c')
579
- if i >= len(t):
580
- raise UnexpectedEOF(t, i)
581
- return i+1, KGChar(t[i])
582
-
583
-
584
- def read_sym(t, i=0, module=None):
585
- p = i
586
- while i < len(t) and is_symbolic(t[i]):
587
- i += 1
588
- x = t[p:i]
589
- return i, reserved_fn_symbol_map.get(x) or KGSym(x if x.startswith('.') or module is None else f"{x}`{module}")
590
-
591
-
592
- def read_op(t, i=0):
593
- if cmatch2(t,i,'\\','~') or cmatch2(t,i,'\\','*'):
594
- return i+2,KGOp(t[i:i+2],arity=0)
595
- return i+1,KGOp(t[i:i+1],arity=0)
596
-
597
-
598
- def read_shifted_comment(t, i=0):
599
- while i < len(t):
600
- c = t[i]
601
- if c == '"':
602
- i += 1
603
- if not cmatch(t,i,'"'):
604
- break
605
- i += 1
606
- return i
607
-
608
-
609
- def read_sys_comment(t,i,a):
610
- """
611
-
612
- .comment(x) [Comment]
613
-
614
- Read and discard lines until the current line starts with the
615
- string specified in "x". Also discard the line containing the
616
- end-of-comment marker and return "x".
617
-
618
- Example: .comment("end-of-comment")
619
- this will be ignored
620
- this, too: *%(*^#)&(#
621
- end-of-comment
622
-
623
- NOTE: this is handled in the parsing phase and is not a runtime function
624
-
625
- """
626
- try:
627
- j = t[i:].index(a)
628
- while t[i+j+1:].startswith(a):
629
- j += 1
630
- return i + j + len(a)
631
- except ValueError:
632
- return RuntimeError("end of comment not found")
633
-
634
-
635
- def skip_space(t, i=0, ignore_newline=False):
636
- """
637
- NOTE: a newline character translates to a semicolon in Klong,
638
- except in functions, dictionaries, conditional expressions,
639
- and lists. So
640
- """
641
- while i < len(t) and (t[i].isspace() and (ignore_newline or t[i] != '\n')):
642
- i += 1
643
- return i
644
-
645
-
646
- def skip(t, i=0, ignore_newline=False):
647
- i = skip_space(t,i,ignore_newline=ignore_newline)
648
- if cmatch2(t, i, ':', '"'):
649
- i = read_shifted_comment(t, i+2)
650
- i = skip(t, i)
651
- return i
652
-
653
-
654
- def read_list(t, delim, i=0, module=None, level=1):
655
- """
656
-
657
- # A list is any number of class lexemes (or lists) delimited by
658
- # square brackets.
659
-
660
- L := '[' (C|L)* ']'
661
-
662
- """
663
- backend = get_default_backend()
664
- arr = []
665
- i = skip(t,i,ignore_newline=True)
666
- while not cmatch(t,i,delim) and i < len(t):
667
- # we can knowingly read neg numbers in list context
668
- i, q = kg_read(t, i, read_neg=True, ignore_newline=True, module=module, list_level=level+1)
669
- if q is None:
670
- break
671
- if safe_eq(q, '['):
672
- i,q = read_list(t, ']', i=i, module=module, level=level+1)
673
- arr.append(q)
674
- i = skip(t,i,ignore_newline=True)
675
- if cmatch(t,i,delim):
676
- i += 1
677
- if level == 1:
678
- try:
679
- aa = kg_asarray(arr, backend)
680
- if get_dtype_kind(aa, backend) not in ['O','i','f']:
681
- aa = numpy.asarray(arr, dtype=object)
682
- except TorchUnsupportedDtypeError:
683
- # Backend can't handle this data - fall back to numpy object array
684
- # Recursively convert inner lists to arrays, converting tensors to numpy
685
- def convert_inner(x):
686
- if isinstance(x, list):
687
- try:
688
- result = kg_asarray(x, backend)
689
- # Convert tensor to numpy for object array compatibility
690
- return to_numpy(result)
691
- except TorchUnsupportedDtypeError:
692
- return numpy.asarray([convert_inner(e) for e in x], dtype=object)
693
- # Convert any tensors to numpy
694
- return to_numpy(x)
695
- aa = numpy.asarray([convert_inner(x) for x in arr], dtype=object)
696
- else:
697
- aa = arr
698
- return i, aa
699
-
700
-
701
- def read_string(t, i=0):
702
- """
703
-
704
- ".*" [String]
705
-
706
- A string is (almost) any sequence of characters enclosed by
707
- double quote characters. To include a double quote character in
708
- a string, it has to be duplicated, so the above regex is not
709
- entirely correct. A comment is a shifted string (see below).
710
- Examples: ""
711
- "hello, world"
712
- "say ""hello""!"
713
-
714
- Note: this comforms to the KG read_string impl.
715
- perf tests show that the final join is fast for short strings
716
-
717
- """
718
- r = []
719
- while i < len(t):
720
- c = t[i]
721
- if c == '"':
722
- i += 1
723
- if not cmatch(t,i,'"'):
724
- break
725
- r.append(c)
726
- i += 1
727
- return i,"".join(r)
728
-
729
-
730
- def read_cond(klong, t, i=0):
731
- """
732
-
733
- # A conditional expression has two forms: :[e1;e2;e3] means "if
734
- # e1 is true, evaluate to e2, else evaluate to e3".
735
- # :[e1;e2:|e3;e4;e5] is short for :[e1;e2:[e3;e4;e5]], i.e. the
736
- # ":|" acts as an "else-if" operator. There may be any number of
737
- # ":|" operators in a conditional.
738
-
739
- c := ':[' ( e ';' e ':|' )* e ';' e ';' e ']'
740
-
741
- """
742
- r = []
743
- i,n = klong._expr(t, i, ignore_newline=True)
744
- r.append(n)
745
- i = cexpect(t, i, ';')
746
- i,n = klong._expr(t, i, ignore_newline=True)
747
- r.append(n)
748
- i = skip(t,i,ignore_newline=True)
749
- if cmatch2(t,i,':','|'):
750
- i,n = read_cond(klong,t,i+2)
751
- r.append(n)
752
- else:
753
- i = cexpect(t, i, ';')
754
- i,n = klong._expr(t, i, ignore_newline=True)
755
- r.append(n)
756
- i = skip(t,i,ignore_newline=True)
757
- i = cexpect(t, i, ']')
758
- return i, KGCond(r)
759
-
760
-
761
- def list_to_dict(a):
762
- return {x[0]:x[1] for x in a}
763
-
764
-
765
- copy_lambda = KGLambda(lambda x: copy.deepcopy(x))
766
-
767
- def kg_read(t, i=0, read_neg=False, ignore_newline=False, module=None, list_level=0):
768
- """
769
-
770
- # Lexeme classes are the sets of the lexemes specified in the
771
- # previous section, except for operators.
772
-
773
- C := I # integer
774
- | H # character
775
- | R # real number
776
- | S # string
777
- | V # variable (symbol)
778
- | Y # (quoted) symbol
779
-
780
- NOTE: this function mirrors the klong implementation so that sys_read/write
781
- match klong's as well. The grammar read here is a superset of C.
782
-
783
- NOTE: a newline character translates to a semicolon in Klong,
784
- except in functions, dictionaries, conditional expressions,
785
- and lists. So
786
-
787
- a()
788
- b()
789
-
790
- is equal to a();b(), but
791
-
792
- [1
793
- 2
794
- 3]
795
-
796
- is equal to [1 2 3] and
797
-
798
- :[x;
799
- y;
800
- z]
801
-
802
- is equal to :[x;y;z] and
803
-
804
- f::{.d("hello ");
805
- .p("world!");
806
- []}
807
-
808
- is a valid function definition.
809
-
810
- """
811
- i = skip(t, i, ignore_newline=ignore_newline)
812
- if i >= len(t):
813
- return i, None
814
- a = t[i]
815
- if a == '\n':
816
- a = ';' # convert newlines to semicolons
817
- if a in [';','(',')','{','}',']']:
818
- return i+1,a
819
- elif cmatch2(t, i, '0', 'c'):
820
- return read_char(t, i)
821
- elif a.isnumeric() or (read_neg and (a == '-' and (i+1) < len(t) and t[i+1].isnumeric())):
822
- return read_num(t, i)
823
- elif a == '"':
824
- return read_string(t, i+1)
825
- elif a == ':' and (i+1 < len(t)):
826
- aa = t[i+1]
827
- if aa.isalpha() or aa == '.':
828
- return read_sym(t, i=i+1, module=module)
829
- elif aa.isnumeric() or aa == '"':
830
- return kg_read(t, i+1, ignore_newline=ignore_newline, module=module)
831
- elif aa == '{':
832
- i, d = read_list(t, '}', i=i+2, module=module, level=list_level+1)
833
- d = list_to_dict(d)
834
- return i, KGCall(copy_lambda,args=d,arity=0)
835
- elif aa == '[':
836
- return i+2,':['
837
- elif aa == '|':
838
- return i+2,':|'
839
- return i+2,KGOp(f":{aa}",arity=0)
840
- elif safe_eq(a, '['):
841
- return read_list(t, ']', i=i+1, module=module, level=list_level+1)
842
- elif is_symbolic(a):
843
- return read_sym(t, i, module=module)
844
- return read_op(t,i)
845
-
846
-
847
- def kg_write_symbol(x, display=False):
848
- return str(x) if display else f":{x}"
849
-
850
-
851
- def kg_write_integer(x, display=False):
852
- return str(int(x))
853
-
854
-
855
- def kg_write_float(x, display=False):
856
- return str(x)
857
-
858
-
859
- def kg_write_char(c, display=False):
860
- return c if display else f"0c{c}"
861
-
862
-
863
- def kg_write_string(s, display=False):
864
- if display:
865
- return s
866
- arr = ['"']
867
- for c in s:
868
- if c == '"':
869
- arr.append('"')
870
- arr.append(c)
871
- arr.append('"')
872
- return ''.join(arr)
873
-
874
-
875
- def kg_write_dict(d, backend, display=False):
876
- # determine if the object d has overwritten the default __str__ and call it
877
- # if so, otherwise use the default dict str
878
- if d.__class__.__name__ != 'dict':
879
- return str(d)
880
- return ''.join([':{', ' '.join([kg_write(list(e), backend, display=display) for e in d.items()]), '}'])
881
-
882
-
883
- def kg_write_list(x, backend, display=False):
884
- return ''.join(['[', ' '.join([kg_write(q, backend, display=display) for q in x]), ']'])
885
-
886
-
887
- def kg_write_fn(x, display=False):
888
- return str(x)
889
-
890
-
891
- def kg_write_channel(x, display=False):
892
- if x.channel_dir == KGChannelDir.INPUT:
893
- return ":inchan.0"
894
- return f":outchan.{2 if x.raw == sys.stderr else 1}"
895
-
896
-
897
- def kg_write(a, backend, display=False):
898
- _backend = backend
899
- # Convert backend arrays (e.g., torch tensors) to display-friendly format
900
- a = _backend.to_display(a)
901
- if isinstance(a,KGSym):
902
- return kg_write_symbol(a, display=display)
903
- elif is_integer(a, _backend):
904
- return kg_write_integer(a,display=display)
905
- elif is_float(a, _backend):
906
- return kg_write_float(a,display=display)
907
- elif isinstance(a,KGChar):
908
- return kg_write_char(a,display=display)
909
- elif isinstance(a, str):
910
- return kg_write_string(a,display=display)
911
- elif isinstance(a,dict):
912
- return kg_write_dict(a, _backend, display=display)
913
- elif is_list(a):
914
- return kg_write_list(a, _backend, display=display)
915
- elif isinstance(a,KGFn):
916
- return kg_write_fn(a,display=display)
917
- elif isinstance(a,KGChannel):
918
- return kg_write_channel(a,display=display)
919
- elif hasattr(a,'__str__'):
920
- return str(a)
921
- elif safe_eq(a, np.inf):
922
- return ":undefined"
923
-
924
-
925
- def kg_argsort(a, backend, descending=False):
926
- """
927
-
928
- Return the indices of the sorted array (may be nested) or a string. Duplicate elements are disambiguated by their position in the array.
929
-
930
- argsort("foobar") => [4 3 0 1 2 5]
931
- ^ ^
932
- arbitrary ordering resolved by index position
933
-
934
- argsort("foobar",descending=True) => [5 2 1 0 3 4]
935
- ^ ^
936
- arbitrary ordering resolved by index position
937
-
938
- """
939
- if not is_iterable(a) or len(a) == 0:
940
- return a
941
-
942
- # Fast path: for simple 1D numeric arrays, use native argsort
943
- if hasattr(a, 'ndim') and a.ndim == 1:
944
- dtype_kind = get_dtype_kind(a, backend)
945
- if dtype_kind in ('i', 'f', 'u'):
946
- return backend.argsort(a, descending=descending)
947
-
948
- # Slow path: nested arrays or strings need element-by-element comparison
949
- def _e(x):
950
- return (-np.inf,x) if is_empty(a[x]) else (np.max(a[x]),x) if is_list(a[x]) else (a[x],x)
951
- return np.asarray(sorted(range(len(a)), key=_e, reverse=descending))
952
-
953
-
954
- def peek_adverb(t,i=0):
955
- x = cpeek2(t,i)
956
- if is_adverb(x):
957
- return i+2,x
958
- x = cpeek(t,i)
959
- if is_adverb(x):
960
- return i+1,x
961
- return i,None
962
-
963
-
964
- def is_adverb(s):
965
- return s in {
966
- "'",
967
- ':\\',
968
- ":'",
969
- ':/',
970
- '/',
971
- ':~',
972
- ':*',
973
- '\\',
974
- '\\~',
975
- '\\*'
976
- }
977
-
978
-
979
- def get_adverb_arity(s, ctx):
980
- if s == "'":
981
- return ctx
982
- elif s == ':\\':
983
- return 2
984
- elif s == ':\'':
985
- return 2
986
- elif s == ':/':
987
- return 2
988
- elif s == '/':
989
- return 2
990
- elif s == ':~':
991
- return 1
992
- elif s == ':*':
993
- return 1
994
- elif s == '\\':
995
- return 2
996
- elif s == '\\~':
997
- return 1
998
- elif s == '\\*':
999
- return 1
1000
- raise RuntimeError(f"unknown adverb: {s}")
1001
-
1002
-
1003
- def merge_projections(arr):
1004
- """
1005
-
1006
- A projection is a new function that is created by projecting an
1007
- existing function onto at least one of its arguments, resulting
1008
- in the partial application of the original function.
1009
-
1010
- The notation of projection is that of function application where
1011
- the arguments onto which the function is being projected are
1012
- omitted. For instance,
1013
-
1014
- Projection Equivalent function
1015
- {x-y}(5;) {5-x}
1016
- {x-y}(;5) {x-5}
1017
-
1018
- and, given a ternary function f3:
1019
-
1020
- Projection Equivalent function
1021
- f3(1;2;) {f3(1;2;x)}
1022
- f3(1;;3) {f3(1;x;3)}
1023
- f3(;2;3) {f3(x;2;3)}
1024
- f3(;;3) {f3(x;y;3)}
1025
- f3(;2;) {f3(x;2;y)}
1026
- f3(1;;) {f3(1;x;y)}
1027
-
1028
- The projection of a triad is a dyad or a monad, depending on the
1029
- number of arguments onto which the triad is being projected. The
1030
- projection of a dyad is always a monad. There is no projection
1031
- of a monad or nilad.
1032
-
1033
- Alternatively, monads and nilads can be considered to be their
1034
- own projections (onto zero arguments), but there is no special
1035
- syntax for this case. Any function that is being projected onto
1036
- all of its arguments is simply the function itself.
1037
-
1038
- Projections are ordinary functions and can be used in all places
1039
- where a verb is expected. For instance:
1040
-
1041
- f::{x,y}
1042
- f(;0)'[1 2 3] --> [[1 0] [2 0] [3 0]]
1043
- f(0;)'[1 2 3] --> [[0 1] [0 2] [0 3]]
1044
-
1045
- g::{x,y,z}
1046
- 1g(;2;)3 --> [1 2 3]
1047
-
1048
- """
1049
- if len(arr) == 0:
1050
- return arr
1051
- if len(arr) == 1 or not has_none(arr[0]):
1052
- return arr[0]
1053
- sparse_fa = np.copy(arr[0])
1054
- i = 0
1055
- k = 1
1056
- while i < len(sparse_fa) and k < len(arr):
1057
- fa = arr[k]
1058
- j = 0
1059
- while i < len(sparse_fa) and j < len(fa):
1060
- if sparse_fa[i] is None:
1061
- sparse_fa[i] = fa[j]
1062
- j += 1
1063
- while j < len(fa) and safe_eq(fa[j], None):
1064
- j += 1
1065
- i += 1
1066
- k += 1
1067
- return sparse_fa
1068
-
1069
-
1070
- def get_fn_arity(f):
1071
- """
1072
- Examine a function AST and infer arity by looking for x,y and z.
1073
- This arity is needed to populate the KGFn.
1074
-
1075
- NOTE: TODO: it maybe easier / better to do this at parse time vs late.
1076
- """
1077
- if isinstance(f,KGFn) and isinstance(f.a,KGSym) and not in_map(f.a,reserved_fn_symbols):
1078
- return sum(1 for x in set(f.args) if in_map(x, reserved_fn_symbols) or (x is None))
1079
- def _e(f, level=0):
1080
- if isinstance(f,KGFn):
1081
- x = _e(f.a, level=1)
1082
- if isinstance(f.args,list):
1083
- for q in f.args:
1084
- x.update(_e(q,level=1))
1085
- elif isinstance(f,list):
1086
- x = set()
1087
- for q in f:
1088
- x.update(_e(q,level=1))
1089
- elif isinstance(f,KGSym):
1090
- x = set([f]) if f in reserved_fn_symbols else set()
1091
- else:
1092
- x = set()
1093
- return x if level else len(x)
1094
- return _e(f)
1
+ """
2
+ KlongPy core module.
3
+
4
+ This module re-exports public symbols from the split modules.
5
+ New code should import directly from the specific modules:
6
+ - klongpy.types: Type definitions and type checking
7
+ - klongpy.parser: Parsing and lexing functions
8
+ - klongpy.writer: Output formatting functions
9
+ """
10
+
11
+ # Re-export everything from the split modules
12
+ from .types import *
13
+ from .parser import *
14
+ from .writer import *
15
+
16
+ # Re-export backend numpy-like module for shared array helpers
17
+ from .backend import bknp
18
+
19
+ __all__ = [
20
+ # Types
21
+ 'KlongException',
22
+ 'KGSym',
23
+ 'KGFn',
24
+ 'KGFnWrapper',
25
+ 'KGCall',
26
+ 'KGOp',
27
+ 'KGAdverb',
28
+ 'KGChar',
29
+ 'KGCond',
30
+ 'KGUndefined',
31
+ 'KLONG_UNDEFINED',
32
+ 'KGLambda',
33
+ 'KGChannel',
34
+ 'KGChannelDir',
35
+ 'RangeError',
36
+ # Type constants
37
+ 'reserved_fn_args',
38
+ 'reserved_fn_symbols',
39
+ 'reserved_fn_symbol_map',
40
+ 'reserved_dot_f_symbol',
41
+ # Type helpers
42
+ 'get_fn_arity_str',
43
+ 'safe_inspect',
44
+ # Type checking
45
+ 'is_list',
46
+ 'is_iterable',
47
+ 'is_empty',
48
+ 'is_dict',
49
+ 'to_list',
50
+ 'is_integer',
51
+ 'is_float',
52
+ 'is_number',
53
+ 'str_is_float',
54
+ 'is_symbolic',
55
+ 'is_char',
56
+ 'is_atom',
57
+ 'kg_truth',
58
+ 'str_to_chr_arr',
59
+ 'get_dtype_kind',
60
+ # Utilities
61
+ 'safe_eq',
62
+ 'in_map',
63
+ 'has_none',
64
+ 'rec_flatten',
65
+ # Adverb utilities
66
+ 'is_adverb',
67
+ 'get_adverb_arity',
68
+ # Function utilities
69
+ 'merge_projections',
70
+ 'get_fn_arity',
71
+ # Parser - character matching
72
+ 'cmatch',
73
+ 'cmatch2',
74
+ 'cpeek',
75
+ 'cpeek2',
76
+ 'cexpect',
77
+ 'cexpect2',
78
+ 'UnexpectedChar',
79
+ 'UnexpectedEOF',
80
+ # Parser - skip
81
+ 'skip_space',
82
+ 'skip',
83
+ # Parser - comment
84
+ 'read_shifted_comment',
85
+ 'read_sys_comment',
86
+ # Parser - lexeme readers
87
+ 'read_num',
88
+ 'read_char',
89
+ 'read_sym',
90
+ 'read_op',
91
+ 'read_string',
92
+ 'read_list',
93
+ 'kg_read',
94
+ 'kg_read_array',
95
+ 'read_cond',
96
+ 'list_to_dict',
97
+ 'copy_lambda',
98
+ 'peek_adverb',
99
+ # Writer
100
+ 'kg_write_symbol',
101
+ 'kg_write_integer',
102
+ 'kg_write_float',
103
+ 'kg_write_char',
104
+ 'kg_write_string',
105
+ 'kg_write_dict',
106
+ 'kg_write_list',
107
+ 'kg_write_fn',
108
+ 'kg_write_channel',
109
+ 'kg_write',
110
+ 'kg_argsort',
111
+ # Backend re-exports
112
+ 'bknp',
113
+ ]