solpl 2.6.6__tar.gz → 2.7.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: solpl
3
- Version: 2.6.6
3
+ Version: 2.7.0
4
4
  Summary: A lightweight, multi-threaded interpreter for the Sol programming language
5
5
  Author-email: stefand-0 <stefand-0@users.noreply.github.com>
6
6
  License-Expression: Apache-2.0
@@ -82,6 +82,11 @@ main()
82
82
  end
83
83
  ```
84
84
 
85
+ > [!IMPORTANT]
86
+ > Asynchronous functions cannot use the _return method to return a value. Please use _out instead.
87
+
88
+ > [!TIP]
89
+ > Make use of the standard libraries for extra data types, such as Booleans, HashMaps etc.
85
90
  # License
86
91
 
87
92
  This repository is licensed with Apache License 2.0, see the LICENSE file for more details
@@ -69,6 +69,11 @@ main()
69
69
  end
70
70
  ```
71
71
 
72
+ > [!IMPORTANT]
73
+ > Asynchronous functions cannot use the _return method to return a value. Please use _out instead.
74
+
75
+ > [!TIP]
76
+ > Make use of the standard libraries for extra data types, such as Booleans, HashMaps etc.
72
77
  # License
73
78
 
74
79
  This repository is licensed with Apache License 2.0, see the LICENSE file for more details
@@ -14,10 +14,10 @@ class SolInterpreter:
14
14
  self.variables = {}
15
15
  self.thread_local = threading.local()
16
16
  self.functions = {}
17
- self.struct_blueprints = {}
17
+ self.struct_blueprints = {}
18
18
  self.lines = []
19
19
  self.threads = []
20
- self._server_started = False
20
+ self._server_started = False
21
21
 
22
22
  @property
23
23
  def scope_stack(self):
@@ -25,83 +25,156 @@ class SolInterpreter:
25
25
  self.thread_local.stack = [self.variables]
26
26
  return self.thread_local.stack
27
27
 
28
- def _import_file(self, filename):
29
- if not os.path.exists(filename):
30
- print(f"Error: Module '{filename}' not found.")
31
- return
28
+ def _has_guard(self, line):
29
+ in_quote = False
30
+ quote_char = None
31
+ for char in line:
32
+ if char in ['"', "'"]:
33
+ if not in_quote:
34
+ in_quote = True
35
+ quote_char = char
36
+ elif char == quote_char:
37
+ in_quote = False
38
+ quote_char = None
39
+ elif not in_quote and char == '?':
40
+ return True
41
+ return False
32
42
 
33
- with open(filename, "r") as f:
34
- code = f.read()
35
- module_name = os.path.splitext(os.path.basename(filename))[0]
43
+ def _split_on_op(self, string, op):
44
+ """Split on first occurrence of op outside quotes. Returns (left, right) or (None, None)."""
45
+ in_quote = False
46
+ quote_char = None
47
+ for i, char in enumerate(string):
48
+ if char in ['"', "'"]:
49
+ if not in_quote:
50
+ in_quote = True
51
+ quote_char = char
52
+ elif char == quote_char:
53
+ in_quote = False
54
+ quote_char = None
55
+ elif not in_quote and char == op:
56
+ return string[:i], string[i+1:]
57
+ return None, None
36
58
 
37
- lines = []
38
- for l in code.split('\n'):
39
- l = l.strip()
40
- if not l: continue
41
- if ';' in l:
42
- l = l.split(';', 1)[0].strip()
43
- if l:
44
- lines.append(l)
59
+ def _split_outside_quotes(self, string, op):
60
+ parts = []
61
+ current = []
62
+ in_quote = False
63
+ quote_char = None
64
+ for char in string:
65
+ if char in ['"', "'"]:
66
+ if not in_quote:
67
+ in_quote = True
68
+ quote_char = char
69
+ elif char == quote_char:
70
+ in_quote = False
71
+ quote_char = None
72
+ current.append(char)
73
+ elif not in_quote and char == op:
74
+ parts.append("".join(current))
75
+ current = []
76
+ else:
77
+ current.append(char)
78
+ parts.append("".join(current))
79
+ return parts
45
80
 
46
- for pc, line in enumerate(lines):
47
- if "(" in line and ")" in line and "main" not in line and "end" not in line and "->" not in line and ">>" not in line and "?" not in line and not line.startswith(("if ", "while ", "for ", "elseif", "else")):
48
- parts = line.split("(")
49
- name = parts[0].strip()
50
- params_str = parts[1].replace(")", "").strip()
51
- params = [p.strip() for p in params_str.split(",")] if params_str else []
52
- self.functions[f"{module_name}.{name}"] = {"start_pc": pc + 1, "params": params, "source_lines": lines}
81
+ def _find_last_op(self, string, op):
82
+ in_quote = False
83
+ quote_char = None
84
+ last_idx = -1
85
+ for i, char in enumerate(string):
86
+ if char in ['"', "'"]:
87
+ if not in_quote:
88
+ in_quote = True
89
+ quote_char = char
90
+ elif char == quote_char:
91
+ in_quote = False
92
+ quote_char = None
93
+ elif not in_quote and char == op:
94
+ last_idx = i
95
+ return last_idx
53
96
 
54
97
  def _compute(self, expr):
55
98
  expr = str(expr).strip()
56
-
57
- def split_outside_quotes(string, op):
58
- parts = []
59
- current = []
60
- in_quote = False
61
- quote_char = None
62
- for char in string:
63
- if char in ['"', "'"]:
64
- if not in_quote:
65
- in_quote = True
66
- quote_char = char
67
- elif char == quote_char:
68
- in_quote = False
69
- quote_char = None
70
- current.append(char)
71
- elif not in_quote and char == op:
72
- parts.append("".join(current))
73
- current = []
74
- else:
75
- current.append(char)
76
- parts.append("".join(current))
77
- return parts
78
-
79
- plus_parts = split_outside_quotes(expr, '+')
99
+
100
+ # Handle parentheses
101
+ idx = 0
102
+ while True:
103
+ end_idx = expr.find(')', idx)
104
+ if end_idx == -1:
105
+ break
106
+ start_idx = expr.rfind('(', 0, end_idx)
107
+ if start_idx == -1:
108
+ break
109
+ # Skip function calls
110
+ if start_idx > 0 and (expr[start_idx - 1].isalnum() or expr[start_idx - 1] == '_'):
111
+ idx = end_idx + 1
112
+ continue
113
+ inner_expr = expr[start_idx + 1:end_idx]
114
+ inner_result = self._compute(inner_expr)
115
+ expr = expr[:start_idx] + str(inner_result) + expr[end_idx + 1:]
116
+ idx = 0
117
+
118
+ # Handle string concatenation with +
119
+ plus_parts = self._split_outside_quotes(expr, '+')
80
120
  if len(plus_parts) > 1:
81
- if any(any(c.isalpha() or c in ['"', "'"] for c in p) for p in plus_parts):
121
+ is_string_concat = False
122
+ for p in plus_parts:
123
+ p = p.strip()
124
+ if (p.startswith('"') and p.endswith('"')) or (p.startswith("'") and p.endswith("'")):
125
+ is_string_concat = True
126
+ break
127
+ if is_string_concat:
82
128
  result = ""
83
129
  for p in plus_parts:
84
130
  val = self._resolve(p.strip())
85
131
  result += str(val) if val is not None else ""
86
132
  return result
87
-
88
- ops = ['+', '-', '*', ':']
89
- for op in ops:
90
- op_parts = split_outside_quotes(expr, op)
91
- if len(op_parts) > 1:
92
- try:
93
- left = op_parts[0]
94
- right = op.join(op_parts[1:])
95
- r1 = self._resolve(left.strip())
96
- r2 = self._resolve(right.strip())
97
- if r1 is None or r2 is None: continue
98
- val1 = float(r1)
99
- val2 = float(r2)
100
- if op == '+': return val1 + val2
101
- if op == '-': return val1 - val2
102
- if op == '*': return val1 * val2
103
- if op == ':': return val1 / val2
104
- except (ValueError, TypeError): continue
133
+
134
+ # Check for equality (=) - comparison operator
135
+ left, right = self._split_on_op(expr, '=')
136
+ if left is not None:
137
+ r1 = self._resolve(left.strip())
138
+ r2 = self._resolve(right.strip())
139
+ if r1 is not None and r2 is not None:
140
+ return 1 if r1 == r2 else 0
141
+
142
+ # Level 1: + and -
143
+ for op in ['+', '-']:
144
+ idx = self._find_last_op(expr, op)
145
+ if idx != -1:
146
+ left = expr[:idx].strip()
147
+ right = expr[idx+1:].strip()
148
+ r1 = self._resolve(left)
149
+ r2 = self._resolve(right)
150
+ if r1 is None or r2 is None:
151
+ continue
152
+ val1 = float(r1)
153
+ val2 = float(r2)
154
+ if op == '+': return val1 + val2
155
+ if op == '-': return val1 - val2
156
+
157
+ # Level 2: * and :
158
+ last_idx = -1
159
+ last_op = None
160
+ for op in ['*', ':']:
161
+ idx = self._find_last_op(expr, op)
162
+ if idx > last_idx:
163
+ last_idx = idx
164
+ last_op = op
165
+ if last_idx != -1:
166
+ left = expr[:last_idx].strip()
167
+ right = expr[last_idx+1:].strip()
168
+ r1 = self._resolve(left)
169
+ r2 = self._resolve(right)
170
+ if r1 is None or r2 is None:
171
+ pass
172
+ else:
173
+ val1 = float(r1)
174
+ val2 = float(r2)
175
+ if last_op == '*': return val1 * val2
176
+ if last_op == ':': return val1 / val2
177
+
105
178
  return self._resolve(expr)
106
179
 
107
180
  def _lookup(self, name):
@@ -119,6 +192,14 @@ class SolInterpreter:
119
192
  if val.startswith("_fetch(") and val.endswith(")"):
120
193
  url_expr = val[7:-1].strip()
121
194
  return self._native_fetch(str(self._compute(url_expr)))
195
+ if val.endswith("{}") and "{" not in val[:-2]:
196
+ struct_name = val[:-2].strip()
197
+ blueprint = self.struct_blueprints.get(struct_name)
198
+ if blueprint is not None:
199
+ return blueprint.copy()
200
+ obj = self._lookup(struct_name)
201
+ if isinstance(obj, dict):
202
+ return obj.copy()
122
203
  if "(" in val and val.endswith(")") and not val.startswith("_"):
123
204
  parts = val.split("(", 1)
124
205
  name = parts[0].strip()
@@ -128,7 +209,7 @@ class SolInterpreter:
128
209
  return self._call_function(name, args)
129
210
  if "[" in val and "]" in val:
130
211
  try:
131
- name, idx_part = val.split("[")
212
+ name, idx_part = val.split("[", 1)
132
213
  idx = int(idx_part.replace("]", "").strip())
133
214
  container = self._lookup(name)
134
215
  if isinstance(container, list): return container[idx]
@@ -143,20 +224,8 @@ class SolInterpreter:
143
224
 
144
225
  if val.replace('.', '', 1).isdigit() or (val.startswith('-') and val[1:].replace('.', '', 1).isdigit()):
145
226
  return float(val) if '.' in val else int(val)
146
-
147
- return None
148
-
149
- def _async_worker(self, expr, action):
150
- result = self._compute(expr)
151
- if action == "_out": print(f"[ASYNC OUTPUT] {result}")
152
227
 
153
- def _async_input_worker(self, prompt, var_name):
154
- user_val = input(prompt)
155
- try:
156
- if '.' in user_val: user_val = float(user_val)
157
- else: user_val = int(user_val)
158
- except ValueError: pass
159
- self._assign(var_name, user_val)
228
+ return None
160
229
 
161
230
  def _native_fetch(self, url):
162
231
  try:
@@ -184,17 +253,17 @@ class SolInterpreter:
184
253
  self.threads.append(t)
185
254
 
186
255
  def _evaluate_condition(self, cond):
187
- if "==" in cond:
188
- left, right = cond.split("==", 1)
189
- return self._compute(left.strip()) == self._compute(right.strip())
190
- if "=" in cond:
191
- left, right = cond.split("=")
192
- return self._compute(left.strip()) == self._compute(right.strip())
193
- elif "<" in cond:
194
- left, right = cond.split("<")
256
+ # Equality comparison uses =
257
+ left, right = self._split_on_op(cond, "=")
258
+ if left is not None:
259
+ return self._compute(cond) # _compute handles =
260
+ # Less than
261
+ left, right = self._split_on_op(cond, "<")
262
+ if left is not None:
195
263
  return self._compute(left.strip()) < self._compute(right.strip())
196
- elif ">" in cond:
197
- left, right = cond.split(">")
264
+ # Greater than
265
+ left, right = self._split_on_op(cond, ">")
266
+ if left is not None:
198
267
  return self._compute(left.strip()) > self._compute(right.strip())
199
268
  return False
200
269
 
@@ -211,8 +280,21 @@ class SolInterpreter:
211
280
  return pc
212
281
 
213
282
  def execute(self, code):
214
- self.lines = [l.strip() for l in code.split('\n') if l.strip() and ';' not in l or (';' in l and l.split(';', 1)[0].strip())]
215
- self.lines = [l.split(';', 1)[0].strip() for l in self.lines]
283
+ raw_lines = code.split('\n')
284
+ self.lines = []
285
+ for l in raw_lines:
286
+ l = l.strip()
287
+ if not l:
288
+ continue
289
+ if l.startswith(';'):
290
+ continue
291
+ if l.startswith('//'):
292
+ continue
293
+ if ';' in l:
294
+ l = l.split(';', 1)[0].strip()
295
+ if l:
296
+ self.lines.append(l)
297
+
216
298
  self.threads = []
217
299
  pc = 0
218
300
  while pc < len(self.lines):
@@ -220,7 +302,10 @@ class SolInterpreter:
220
302
  if "(" in line and ")" in line and "main" not in line and "end" not in line and "->" not in line and ">>" not in line and "?" not in line and not line.startswith(("if ", "while ", "for ", "elseif", "else")):
221
303
  parts = line.split("(")
222
304
  self.functions[parts[0].strip()] = {"start_pc": pc + 1, "params": [p.strip() for p in parts[1].replace(")", "").split(",")] if parts[1].replace(")", "").strip() else []}
223
- elif "{" in line: pc = self._parse_struct(pc)
305
+ elif line.endswith("{}") and "->" not in line and ">>" not in line and "?" not in line and not line.startswith("_"):
306
+ pc = self._parse_struct(pc)
307
+ pc += 1
308
+ continue
224
309
  pc += 1
225
310
  for i, line in enumerate(self.lines):
226
311
  if line == "main()":
@@ -253,7 +338,9 @@ class SolInterpreter:
253
338
  return self._run_with_scope(self.functions[name]["start_pc"], self.functions[name]["params"], args)
254
339
 
255
340
  def _run_with_scope(self, start_pc, params, args):
256
- local_scope = {p: a for p, a in zip(params, args)}
341
+ local_scope = {}
342
+ for p, a in zip(params, args):
343
+ local_scope[p] = a
257
344
  self.scope_stack.append(local_scope)
258
345
  try: self._run_block(start_pc)
259
346
  except ReturnSignal as sig: return sig.value
@@ -261,55 +348,114 @@ class SolInterpreter:
261
348
  return 0
262
349
 
263
350
  def _execute_line(self, line):
264
- if "_get(" in line:
351
+ # Handle imports
352
+ if line.startswith("_get("):
265
353
  filename = line.split('"')[1]
266
354
  self._import_file(filename)
267
355
  return
268
- if ">>" in line and "_input" in line:
356
+
357
+ # Handle piping input
358
+ if ">>" in line and "_in" in line:
359
+ parts = [p.strip() for p in line.split(">>")]
360
+ if len(parts) >= 3 and parts[1] == "_in":
361
+ prompt = str(self._compute(parts[0]))
362
+ var_name = parts[2]
363
+ sys.stdout.write(prompt + " "); sys.stdout.flush()
364
+ val = sys.stdin.readline().strip()
365
+ try: self._assign(var_name, float(val) if '.' in val else int(val))
366
+ except: self._assign(var_name, val)
367
+ return
368
+
369
+ # Check if async
370
+ is_async = False
371
+ if ">>" in line:
372
+ parts = [p.strip() for p in line.split(">>")]
373
+ if parts[-1] == "_async":
374
+ line = ">>".join(line.split(">>")[:-1]).strip()
375
+ is_async = True
376
+
377
+ if line.startswith("_return ->"):
378
+ raise ReturnSignal(self._compute(line.split("->", 1)[1]))
379
+
380
+ # Handle _out with piping
381
+ if ">>" in line and "_out" in line:
269
382
  parts = [p.strip() for p in line.split(">>")]
270
- t = threading.Thread(target=self._async_input_worker, args=(str(self._compute(parts[0])), parts[2]))
271
- t.start(); self.threads.append(t); return
272
- is_async_func = ">>" in line and "_async" in line and "(" in line and not line.startswith("_")
273
- if is_async_func: line = line.split(">>")[0].strip()
274
- if line.startswith("_return ->"): raise ReturnSignal(self._compute(line.split("->", 1)[1]))
275
- elif ">>" in line and "_async" in line:
276
- parts = line.split(">>")
277
- t = threading.Thread(target=self._async_worker, args=(parts[0].strip(), parts[2].strip()))
278
- t.start(); self.threads.append(t)
279
- elif line.startswith("_in"):
280
- prompt = line.split('(')[1].split(')')[0].replace('"', '').replace("'", "") if "(" in line else f"Input {line.split('->')[1].strip()}: "
281
- sys.stdout.write(prompt + " "); sys.stdout.flush()
282
- val = sys.stdin.readline().strip()
283
- try: self._assign(line.split("->")[1].strip(), float(val) if '.' in val else int(val))
284
- except: self._assign(line.split("->")[1].strip(), val)
285
- elif "_add(" in line:
383
+ if len(parts) == 2 and parts[1] == "_out":
384
+ val = self._compute(parts[0])
385
+ if is_async:
386
+ t = threading.Thread(target=lambda v=val: print(v if v is not None else ""))
387
+ t.start(); self.threads.append(t)
388
+ else:
389
+ print(val if val is not None else "")
390
+ return
391
+
392
+ if line.startswith("_out"):
393
+ val = self._compute(line.split("->", 1)[1].strip())
394
+ if is_async:
395
+ t = threading.Thread(target=lambda v=val: print(v if v is not None else ""))
396
+ t.start(); self.threads.append(t)
397
+ else:
398
+ print(val if val is not None else "")
399
+ return
400
+
401
+ if line.startswith("_in"):
402
+ if "->" in line:
403
+ var_name = line.split("->", 1)[1].strip()
404
+ sys.stdout.write(f"Input {var_name}: "); sys.stdout.flush()
405
+ val = sys.stdin.readline().strip()
406
+ try: self._assign(var_name, float(val) if '.' in val else int(val))
407
+ except: self._assign(var_name, val)
408
+ return
409
+
410
+ if "_add(" in line:
286
411
  parts = [p.strip() for p in line.replace("_add(", "").replace(")", "").split(",")]
287
412
  container = self._lookup(parts[0])
288
413
  if isinstance(container, list): container.insert(int(parts[1]), self._resolve(parts[2]))
289
- elif "_remove(" in line:
414
+ return
415
+
416
+ if "_remove(" in line:
290
417
  parts = [p.strip() for p in line.replace("_remove(", "").replace(")", "").split(",")]
291
418
  container = self._lookup(parts[0])
292
419
  if isinstance(container, list): container.pop(int(parts[1]))
293
- elif "_listen(" in line:
420
+ return
421
+
422
+ if "_listen(" in line:
294
423
  p, h = [p.strip() for p in line.replace("_listen(", "").replace(")", "").split(",")]
295
424
  self._native_listen(self._compute(p), h)
296
- elif "_out" in line:
297
- val = self._compute(line.split("->")[1].strip())
298
- print(val if val is not None else "Error")
299
- elif "mylist<" in line: self._assign(line.split("<")[1].split(">")[0], [])
300
- elif "->" in line:
301
- var, val = [x.strip() for x in line.split("->")]
425
+ return
426
+
427
+ # List declaration
428
+ if "<" in line and ">" in line and "->" not in line and ">>" not in line and "?" not in line and not line.startswith("_"):
429
+ parts = line.split("<")
430
+ if len(parts) == 2 and ">" in parts[1]:
431
+ list_name = parts[1].split(">")[0].strip()
432
+ self._assign(list_name, [])
433
+ return
434
+
435
+ # Assignment - ONLY with ->
436
+ if "->" in line:
437
+ var, val = [x.strip() for x in line.split("->", 1)]
302
438
  if "." in var:
303
439
  obj, prop = var.split(".", 1)
304
440
  target = self._lookup(obj)
305
441
  if isinstance(target, dict): target[prop] = self._compute(val)
306
- else: self._assign(var, self._compute(val))
307
- elif "(" in line and line.endswith(")") and not line.startswith("_"):
442
+ else:
443
+ computed = self._compute(val)
444
+ self._assign(var, computed)
445
+ return
446
+
447
+ # Function call
448
+ if "(" in line and line.endswith(")") and not line.startswith("_"):
308
449
  parts = line.split("(")
309
- if is_async_func:
310
- t = threading.Thread(target=self._call_function, args=(parts[0].strip(), [self._compute(a.strip()) for a in parts[1].replace(")", "").split(",")]))
450
+ func_name = parts[0].strip()
451
+ args_str = parts[1][:-1].strip()
452
+ args = [self._compute(a.strip()) for a in args_str.split(",")] if args_str else []
453
+ if is_async:
454
+ t = threading.Thread(target=self._call_function, args=(func_name, args))
311
455
  t.start(); self.threads.append(t)
312
- else: self._call_function(parts[0].strip(), [self._compute(a.strip()) for a in parts[1].replace(")", "").split(",")])
456
+ else:
457
+ self._call_function(func_name, args)
458
+ return
313
459
 
314
460
  def _run_block(self, start_pc):
315
461
  pc = start_pc
@@ -345,8 +491,9 @@ class SolInterpreter:
345
491
  elif line.startswith("for "):
346
492
  parts = line.split("for ", 1)[1].split("->")
347
493
  var_name = parts[0].strip()
348
- start_v = int(self._compute(parts[1].split(" to ")[0].strip()))
349
- end_v = int(self._compute(parts[1].split(" to ")[1].strip()))
494
+ range_parts = parts[1].split(" to ")
495
+ start_v = int(self._compute(range_parts[0].strip()))
496
+ end_v = int(self._compute(range_parts[1].strip()))
350
497
  end_pc = self._get_block_end(pc)
351
498
  if is_executing and start_v <= end_v:
352
499
  self._assign(var_name, start_v)
@@ -365,7 +512,7 @@ class SolInterpreter:
365
512
  pc = top["start_pc"] - 1
366
513
  pc += 1; continue
367
514
  if is_executing:
368
- if "?" in line:
515
+ if self._has_guard(line):
369
516
  cond, action = line.split("?", 1)
370
517
  if self._evaluate_condition(cond.strip()): self._execute_line(action.strip())
371
518
  else: self._execute_line(line)
@@ -383,4 +530,3 @@ def main():
383
530
 
384
531
  if __name__ == "__main__":
385
532
  main()
386
-
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "solpl"
7
- version = "2.6.6"
7
+ version = "2.7.0"
8
8
  authors = [
9
9
  { name = "stefand-0", email = "stefand-0@users.noreply.github.com" }
10
10
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: solpl
3
- Version: 2.6.6
3
+ Version: 2.7.0
4
4
  Summary: A lightweight, multi-threaded interpreter for the Sol programming language
5
5
  Author-email: stefand-0 <stefand-0@users.noreply.github.com>
6
6
  License-Expression: Apache-2.0
@@ -82,6 +82,11 @@ main()
82
82
  end
83
83
  ```
84
84
 
85
+ > [!IMPORTANT]
86
+ > Asynchronous functions cannot use the _return method to return a value. Please use _out instead.
87
+
88
+ > [!TIP]
89
+ > Make use of the standard libraries for extra data types, such as Booleans, HashMaps etc.
85
90
  # License
86
91
 
87
92
  This repository is licensed with Apache License 2.0, see the LICENSE file for more details
File without changes
File without changes
File without changes