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/parser.py ADDED
@@ -0,0 +1,328 @@
1
+ """
2
+ KlongPy parser and lexer functions.
3
+
4
+ This module contains all the parsing functions for the Klong language:
5
+ - Lexeme reading (numbers, strings, symbols, operators)
6
+ - List parsing
7
+ - Conditional expression parsing
8
+ - Comment handling
9
+ """
10
+ import copy
11
+
12
+ from .types import (
13
+ KGSym, KGChar, KGOp, KGCond, KGCall, KGLambda,
14
+ reserved_fn_symbol_map,
15
+ safe_eq, is_symbolic, is_adverb
16
+ )
17
+
18
+
19
+ # Character matching utilities
20
+
21
+ def cmatch(t, i, c):
22
+ return i < len(t) and t[i] == c
23
+
24
+
25
+ def cmatch2(t, i, a, b):
26
+ return cmatch(t, i, a) and cmatch(t, i+1, b)
27
+
28
+
29
+ def cpeek(t, i):
30
+ return t[i] if i < len(t) else None
31
+
32
+
33
+ def cpeek2(t, i):
34
+ return t[i:i+2] if i < (len(t)-1) else None
35
+
36
+
37
+ class UnexpectedChar(Exception):
38
+ def __init__(self, t, i, c):
39
+ super().__init__(f"t: {t[i-10:i+10]} pos: {i} char: {c}")
40
+
41
+
42
+ class UnexpectedEOF(Exception):
43
+ def __init__(self, t, i):
44
+ self.t = t
45
+ self.i = i
46
+ super().__init__(f"t: {t[i-10:]} pos: {i}")
47
+
48
+
49
+ def cexpect(t, i, c):
50
+ if cmatch(t, i, c):
51
+ return i + 1
52
+ raise UnexpectedChar(t, i, c)
53
+
54
+
55
+ def cexpect2(t, i, a, b):
56
+ if cmatch(t, i, a) and cmatch(t, i+1, b):
57
+ return i + 2
58
+ raise UnexpectedChar(t, i, b)
59
+
60
+
61
+ # Comment handling
62
+
63
+ def read_shifted_comment(t, i=0):
64
+ while i < len(t):
65
+ c = t[i]
66
+ if c == '"':
67
+ i += 1
68
+ if not cmatch(t, i, '"'):
69
+ break
70
+ i += 1
71
+ return i
72
+
73
+
74
+ def read_sys_comment(t, i, a):
75
+ """
76
+ .comment(x) [Comment]
77
+
78
+ Read and discard lines until the current line starts with the
79
+ string specified in "x". Also discard the line containing the
80
+ end-of-comment marker and return "x".
81
+
82
+ Example: .comment("end-of-comment")
83
+ this will be ignored
84
+ this, too: *%(*^#)&(#
85
+ end-of-comment
86
+
87
+ NOTE: this is handled in the parsing phase and is not a runtime function
88
+ """
89
+ try:
90
+ j = t[i:].index(a)
91
+ while t[i+j+1:].startswith(a):
92
+ j += 1
93
+ return i + j + len(a)
94
+ except ValueError:
95
+ return RuntimeError("end of comment not found")
96
+
97
+
98
+ # Whitespace handling
99
+
100
+ def skip_space(t, i=0, ignore_newline=False):
101
+ """
102
+ NOTE: a newline character translates to a semicolon in Klong,
103
+ except in functions, dictionaries, conditional expressions,
104
+ and lists. So
105
+ """
106
+ while i < len(t) and (t[i].isspace() and (ignore_newline or t[i] != '\n')):
107
+ i += 1
108
+ return i
109
+
110
+
111
+ def skip(t, i=0, ignore_newline=False):
112
+ i = skip_space(t, i, ignore_newline=ignore_newline)
113
+ if cmatch2(t, i, ':', '"'):
114
+ i = read_shifted_comment(t, i+2)
115
+ i = skip(t, i)
116
+ return i
117
+
118
+
119
+ # Lexeme readers
120
+
121
+ def read_num(t, i=0):
122
+ p = i
123
+ use_float = False
124
+ if t[i] == '-':
125
+ i += 1
126
+ while i < len(t):
127
+ if t[i] == '.':
128
+ use_float = True
129
+ elif t[i] == 'e':
130
+ use_float = True
131
+ if cmatch(t, i+1, '-'):
132
+ i += 2
133
+ elif not t[i].isnumeric():
134
+ break
135
+ i += 1
136
+ return i, float(t[p:i]) if use_float else int(t[p:i])
137
+
138
+
139
+ def read_char(t, i):
140
+ i = cexpect2(t, i, '0', 'c')
141
+ if i >= len(t):
142
+ raise UnexpectedEOF(t, i)
143
+ return i+1, KGChar(t[i])
144
+
145
+
146
+ def read_sym(t, i=0, module=None):
147
+ p = i
148
+ while i < len(t) and is_symbolic(t[i]):
149
+ i += 1
150
+ x = t[p:i]
151
+ return i, reserved_fn_symbol_map.get(x) or KGSym(x if x.startswith('.') or module is None else f"{x}`{module}")
152
+
153
+
154
+ def read_op(t, i=0):
155
+ if cmatch2(t, i, '\\', '~') or cmatch2(t, i, '\\', '*'):
156
+ return i+2, KGOp(t[i:i+2], arity=0)
157
+ return i+1, KGOp(t[i:i+1], arity=0)
158
+
159
+
160
+ def read_string(t, i=0):
161
+ """
162
+ ".*" [String]
163
+
164
+ A string is (almost) any sequence of characters enclosed by
165
+ double quote characters. To include a double quote character in
166
+ a string, it has to be duplicated, so the above regex is not
167
+ entirely correct. A comment is a shifted string (see below).
168
+ Examples: ""
169
+ "hello, world"
170
+ "say ""hello""!"
171
+
172
+ Note: this comforms to the KG read_string impl.
173
+ perf tests show that the final join is fast for short strings
174
+ """
175
+ r = []
176
+ while i < len(t):
177
+ c = t[i]
178
+ if c == '"':
179
+ i += 1
180
+ if not cmatch(t, i, '"'):
181
+ break
182
+ r.append(c)
183
+ i += 1
184
+ return i, "".join(r)
185
+
186
+
187
+ # Dictionary helper
188
+
189
+ def list_to_dict(a):
190
+ return {x[0]:x[1] for x in a}
191
+
192
+
193
+ # Lambda for copy operations (used in dict parsing)
194
+ copy_lambda = KGLambda(lambda x: copy.deepcopy(x))
195
+
196
+
197
+ def read_list(t, delim, i=0, module=None):
198
+ """
199
+ Parse a list from string t starting at position i.
200
+ Returns a Python list (caller converts to array if needed).
201
+
202
+ L := '[' (C|L)* ']'
203
+ """
204
+ arr = []
205
+ i = skip(t, i, ignore_newline=True)
206
+ while not cmatch(t, i, delim) and i < len(t):
207
+ i, q = kg_read(t, i, read_neg=True, ignore_newline=True, module=module)
208
+ if q is None:
209
+ break
210
+ if safe_eq(q, '['):
211
+ i, q = read_list(t, ']', i=i, module=module)
212
+ arr.append(q)
213
+ i = skip(t, i, ignore_newline=True)
214
+ if cmatch(t, i, delim):
215
+ i += 1
216
+ return i, arr
217
+
218
+
219
+ def kg_read(t, i, read_neg=False, ignore_newline=False, module=None):
220
+ """
221
+ Read a Klong lexeme from string t starting at position i.
222
+
223
+ C := I | H | R | S | V | Y
224
+ """
225
+ i = skip(t, i, ignore_newline=ignore_newline)
226
+ if i >= len(t):
227
+ return i, None
228
+ a = t[i]
229
+ if a == '\n':
230
+ a = ';'
231
+ if a in [';', '(', ')', '{', '}', ']']:
232
+ return i+1, a
233
+ elif cmatch2(t, i, '0', 'c'):
234
+ return read_char(t, i)
235
+ elif a.isnumeric() or (read_neg and (a == '-' and (i+1) < len(t) and t[i+1].isnumeric())):
236
+ return read_num(t, i)
237
+ elif a == '"':
238
+ return read_string(t, i+1)
239
+ elif a == ':' and (i+1 < len(t)):
240
+ aa = t[i+1]
241
+ if aa.isalpha() or aa == '.':
242
+ return read_sym(t, i=i+1, module=module)
243
+ elif aa.isnumeric() or aa == '"':
244
+ return kg_read(t, i+1, ignore_newline=ignore_newline, module=module)
245
+ elif aa == '{':
246
+ i, d = read_list(t, '}', i=i+2, module=module)
247
+ d = list_to_dict(d)
248
+ return i, KGCall(copy_lambda, args=d, arity=0)
249
+ elif aa == '[':
250
+ return i+2, ':['
251
+ elif aa == '|':
252
+ return i+2, ':|'
253
+ return i+2, KGOp(f":{aa}", arity=0)
254
+ elif safe_eq(a, '['):
255
+ return read_list(t, ']', i=i+1, module=module)
256
+ elif is_symbolic(a):
257
+ return read_sym(t, i, module=module)
258
+ return read_op(t, i)
259
+
260
+
261
+ def kg_read_array(t, i, backend, **kwargs):
262
+ """
263
+ Read a value and convert lists to arrays using the provided backend.
264
+
265
+ This is a helper that wraps kg_read and handles list-to-array conversion,
266
+ centralizing the pattern used by the interpreter and eval_sys.
267
+
268
+ Parameters
269
+ ----------
270
+ t : str
271
+ The string to read from.
272
+ i : int
273
+ Starting position in the string.
274
+ backend : BackendProvider
275
+ The backend to use for array conversion.
276
+ **kwargs
277
+ Additional arguments passed to kg_read (read_neg, ignore_newline, module).
278
+
279
+ Returns
280
+ -------
281
+ tuple
282
+ (new_position, value) where value is converted to an array if it was a list.
283
+ """
284
+ i, a = kg_read(t, i, **kwargs)
285
+ if isinstance(a, list):
286
+ a = backend.kg_asarray(a)
287
+ return i, a
288
+
289
+
290
+ def read_cond(klong, t, i=0):
291
+ """
292
+ # A conditional expression has two forms: :[e1;e2;e3] means "if
293
+ # e1 is true, evaluate to e2, else evaluate to e3".
294
+ # :[e1;e2:|e3;e4;e5] is short for :[e1;e2:[e3;e4;e5]], i.e. the
295
+ # ":|" acts as an "else-if" operator. There may be any number of
296
+ # ":|" operators in a conditional.
297
+
298
+ c := ':[' ( e ';' e ':|' )* e ';' e ';' e ']'
299
+ """
300
+ r = []
301
+ i, n = klong._expr(t, i, ignore_newline=True)
302
+ r.append(n)
303
+ i = cexpect(t, i, ';')
304
+ i, n = klong._expr(t, i, ignore_newline=True)
305
+ r.append(n)
306
+ i = skip(t, i, ignore_newline=True)
307
+ if cmatch2(t, i, ':', '|'):
308
+ i, n = read_cond(klong, t, i+2)
309
+ r.append(n)
310
+ else:
311
+ i = cexpect(t, i, ';')
312
+ i, n = klong._expr(t, i, ignore_newline=True)
313
+ r.append(n)
314
+ i = skip(t, i, ignore_newline=True)
315
+ i = cexpect(t, i, ']')
316
+ return i, KGCond(r)
317
+
318
+
319
+ # Adverb peeking
320
+
321
+ def peek_adverb(t, i=0):
322
+ x = cpeek2(t, i)
323
+ if is_adverb(x):
324
+ return i+2, x
325
+ x = cpeek(t, i)
326
+ if is_adverb(x):
327
+ return i+1, x
328
+ return i, None
klongpy/repl.py CHANGED
@@ -72,13 +72,13 @@ def append_pkg_resource_path_KLONGPATH() -> None:
72
72
  os.environ['KLONGPATH'] = klongpath
73
73
 
74
74
 
75
- def create_repl(debug: bool = False):
75
+ def create_repl(debug: bool = False, backend: Optional[str] = None, device: Optional[str] = None):
76
76
  io_loop, io_thread, io_stop = setup_async_loop(debug=debug)
77
77
  klong_loop, klong_thread, klong_stop = setup_async_loop(debug=debug)
78
78
 
79
79
  append_pkg_resource_path_KLONGPATH()
80
80
 
81
- klong = KlongInterpreter()
81
+ klong = KlongInterpreter(backend=backend, device=device)
82
82
  shutdown_event = CallbackEvent()
83
83
  klong['.system'] = {'ioloop': io_loop, 'klongloop': klong_loop, 'closeEvent': shutdown_event}
84
84
 
klongpy/sys_fn.py CHANGED
@@ -12,23 +12,21 @@ 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_read, kg_write, np,
15
+ bknp, is_dict, is_empty, is_list, kg_read_array, kg_write,
16
16
  reserved_fn_args, reserved_fn_symbol_map, safe_eq, safe_inspect)
17
- from .backend import to_numpy, get_default_backend, kg_asarray
18
17
 
19
18
 
20
- def _to_display_value(x):
19
+ def _to_display_value(x, backend):
21
20
  """Convert backend tensors to numpy for cleaner display."""
22
- backend = get_default_backend()
23
21
  # Convert backend arrays (tensors) to numpy
24
22
  if backend.is_backend_array(x):
25
- return to_numpy(x)
23
+ return backend.to_numpy(x)
26
24
  # Handle numpy arrays with tensors inside (object arrays)
27
25
  if isinstance(x, numpy.ndarray) and x.dtype == object:
28
- return numpy.array([_to_display_value(item) for item in x], dtype=object)
26
+ return numpy.array([_to_display_value(item, backend) for item in x], dtype=object)
29
27
  # Handle lists with tensors
30
28
  if isinstance(x, list):
31
- return [_to_display_value(item) for item in x]
29
+ return [_to_display_value(item, backend) for item in x]
32
30
  return x
33
31
 
34
32
 
@@ -63,11 +61,11 @@ def eval_sys_display(klong, x):
63
61
 
64
62
  .d(x) [Display]
65
63
 
66
- Display the object "x". Tensors are converted to numpy for cleaner output.
64
+ Display the object "x". Backend arrays are converted to numpy for cleaner output.
67
65
  Use .bkd() for raw backend-specific display.
68
66
 
69
67
  """
70
- x = _to_display_value(x)
68
+ x = _to_display_value(x, klong._backend)
71
69
  r = kg_write(x, klong._backend, display=True)
72
70
  klong['.sys.cout'].raw.write(r)
73
71
  return r
@@ -303,11 +301,11 @@ def eval_sys_print(klong, x):
303
301
  .p(x) [Print]
304
302
 
305
303
  Pretty-print the object "x" (like Display) and then print a
306
- newline sequence. Tensors are converted to numpy for cleaner output.
304
+ newline sequence. Backend arrays are converted to numpy for cleaner output.
307
305
  Use .bkp() for raw backend-specific print.
308
306
 
309
307
  """
310
- x = _to_display_value(x)
308
+ x = _to_display_value(x, klong._backend)
311
309
  o = kg_write(x, klong._backend, display=True)
312
310
  klong['.sys.cout'].raw.write(o+"\n")
313
311
  return o
@@ -697,7 +695,7 @@ def eval_sys_random_number():
697
695
  Return a random number x, such that 0 <= x < 1.
698
696
 
699
697
  """
700
- return np.random.random()
698
+ return bknp.random.random()
701
699
 
702
700
 
703
701
  def eval_sys_read(klong):
@@ -718,7 +716,7 @@ def eval_sys_read(klong):
718
716
  f.at_eof = True
719
717
  return None
720
718
  else:
721
- i,a = kg_read(r,i=0,module=klong.current_module())
719
+ i,a = kg_read_array(r, 0, klong._backend, module=klong.current_module())
722
720
  f.raw.seek(k+i,0)
723
721
  return a
724
722
 
@@ -753,7 +751,7 @@ def eval_sys_read_lines(klong):
753
751
  f = klong['.sys.cin']
754
752
  r = f.raw.readlines()
755
753
  f.at_eof = True
756
- return kg_asarray(r)
754
+ return klong._backend.kg_asarray(r)
757
755
 
758
756
 
759
757
  def eval_sys_read_string(klong, x):
@@ -767,7 +765,8 @@ def eval_sys_read_string(klong, x):
767
765
  forms.
768
766
 
769
767
  """
770
- return kg_read(x, i=0, module=klong.current_module(), read_neg=True)[1]
768
+ _, a = kg_read_array(x, 0, klong._backend, module=klong.current_module(), read_neg=True)
769
+ return a
771
770
 
772
771
 
773
772
  def eval_sys_system(x):
@@ -853,6 +852,45 @@ def eval_sys_exit(x):
853
852
  sys.exit(1)
854
853
 
855
854
 
855
+ def eval_sys_strict(klong):
856
+ """
857
+
858
+ .strict() [Strict]
859
+
860
+ Enable strict mode for variable assignment (level 1).
861
+ In strict mode, functions cannot create new global variables
862
+ unless they are explicitly declared with the global: prefix.
863
+
864
+ Example:
865
+ .strict()
866
+ counter::0
867
+ increment::{counter::counter+1} :" ERROR"
868
+ increment::{[global:counter];counter::counter+1} :" OK"
869
+
870
+ """
871
+ klong._context._strict_mode = 1
872
+ return 1
873
+
874
+
875
+ def eval_sys_unsafe(klong):
876
+ """
877
+
878
+ .unsafe() [Unsafe]
879
+
880
+ Disable strict mode for variable assignment (level 0).
881
+ In unsafe mode, functions can create new global variables
882
+ without declaration (legacy behavior).
883
+
884
+ Example:
885
+ .unsafe()
886
+ f::{newvar::42} :" OK - creates global newvar"
887
+ f()
888
+
889
+ """
890
+ klong._context._strict_mode = 0
891
+ return 0
892
+
893
+
856
894
  def create_system_functions():
857
895
  def _get_name(s):
858
896
  i = s.index('.')
klongpy/sys_fn_ipc.py CHANGED
@@ -8,10 +8,8 @@ import uuid
8
8
  from asyncio import StreamReader, StreamWriter
9
9
  from asyncio.exceptions import IncompleteReadError
10
10
 
11
- import numpy as np
12
-
13
11
  from klongpy.core import (KGCall, KGFn, KGFnWrapper, KGLambda, KGSym,
14
- KlongException, get_fn_arity_str, is_list,
12
+ KlongException, KLONG_UNDEFINED, get_fn_arity_str, is_list,
15
13
  reserved_fn_args, reserved_fn_symbols, reserved_fn_symbol_map)
16
14
 
17
15
 
@@ -1011,12 +1009,9 @@ def create_system_functions_ipc():
1011
1009
 
1012
1010
  def create_system_var_ipc():
1013
1011
  # populate the .srv.* handlers with undefined values
1014
- # TODO: use real undefined value instead of np.inf
1015
1012
  registry = {
1016
- ".srv.o": np.inf,
1017
- ".srv.c": np.inf,
1018
- ".srv.e": np.inf,
1013
+ ".srv.o": KLONG_UNDEFINED,
1014
+ ".srv.c": KLONG_UNDEFINED,
1015
+ ".srv.e": KLONG_UNDEFINED,
1019
1016
  }
1020
1017
  return registry
1021
-
1022
-