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