rapydscript-ns 0.8.2 → 0.8.3

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 (50) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/PYTHON_DIFFERENCES_REPORT.md +291 -0
  3. package/PYTHON_FEATURE_COVERAGE.md +96 -5
  4. package/README.md +161 -46
  5. package/TODO.md +2 -283
  6. package/language-service/index.js +4474 -0
  7. package/language-service/language-service.d.ts +40 -0
  8. package/package.json +9 -7
  9. package/src/baselib-builtins.pyj +77 -1
  10. package/src/baselib-containers.pyj +8 -4
  11. package/src/baselib-internal.pyj +30 -1
  12. package/src/baselib-str.pyj +8 -1
  13. package/src/lib/collections.pyj +1 -1
  14. package/src/lib/numpy.pyj +10 -10
  15. package/src/monaco-language-service/analyzer.js +131 -9
  16. package/src/monaco-language-service/builtins.js +12 -2
  17. package/src/monaco-language-service/completions.js +170 -1
  18. package/src/monaco-language-service/diagnostics.js +1 -1
  19. package/src/monaco-language-service/index.js +17 -0
  20. package/src/monaco-language-service/scope.js +3 -0
  21. package/src/output/classes.pyj +20 -3
  22. package/src/output/codegen.pyj +1 -1
  23. package/src/output/functions.pyj +4 -16
  24. package/src/output/loops.pyj +0 -9
  25. package/src/output/modules.pyj +1 -4
  26. package/src/output/operators.pyj +14 -0
  27. package/src/output/stream.pyj +0 -13
  28. package/src/parse.pyj +17 -1
  29. package/test/baselib.pyj +4 -4
  30. package/test/classes.pyj +56 -17
  31. package/test/collections.pyj +5 -5
  32. package/test/python_compat.pyj +326 -0
  33. package/test/python_features.pyj +110 -23
  34. package/test/slice.pyj +105 -0
  35. package/test/str.pyj +25 -0
  36. package/test/unit/fixtures/fibonacci_expected.js +1 -1
  37. package/test/unit/index.js +119 -7
  38. package/test/unit/language-service-builtins.js +70 -0
  39. package/test/unit/language-service-bundle.js +5 -5
  40. package/test/unit/language-service-completions.js +180 -0
  41. package/test/unit/language-service-index.js +350 -0
  42. package/test/unit/language-service-scope.js +255 -0
  43. package/test/unit/language-service.js +35 -0
  44. package/test/unit/run-language-service.js +1 -0
  45. package/test/unit/web-repl.js +134 -0
  46. package/tools/build-language-service.js +2 -2
  47. package/tools/compiler.js +0 -24
  48. package/tools/export.js +3 -37
  49. package/web-repl/rapydscript.js +6 -40
  50. package/web-repl/language-service.js +0 -4187
@@ -0,0 +1,40 @@
1
+ // language-service.d.ts — Ambient type declarations for language-service.js
2
+ // The actual implementation lives in language-service.js (bundled, do not edit directly).
3
+
4
+ export interface WebReplInstance {
5
+ in_block_mode: boolean;
6
+ compile(code: string, opts?: Record<string, unknown>): string;
7
+ compile_mapped(code: string, opts?: Record<string, unknown>): { code: string; sourceMap: string };
8
+ runjs(code: string): unknown;
9
+ replace_print(writeLine: (...args: unknown[]) => void): void;
10
+ is_input_complete(source: string): boolean;
11
+ init_completions(completelib: unknown): void;
12
+ find_completions(line: string): unknown;
13
+ }
14
+
15
+ export interface RapydScriptOptions {
16
+ compiler?: unknown;
17
+ virtualFiles?: Record<string, string>;
18
+ stdlibFiles?: Record<string, string>;
19
+ dtsFiles?: Array<{ name: string; content: string }>;
20
+ parseDelay?: number;
21
+ extraBuiltins?: Record<string, true>;
22
+ pythonFlags?: string;
23
+ pythonize_strings?: boolean;
24
+ loadDts?: (name: string) => string | Promise<string>;
25
+ }
26
+
27
+ export declare class RapydScriptLanguageService {
28
+ constructor(monaco: unknown, options: RapydScriptOptions);
29
+
30
+ setVirtualFiles(files: Record<string, string>): void;
31
+ removeVirtualFile(name: string): void;
32
+ addGlobals(names: string[]): void;
33
+ getScopeMap(model: unknown): unknown | null;
34
+ addDts(name: string, dtsText: string): void;
35
+ loadDts(name: string): Promise<void>;
36
+ dispose(): void;
37
+ }
38
+
39
+ export declare function registerRapydScript(monaco: unknown, options?: RapydScriptOptions): RapydScriptLanguageService;
40
+ export declare function web_repl(): WebReplInstance;
package/package.json CHANGED
@@ -2,6 +2,11 @@
2
2
  "name": "rapydscript-ns",
3
3
  "description": "Pythonic JavaScript that doesn't suck",
4
4
  "homepage": "https://github.com/ficocelliguy/rapydscript-ns",
5
+ "version": "0.8.3",
6
+ "license": "BSD-2-Clause",
7
+ "engines": {
8
+ "node": ">=0.12.0"
9
+ },
5
10
  "keywords": [
6
11
  "javascript",
7
12
  "rapydscript",
@@ -11,7 +16,10 @@
11
16
  "main": "tools/compiler.js",
12
17
  "exports": {
13
18
  ".": "./tools/compiler.js",
14
- "./language-service": "./web-repl/language-service.js"
19
+ "./language-service": {
20
+ "types": "./language-service/language-service.d.ts",
21
+ "default": "./language-service/index.js"
22
+ }
15
23
  },
16
24
  "scripts": {
17
25
  "test": "node bin/rapydscript self --complete --test && npm run test:unit && npm run test:ls",
@@ -23,11 +31,6 @@
23
31
  "build": "rm -rf **/*pyj-cached dev && node bin/rapydscript self --complete && node bin/web-repl-export web-repl && node tools/build-language-service.js",
24
32
  "prepublishOnly": "node tools/build-language-service.js"
25
33
  },
26
- "version": "0.8.2",
27
- "license": "BSD-2-Clause",
28
- "engines": {
29
- "node": ">=0.12.0"
30
- },
31
34
  "maintainers": [
32
35
  {
33
36
  "name": "Michael Ficocelli",
@@ -39,7 +42,6 @@
39
42
  "url": "https://github.com/ficocelliguy/rapydscript-ns.git"
40
43
  },
41
44
  "dependencies": {
42
- "regenerator": ">= 0.12.1",
43
45
  "uglify-js": ">= 3.0.15"
44
46
  },
45
47
  "bin": {
@@ -164,7 +164,22 @@ def ρσ_reversed(iterable):
164
164
  return ans
165
165
  raise TypeError('reversed() can only be called on arrays or strings')
166
166
 
167
- def ρσ_iter(iterable):
167
+ def ρσ_iter(iterable, sentinel):
168
+ # Two-argument form: iter(callable, sentinel) — call repeatedly until sentinel is returned
169
+ if arguments.length >= 2:
170
+ callable_ = iterable
171
+ ans = v'{"_callable":callable_,"_sentinel":sentinel,"_done":false}'
172
+ ans[ρσ_iterator_symbol] = def():
173
+ return this
174
+ ans['next'] = def():
175
+ if this._done:
176
+ return v"{'done':true}"
177
+ val = ρσ_callable_call(this._callable)
178
+ if v'val === this._sentinel':
179
+ this._done = True
180
+ return v"{'done':true}"
181
+ return v"{'done':false,'value':val}"
182
+ return ans
168
183
  # Generate a JavaScript iterator object from iterable
169
184
  if jstype(iterable[ρσ_iterator_symbol]) is 'function':
170
185
  return iterable.keys() if jstype(Map) is 'function' and v'iterable instanceof Map' else iterable[ρσ_iterator_symbol]()
@@ -363,6 +378,66 @@ def ρσ_max(*args, **kwargs):
363
378
  raise TypeError('expected at least one argument')
364
379
 
365
380
 
381
+ class ρσ_slice:
382
+ def __init__(self, start_or_stop, stop, step):
383
+ if arguments.length is 1:
384
+ self.start = None
385
+ self.stop = start_or_stop
386
+ self.step = None
387
+ elif arguments.length is 2:
388
+ self.start = start_or_stop
389
+ self.stop = stop
390
+ self.step = None
391
+ else:
392
+ self.start = start_or_stop
393
+ self.stop = stop
394
+ self.step = step
395
+
396
+ def indices(self, length):
397
+ step = 1 if self.step is None else self.step
398
+ if step is 0:
399
+ raise ValueError('slice step cannot be zero')
400
+ if step > 0:
401
+ lower = 0
402
+ upper = length
403
+ start = lower if self.start is None else self.start
404
+ stop = upper if self.stop is None else self.stop
405
+ else:
406
+ lower = -1
407
+ upper = length - 1
408
+ start = upper if self.start is None else self.start
409
+ stop = lower if self.stop is None else self.stop
410
+ # Only clamp values that were explicitly provided (None defaults are already correct).
411
+ if self.start is not None:
412
+ if start < 0:
413
+ start = max(start + length, lower)
414
+ if start > upper:
415
+ start = upper
416
+ if self.stop is not None:
417
+ if stop < 0:
418
+ stop = max(stop + length, lower)
419
+ if stop > upper:
420
+ stop = upper
421
+ return start, stop, step
422
+
423
+ def __repr__(self):
424
+ s = 'None' if self.start is None else String(self.start)
425
+ stop = 'None' if self.stop is None else String(self.stop)
426
+ step = 'None' if self.step is None else String(self.step)
427
+ return 'slice(' + s + ', ' + stop + ', ' + step + ')'
428
+
429
+ def __str__(self):
430
+ return self.__repr__()
431
+
432
+ def __eq__(self, other):
433
+ if not v'other instanceof ρσ_slice':
434
+ return False
435
+ return self.start is other.start and self.stop is other.stop and self.step is other.step
436
+
437
+ def __hash__(self):
438
+ raise TypeError("unhashable type: 'slice'")
439
+
440
+
366
441
  v'var abs = Math.abs, max = ρσ_max.bind(Math.max), min = ρσ_max.bind(Math.min), bool = ρσ_bool, type = ρσ_type'
367
442
  v'var float = ρσ_float, int = ρσ_int, arraylike = ρσ_arraylike_creator(), ρσ_arraylike = arraylike'
368
443
  v'var id = ρσ_id, get_module = ρσ_get_module, pow = ρσ_pow, divmod = ρσ_divmod'
@@ -371,3 +446,4 @@ v'var enumerate = ρσ_enumerate, iter = ρσ_iter, reversed = ρσ_reversed, le
371
446
  v'var range = ρσ_range, getattr = ρσ_getattr, setattr = ρσ_setattr, hasattr = ρσ_hasattr, issubclass = ρσ_issubclass, hash = ρσ_hash, next = ρσ_next'
372
447
  v'var ρσ_Ellipsis = Object.freeze({toString: function(){return "Ellipsis";}, __repr__: function(){return "Ellipsis";}})'
373
448
  v'var Ellipsis = ρσ_Ellipsis'
449
+ v'var slice = ρσ_slice'
@@ -140,7 +140,7 @@ def ρσ_list_sort(key=None, reverse=False):
140
140
  k = this[i] # noqa:undef
141
141
  keymap.set(k, key(k))
142
142
  posmap.set(k, i)
143
- this.sort(def (a, b): return mult * ρσ_list_sort_cmp(keymap.get(a), keymap.get(b), posmap.get(a), posmap.get(b));)
143
+ Array.prototype.sort.call(this, def (a, b): return mult * ρσ_list_sort_cmp(keymap.get(a), keymap.get(b), posmap.get(a), posmap.get(b));)
144
144
 
145
145
  def ρσ_list_concat(): # ensure concat() returns an object of type list
146
146
  ans = Array.prototype.concat.apply(this, arguments)
@@ -190,14 +190,18 @@ def ρσ_list_decorate(ans):
190
190
  ans.inspect = ρσ_list_to_string
191
191
  ans.extend = ρσ_list_extend
192
192
  ans.index = ρσ_list_index
193
- ans.pypop = ρσ_list_pop
193
+ ans.jspop = Array.prototype.pop # native JS pop (no bounds check, ignores args)
194
+ ans.pop = ρσ_list_pop # Python pop (bounds-checked, supports negative index)
195
+ ans.pypop = ρσ_list_pop # backward-compat alias for pop
194
196
  ans.remove = ρσ_list_remove
195
197
  ans.insert = ρσ_list_insert
196
198
  ans.copy = ρσ_list_copy
197
199
  ans.clear = ρσ_list_clear
198
200
  ans.count = ρσ_list_count
199
201
  ans.concat = ρσ_list_concat
200
- ans.pysort = ρσ_list_sort
202
+ ans.jssort = Array.prototype.sort # native JS sort (lexicographic by default)
203
+ ans.sort = ρσ_list_sort # Python sort (numeric by default)
204
+ ans.pysort = ρσ_list_sort # backward-compat alias for sort
201
205
  ans.slice = ρσ_list_slice
202
206
  ans.as_array = ρσ_list_as_array
203
207
  ans.__len__ = ρσ_list_len
@@ -235,7 +239,7 @@ v'var list = ρσ_list_constructor, list_wrap = ρσ_list_decorate'
235
239
 
236
240
  def sorted(iterable, key=None, reverse=False):
237
241
  ans = ρσ_list_constructor(iterable)
238
- ans.pysort(key, reverse)
242
+ ans.sort(key, reverse)
239
243
  return ans
240
244
  # }}}
241
245
 
@@ -1,7 +1,7 @@
1
1
  # vim:fileencoding=utf-8
2
2
  # License: BSD
3
3
 
4
- # globals: exports, console, ρσ_iterator_symbol, ρσ_kwargs_symbol, ρσ_arraylike, ρσ_list_contains, ρσ_list_constructor, ρσ_str, ρσ_int, ρσ_float
4
+ # globals: exports, console, ρσ_iterator_symbol, ρσ_kwargs_symbol, ρσ_arraylike, ρσ_list_contains, ρσ_list_constructor, ρσ_str, ρσ_int, ρσ_float, ρσ_slice
5
5
 
6
6
  def ρσ_eslice(arr, step, start, end):
7
7
  if jstype(arr) is 'string' or v'arr instanceof String':
@@ -179,6 +179,11 @@ def ρσ_interpolate_kwargs_constructor(apply, f, supplied_args):
179
179
  def ρσ_getitem(obj, key):
180
180
  if obj.__getitem__:
181
181
  return obj.__getitem__(key)
182
+ if v'typeof ρσ_slice !== "undefined" && key instanceof ρσ_slice':
183
+ return ρσ_eslice(obj,
184
+ v'(key.step !== null && key.step !== undefined) ? key.step : 1',
185
+ v'(key.start !== null && key.start !== undefined) ? key.start : undefined',
186
+ v'(key.stop !== null && key.stop !== undefined) ? key.stop : undefined')
182
187
  if jstype(key) is 'number' and key < 0:
183
188
  key += obj.length
184
189
  return obj[key]
@@ -186,6 +191,10 @@ def ρσ_getitem(obj, key):
186
191
  def ρσ_setitem(obj, key, val):
187
192
  if obj.__setitem__:
188
193
  obj.__setitem__(key, val)
194
+ elif v'typeof ρσ_slice !== "undefined" && key instanceof ρσ_slice':
195
+ ρσ_splice(obj, val,
196
+ v'(key.start !== null && key.start !== undefined) ? key.start : 0',
197
+ v'(key.stop !== null && key.stop !== undefined) ? key.stop : obj.length')
189
198
  else:
190
199
  if jstype(key) is 'number' and key < 0:
191
200
  key += obj.length
@@ -195,6 +204,11 @@ def ρσ_setitem(obj, key, val):
195
204
  def ρσ_delitem(obj, key):
196
205
  if obj.__delitem__:
197
206
  obj.__delitem__(key)
207
+ elif v'typeof ρσ_slice !== "undefined" && key instanceof ρσ_slice':
208
+ ρσ_delslice(obj,
209
+ v'(key.step !== null && key.step !== undefined) ? key.step : 1',
210
+ v'(key.start !== null && key.start !== undefined) ? key.start : undefined',
211
+ v'(key.stop !== null && key.stop !== undefined) ? key.stop : undefined')
198
212
  elif jstype(obj.splice) is 'function':
199
213
  obj.splice(key, 1)
200
214
  else:
@@ -339,6 +353,21 @@ def ρσ_op_rshift(a, b):
339
353
  if b is not None and jstype(b.__rrshift__) is 'function': return b.__rrshift__(a)
340
354
  return a >> b
341
355
 
356
+ # List-concatenation helpers (always available, no overload_operators required).
357
+ # ρσ_list_add: used for the + operator; creates a new list when both sides are arrays.
358
+ # NOTE: the fallback uses v'a + b' (verbatim JS) to avoid recursive self-compilation.
359
+ def ρσ_list_add(a, b):
360
+ if Array.isArray(a) and Array.isArray(b):
361
+ return ρσ_list_constructor(a.concat(b))
362
+ return v'a + b'
363
+
364
+ # ρσ_list_iadd: used for +=; extends a in-place when both sides are arrays (Python semantics).
365
+ def ρσ_list_iadd(a, b):
366
+ if Array.isArray(a) and Array.isArray(b):
367
+ v'Array.prototype.push.apply(a, b)'
368
+ return a
369
+ return v'a + b'
370
+
342
371
  # Unary operator overloading helpers
343
372
  def ρσ_op_neg(a):
344
373
  if a is not None and jstype(a.__neg__) is 'function': return a.__neg__()
@@ -829,4 +829,11 @@ define_str_func('zfill', def(width):
829
829
  ρσ_str.whitespace = ' \t\n\r\x0b\x0c'
830
830
 
831
831
  v'define_str_func = undefined'
832
- v'var str = ρσ_str, repr = ρσ_repr'
832
+ def ρσ_format(value, spec):
833
+ if value is not None and value is not undefined and v'typeof value.__format__ === "function"':
834
+ return value.__format__(spec if spec is not undefined else '')
835
+ if spec is undefined or spec is '':
836
+ return ρσ_str(value)
837
+ return str.format('{:' + spec + '}', value)
838
+
839
+ v'var str = ρσ_str, repr = ρσ_repr, format = ρσ_format'
@@ -353,7 +353,7 @@ class Counter:
353
353
 
354
354
  def most_common(self, n=None):
355
355
  items = [[k, self._data[k]] for k in self._data]
356
- items.pysort(key=def(pair): return -pair[1];)
356
+ items.sort(key=def(pair): return -pair[1];)
357
357
  if n is not None:
358
358
  return items[:n]
359
359
  return items
package/src/lib/numpy.pyj CHANGED
@@ -1055,7 +1055,7 @@ def sort(a, axis=-1):
1055
1055
  a = asarray(a)
1056
1056
  if a.ndim is 1 or axis is None:
1057
1057
  data = a._data.slice()
1058
- data.sort(def(x, y): return x - y;)
1058
+ data.jssort(def(x, y): return x - y;)
1059
1059
  return ndarray(a.shape.slice(), a.dtype, data, True)
1060
1060
  # 2D sort along last axis (axis=1 or axis=-1)
1061
1061
  if axis < 0:
@@ -1066,7 +1066,7 @@ def sort(a, axis=-1):
1066
1066
  if axis is 1:
1067
1067
  for i in range(rows):
1068
1068
  row = a._data.slice(i * cols, (i + 1) * cols)
1069
- row.sort(def(x, y): return x - y;)
1069
+ row.jssort(def(x, y): return x - y;)
1070
1070
  for v in row:
1071
1071
  new_data.push(v)
1072
1072
  elif axis is 0:
@@ -1074,7 +1074,7 @@ def sort(a, axis=-1):
1074
1074
  col = []
1075
1075
  for i in range(rows):
1076
1076
  col.push(a._data[i * cols + j])
1077
- col.sort(def(x, y): return x - y;)
1077
+ col.jssort(def(x, y): return x - y;)
1078
1078
  for i in range(rows):
1079
1079
  new_data[i * cols + j] = col[i]
1080
1080
  if new_data.length is 0:
@@ -1089,14 +1089,14 @@ def argsort(a, axis=-1):
1089
1089
  for i in range(a.size):
1090
1090
  indices.push(i)
1091
1091
  data = a._data.slice()
1092
- indices.sort(def(i, j): return data[i] - data[j];)
1092
+ indices.jssort(def(i, j): return data[i] - data[j];)
1093
1093
  return ndarray([a.size], 'int32', indices, True)
1094
1094
  # Flatten and sort
1095
1095
  data = a._data.slice()
1096
1096
  indices = []
1097
1097
  for i in range(data.length):
1098
1098
  indices.push(i)
1099
- indices.sort(def(i, j): return data[i] - data[j];)
1099
+ indices.jssort(def(i, j): return data[i] - data[j];)
1100
1100
  return ndarray([indices.length], 'int32', indices, True)
1101
1101
 
1102
1102
  def searchsorted(a, v, side='left'):
@@ -1157,7 +1157,7 @@ def where(condition, x=None, y=None):
1157
1157
  def median(a, axis=None):
1158
1158
  a = asarray(a)
1159
1159
  data = a._data.slice()
1160
- data.sort(def(x, y): return x - y;)
1160
+ data.jssort(def(x, y): return x - y;)
1161
1161
  n = data.length
1162
1162
  if n % 2 is 1:
1163
1163
  return data[Math.floor(n / 2)]
@@ -1166,7 +1166,7 @@ def median(a, axis=None):
1166
1166
  def percentile(a, q, axis=None):
1167
1167
  a = asarray(a)
1168
1168
  data = a._data.slice()
1169
- data.sort(def(x, y): return x - y;)
1169
+ data.jssort(def(x, y): return x - y;)
1170
1170
  n = data.length
1171
1171
  idx = q / 100.0 * (n - 1)
1172
1172
  lo = Math.floor(idx)
@@ -1497,7 +1497,7 @@ def union1d(ar1, ar2):
1497
1497
  if not seen[key]:
1498
1498
  seen[key] = True
1499
1499
  unique.push(v)
1500
- unique.sort(def(a, b): return a - b;)
1500
+ unique.jssort(def(a, b): return a - b;)
1501
1501
  return ndarray([unique.length], 'float64', unique, True)
1502
1502
 
1503
1503
  def intersect1d(ar1, ar2):
@@ -1513,7 +1513,7 @@ def intersect1d(ar1, ar2):
1513
1513
  if s[key] and not seen[key]:
1514
1514
  seen[key] = True
1515
1515
  result.push(v)
1516
- result.sort(def(a, b): return a - b;)
1516
+ result.jssort(def(a, b): return a - b;)
1517
1517
  return ndarray([result.length], 'float64', result, True)
1518
1518
 
1519
1519
  def setdiff1d(ar1, ar2):
@@ -1529,7 +1529,7 @@ def setdiff1d(ar1, ar2):
1529
1529
  if not s[key] and not seen[key]:
1530
1530
  seen[key] = True
1531
1531
  result.push(v)
1532
- result.sort(def(a, b): return a - b;)
1532
+ result.jssort(def(a, b): return a - b;)
1533
1533
  return ndarray([result.length], 'float64', result, True)
1534
1534
 
1535
1535
  def in1d(ar1, ar2):
@@ -46,6 +46,95 @@ function dot_path_from_node(node, RS) {
46
46
  return null;
47
47
  }
48
48
 
49
+ /**
50
+ * Infer the type of a single return-value expression.
51
+ * Returns a type string ('list', 'dict', 'str', 'number', 'bool', or a class name),
52
+ * or null if the type cannot be determined from this expression alone.
53
+ *
54
+ * @param {object} node - The AST expression node (return value)
55
+ * @param {ScopeFrame} scope - The function's own scope frame (fully populated)
56
+ * @param {object} RS - RapydScript compiler object
57
+ * @returns {string|null}
58
+ */
59
+ function infer_expr_type(node, scope, RS) {
60
+ if (node instanceof RS.AST_Array) return 'list';
61
+ if (node instanceof RS.AST_Object) return 'dict';
62
+ if (node instanceof RS.AST_String) return 'str';
63
+ if (node instanceof RS.AST_Number) return 'number';
64
+ if ((RS.AST_True && node instanceof RS.AST_True) ||
65
+ (RS.AST_False && node instanceof RS.AST_False)) return 'bool';
66
+ if (RS.AST_Null && node instanceof RS.AST_Null) return null; // None → not counted
67
+
68
+ if (node instanceof RS.AST_SymbolRef) {
69
+ const sym = scope ? scope.getSymbol(node.name) : null;
70
+ return (sym && sym.inferred_class) ? sym.inferred_class : null;
71
+ }
72
+
73
+ if (node instanceof RS.AST_BaseCall && node.expression instanceof RS.AST_SymbolRef) {
74
+ const name = node.expression.name;
75
+ // PascalCase → assume class constructor, use name as type
76
+ if (/^[A-Z]/.test(name)) return name;
77
+ // Known local function with an already-resolved return_type
78
+ const sym = scope ? scope.getSymbol(name) : null;
79
+ if (sym && sym.return_type) return sym.return_type;
80
+ }
81
+
82
+ return null;
83
+ }
84
+
85
+ /**
86
+ * Walk a function node's body, collect all return-expression types, and return
87
+ * the single inferred type if every non-None return agrees on one type.
88
+ * Skips nested function bodies (their returns don't belong to this function).
89
+ * Returns null if returns disagree, are unrecognised, or the body has no typed returns.
90
+ *
91
+ * @param {object} func_node - AST_Lambda node
92
+ * @param {ScopeFrame} func_scope - The function's own scope frame (already populated)
93
+ * @param {object} RS - RapydScript compiler object
94
+ * @returns {string|null}
95
+ */
96
+ function collect_return_type(func_node, func_scope, RS) {
97
+ const types = new Set();
98
+ // Use a plain visitor object (same pattern as ScopeBuilder): _visit(node, descend)
99
+ // receives the node and a closure that walks its children; call descend() to recurse.
100
+ func_node.walk({
101
+ _visit: function (node, descend) {
102
+ // Stop descent into nested functions — their returns are not ours.
103
+ if (node !== func_node && node instanceof RS.AST_Lambda) return;
104
+ if (node instanceof RS.AST_Return && node.value) {
105
+ const t = infer_expr_type(node.value, func_scope, RS);
106
+ if (t) types.add(t);
107
+ }
108
+ if (descend) descend(); // recurse into children
109
+ },
110
+ });
111
+ return types.size === 1 ? [...types][0] : null;
112
+ }
113
+
114
+ /**
115
+ * Extract the return type annotation from a function node as a normalized string.
116
+ * Maps `-> [X]` to 'list', `-> {k: v}` to 'dict', `-> str` to 'str', etc.
117
+ * Returns null if no annotation or unrecognised shape.
118
+ * @param {object} func_node
119
+ * @param {object} RS
120
+ * @returns {string|null}
121
+ */
122
+ function extract_return_type(func_node, RS) {
123
+ const ann = func_node.return_annotation;
124
+ if (!ann) return null;
125
+ if (ann instanceof RS.AST_Array) return 'list';
126
+ if (ann instanceof RS.AST_Object) return 'dict';
127
+ if (ann instanceof RS.AST_SymbolRef ||
128
+ (RS.AST_SymbolVar && ann instanceof RS.AST_SymbolVar)) {
129
+ const n = ann.name;
130
+ if (n === 'str' || n === 'string') return 'str';
131
+ if (n === 'int' || n === 'float' || n === 'number') return 'number';
132
+ if (n === 'bool' || n === 'boolean') return 'bool';
133
+ return n; // user-defined class name or 'list'/'dict' spelled out
134
+ }
135
+ return null;
136
+ }
137
+
49
138
  /**
50
139
  * Collect parameter descriptors from an AST_Lambda node.
51
140
  * Handles regular args, positional-only (/), keyword-only (*), *args, and **kwargs.
@@ -94,10 +183,11 @@ function extract_params(lambda_node) {
94
183
 
95
184
  class ScopeBuilder {
96
185
  constructor(RS, map) {
97
- this._RS = RS;
98
- this._map = map;
99
- this._scopes = []; // stack of ScopeFrame
100
- this.current_node = null;
186
+ this._RS = RS;
187
+ this._map = map;
188
+ this._scopes = []; // stack of ScopeFrame
189
+ this.current_node = null;
190
+ this._current_from_module = null; // set while inside an AST_Import with argnames
101
191
  }
102
192
 
103
193
  _current_scope() {
@@ -143,6 +233,9 @@ class ScopeBuilder {
143
233
  params: opts.params || null,
144
234
  inferred_class: opts.inferred_class || null,
145
235
  dts_call_path: opts.dts_call_path || null,
236
+ return_type: opts.return_type || null,
237
+ source_module: opts.source_module || null,
238
+ original_name: opts.original_name || null,
146
239
  });
147
240
  scope.addSymbol(sym);
148
241
  return sym;
@@ -176,6 +269,7 @@ class ScopeBuilder {
176
269
  scope_depth: parent.depth,
177
270
  doc: extract_doc(node),
178
271
  params: extract_params(node),
272
+ return_type: extract_return_type(node, RS),
179
273
  });
180
274
  parent.addSymbol(sym);
181
275
  }
@@ -239,18 +333,28 @@ class ScopeBuilder {
239
333
  defined_at: pos_from_token(node.start),
240
334
  });
241
335
 
336
+ } else if (node instanceof RS.AST_Import && node.argnames) {
337
+ // `from X import name [as alias], ...` — record module name so children can use it.
338
+ const mod_node = node.module;
339
+ this._current_from_module = mod_node
340
+ ? (mod_node.name || mod_node.value || null)
341
+ : null;
342
+
242
343
  } else if (node instanceof RS.AST_ImportedVar) {
243
344
  // `from X import name` or `from X import name as alias`
244
- const name = (node.alias && node.alias.name) ? node.alias.name : node.name;
245
- if (name) {
345
+ const alias = (node.alias && node.alias.name) ? node.alias.name : node.name;
346
+ if (alias) {
246
347
  this._add_symbol({
247
- name,
248
- kind: 'import',
249
- defined_at: pos_from_token(node.start),
348
+ name: alias,
349
+ kind: 'import',
350
+ defined_at: pos_from_token(node.start),
351
+ source_module: this._current_from_module || null,
352
+ original_name: (node.name !== alias) ? node.name : null,
250
353
  });
251
354
  }
252
355
 
253
356
  } else if (node instanceof RS.AST_Import && !node.argnames) {
357
+ this._current_from_module = null;
254
358
  // `import X` or `import X as Y` (no from-clause)
255
359
  const name = (node.alias && node.alias.name)
256
360
  ? node.alias.name
@@ -456,6 +560,24 @@ class ScopeBuilder {
456
560
 
457
561
  if (cont) cont();
458
562
 
563
+ // ------------------------------------------------------------------
564
+ // 3b. Post-traversal: infer return type for unannotated functions.
565
+ // The function's own scope is fully populated at this point, so
566
+ // local variable inferred_class values are available for lookup.
567
+ // ------------------------------------------------------------------
568
+
569
+ if (node instanceof RS.AST_Lambda && this._scopes.length > prev_depth) {
570
+ const func_scope = this._current_scope();
571
+ const parent_frame = func_scope ? func_scope.parent : null;
572
+ const fname = node.name ? node.name.name : null;
573
+ if (fname && parent_frame) {
574
+ const sym = parent_frame.getSymbol(fname);
575
+ if (sym && !sym.return_type) {
576
+ sym.return_type = collect_return_type(node, func_scope, RS);
577
+ }
578
+ }
579
+ }
580
+
459
581
  // ------------------------------------------------------------------
460
582
  // 4. Pop the scope we pushed (if any).
461
583
  // ------------------------------------------------------------------
@@ -104,6 +104,11 @@ const STUBS = [
104
104
  return_type: 'iterable',
105
105
  doc: 'Return an iterator of elements from iterable for which function returns true.' }),
106
106
 
107
+ new BuiltinInfo({ name: 'format',
108
+ params: [p('value'), p('spec', { type: 'str', optional: true })],
109
+ return_type: 'str',
110
+ doc: 'Return value formatted according to the format spec string.\n\nEquivalent to calling `value.__format__(spec)` or applying spec as a `str.format()` format-spec field. The spec mini-language is the same as what follows `:` in f-strings and `str.format()` fields: alignment (`<>^=`), sign (`+-`), width, grouping (`,_`), precision (`.N`), and type (`bcdoxXeEfFgGns%`).\n\nExamples:\n\n format(42, \'08b\') # \'00101010\'\n format(3.14, \'.2f\') # \'3.14\'\n format(\'hi\', \'>10\') # \' hi\'\n format(42) # \'42\'' }),
111
+
107
112
  new BuiltinInfo({ name: 'float', kind: 'class',
108
113
  params: [p('x', { optional: true })],
109
114
  return_type: 'float',
@@ -150,9 +155,9 @@ const STUBS = [
150
155
  doc: 'Return True if cls is a subclass of classinfo. classinfo may be a class or tuple of classes.' }),
151
156
 
152
157
  new BuiltinInfo({ name: 'iter',
153
- params: [p('obj')],
158
+ params: [p('obj'), p('sentinel', { optional: true })],
154
159
  return_type: 'iterator',
155
- doc: 'Return an iterator object for obj.' }),
160
+ doc: 'iter(iterable) iterator over iterable. iter(callable, sentinel) → calls callable repeatedly until it returns sentinel.' }),
156
161
 
157
162
  new BuiltinInfo({ name: 'len',
158
163
  params: [p('s')],
@@ -219,6 +224,11 @@ const STUBS = [
219
224
  return_type: 'set',
220
225
  doc: 'Create a new set, optionally populated from an iterable.' }),
221
226
 
227
+ new BuiltinInfo({ name: 'slice', kind: 'class',
228
+ params: [p('start_or_stop', { type: 'int' }), p('stop', { type: 'int', optional: true }), p('step', { type: 'int', optional: true })],
229
+ return_type: 'slice',
230
+ doc: 'Create a slice object representing the set of indices specified by range(start, stop, step).\n\nForms:\n- `slice(stop)` — equivalent to `slice(None, stop, None)`\n- `slice(start, stop)` — equivalent to `slice(start, stop, None)`\n- `slice(start, stop, step)` — full form\n\nAttributes: `.start`, `.stop`, `.step` (each may be `None`).\n\nMethod: `.indices(length)` — returns `(start, stop, step)` normalized for a sequence of the given length.\n\nExample:\n\n s = slice(1, 5)\n lst[s] # same as lst[1:5]\n s.indices(10) # (1, 5, 1)' }),
231
+
222
232
  new BuiltinInfo({ name: 'setattr',
223
233
  params: [p('obj'), p('name', { type: 'str' }), p('value')],
224
234
  return_type: 'None',