rapydscript-ns 0.8.1 → 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 (58) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/CONTRIBUTORS +3 -2
  3. package/PYTHON_DIFFERENCES_REPORT.md +291 -0
  4. package/PYTHON_FEATURE_COVERAGE.md +200 -0
  5. package/README.md +480 -79
  6. package/TODO.md +6 -318
  7. package/hack_demo.pyj +112 -0
  8. package/language-service/index.js +4474 -0
  9. package/language-service/language-service.d.ts +40 -0
  10. package/package.json +9 -10
  11. package/src/ast.pyj +30 -6
  12. package/src/baselib-builtins.pyj +181 -11
  13. package/src/baselib-containers.pyj +154 -5
  14. package/src/baselib-errors.pyj +3 -0
  15. package/src/baselib-internal.pyj +40 -1
  16. package/src/baselib-str.pyj +42 -1
  17. package/src/lib/collections.pyj +1 -1
  18. package/src/lib/numpy.pyj +10 -10
  19. package/src/monaco-language-service/analyzer.js +132 -22
  20. package/src/monaco-language-service/builtins.js +22 -2
  21. package/src/monaco-language-service/completions.js +224 -3
  22. package/src/monaco-language-service/diagnostics.js +55 -5
  23. package/src/monaco-language-service/index.js +26 -5
  24. package/src/monaco-language-service/scope.js +3 -0
  25. package/src/output/classes.pyj +20 -3
  26. package/src/output/codegen.pyj +38 -3
  27. package/src/output/functions.pyj +35 -25
  28. package/src/output/loops.pyj +64 -11
  29. package/src/output/modules.pyj +1 -4
  30. package/src/output/operators.pyj +67 -1
  31. package/src/output/statements.pyj +7 -3
  32. package/src/output/stream.pyj +6 -13
  33. package/src/parse.pyj +94 -14
  34. package/src/tokenizer.pyj +1 -0
  35. package/test/baselib.pyj +4 -4
  36. package/test/classes.pyj +56 -17
  37. package/test/collections.pyj +5 -5
  38. package/test/python_compat.pyj +326 -0
  39. package/test/python_features.pyj +1271 -0
  40. package/test/slice.pyj +105 -0
  41. package/test/str.pyj +25 -0
  42. package/test/unit/fixtures/fibonacci_expected.js +1 -1
  43. package/test/unit/index.js +119 -7
  44. package/test/unit/language-service-builtins.js +70 -0
  45. package/test/unit/language-service-bundle.js +83 -0
  46. package/test/unit/language-service-completions.js +289 -0
  47. package/test/unit/language-service-index.js +350 -0
  48. package/test/unit/language-service-scope.js +255 -0
  49. package/test/unit/language-service.js +158 -1
  50. package/test/unit/run-language-service.js +2 -0
  51. package/test/unit/web-repl.js +134 -0
  52. package/tools/build-language-service.js +2 -2
  53. package/tools/compiler.js +0 -24
  54. package/tools/export.js +3 -37
  55. package/tools/lint.js +1 -1
  56. package/tools/self.js +1 -9
  57. package/web-repl/rapydscript.js +6 -40
  58. package/web-repl/language-service.js +0 -4084
@@ -8,6 +8,25 @@
8
8
  import { SourceAnalyzer } from './analyzer.js';
9
9
  import { resolve_first_type } from './dts.js';
10
10
 
11
+ // ---------------------------------------------------------------------------
12
+ // Language keywords
13
+ // ---------------------------------------------------------------------------
14
+
15
+ /**
16
+ * RapydScript / Python keywords that are valid identifier-completion tokens.
17
+ * Shown in the `identifier` completion context only (not dot / import contexts).
18
+ */
19
+ export const KEYWORDS = [
20
+ 'and', 'as', 'assert', 'async', 'await',
21
+ 'break', 'class', 'continue', 'def', 'del',
22
+ 'elif', 'else', 'except', 'finally', 'for',
23
+ 'from', 'global', 'if', 'import', 'in', 'is',
24
+ 'lambda', 'nonlocal', 'not', 'or', 'pass',
25
+ 'raise', 'return', 'try', 'while', 'with',
26
+ 'yield',
27
+ 'True', 'False', 'None',
28
+ ];
29
+
11
30
  // ---------------------------------------------------------------------------
12
31
  // Context detection
13
32
  // ---------------------------------------------------------------------------
@@ -26,6 +45,13 @@ import { resolve_first_type } from './dts.js';
26
45
  * @returns {{ type: string, objectName?: string, moduleName?: string, prefix: string }}
27
46
  */
28
47
  export function detect_context(linePrefix) {
48
+ // `obj.method().attr` — dot access on a call result (single-level call expression).
49
+ // Must be checked before the plain dot match since `[\w.]+` cannot span `()`.
50
+ const call_result_match = linePrefix.match(/([\w.]+)\([^)]*\)\.([\w]*)$/);
51
+ if (call_result_match) {
52
+ return { type: 'call_result', callPath: call_result_match[1], prefix: call_result_match[2] };
53
+ }
54
+
29
55
  // `obj.attr` or `obj.sub.attr` — dot access (captures multi-level path)
30
56
  const dot_match = linePrefix.match(/([\w.]+)\.([\w]*)$/);
31
57
  if (dot_match) {
@@ -246,6 +272,9 @@ export class CompletionEngine {
246
272
  getCompletions(scopeMap, position, linePrefix, monacoKind) {
247
273
  const ctx = detect_context(linePrefix);
248
274
 
275
+ if (ctx.type === 'call_result') {
276
+ return { suggestions: this._call_result_completions(position, ctx, monacoKind, scopeMap) };
277
+ }
249
278
  if (ctx.type === 'dot') {
250
279
  return { suggestions: this._dot_completions(scopeMap, position, ctx, monacoKind) };
251
280
  }
@@ -267,7 +296,25 @@ export class CompletionEngine {
267
296
  const seen = new Set();
268
297
 
269
298
  if (scopeMap) {
270
- const symbols = scopeMap.getSymbolsAtPosition(position.lineNumber, position.column);
299
+ let symbols = scopeMap.getSymbolsAtPosition(position.lineNumber, position.column);
300
+
301
+ // Fallback: cursor is on a new line not yet covered by the last debounced parse
302
+ // (e.g. the user typed Return and is now on a line past the end of the last AST).
303
+ // Collect all symbols from all frames, innermost-first, so locals remain visible.
304
+ if (symbols.length === 0 && scopeMap.frames.length > 0) {
305
+ const all_frames = scopeMap.frames.slice().sort((a, b) => b.depth - a.depth);
306
+ const fallback_seen = new Set();
307
+ symbols = [];
308
+ for (const frame of all_frames) {
309
+ for (const [name, sym] of frame.symbols) {
310
+ if (!fallback_seen.has(name)) {
311
+ fallback_seen.add(name);
312
+ symbols.push(sym);
313
+ }
314
+ }
315
+ }
316
+ }
317
+
271
318
  for (const sym of symbols) {
272
319
  if (!ctx.prefix || sym.name.startsWith(ctx.prefix)) {
273
320
  if (!seen.has(sym.name)) {
@@ -279,7 +326,7 @@ export class CompletionEngine {
279
326
  }
280
327
  }
281
328
 
282
- // Builtins (lowest priority) — use rich stub item when available
329
+ // Builtins (lower priority) — use rich stub item when available
283
330
  for (const name of this._builtinNames) {
284
331
  if (!seen.has(name) && (!ctx.prefix || name.startsWith(ctx.prefix))) {
285
332
  seen.add(name);
@@ -291,9 +338,100 @@ export class CompletionEngine {
291
338
  }
292
339
  }
293
340
 
341
+ // Keywords (lowest priority)
342
+ const kw_kind = monacoKind.Keyword !== undefined ? monacoKind.Keyword : monacoKind.Variable;
343
+ for (const kw of KEYWORDS) {
344
+ if (!seen.has(kw) && (!ctx.prefix || kw.startsWith(ctx.prefix))) {
345
+ seen.add(kw);
346
+ items.push({
347
+ label: kw,
348
+ kind: kw_kind,
349
+ sortText: '3_' + kw,
350
+ insertText: kw,
351
+ range,
352
+ });
353
+ }
354
+ }
355
+
294
356
  return items;
295
357
  }
296
358
 
359
+ // ---- Return-type helpers -----------------------------------------------
360
+
361
+ /**
362
+ * Analyze a module and return the `return_type` of a named function.
363
+ * @param {string} func_name
364
+ * @param {string} module_name
365
+ * @returns {string|null}
366
+ */
367
+ _get_return_type_from_module(func_name, module_name) {
368
+ const src = this._virtualFiles[module_name] || this._stdlibFiles[module_name];
369
+ if (!src) return null;
370
+ let mod_map;
371
+ try {
372
+ mod_map = this._analyzer.analyze(src, {});
373
+ } catch (_e) {
374
+ return null;
375
+ }
376
+ const mod_frame = mod_map.frames.find(f => f.kind === 'module');
377
+ if (!mod_frame) return null;
378
+ const fn_sym = mod_frame.getSymbol(func_name);
379
+ if (fn_sym && fn_sym.return_type) return fn_sym.return_type;
380
+ return null;
381
+ }
382
+
383
+ /**
384
+ * Resolve a type name (e.g. from inferred_class) through function return annotations,
385
+ * including cross-file imports. Returns the resolved type name, or null if unresolvable.
386
+ * @param {string} name
387
+ * @param {import('./scope.js').ScopeMap} scopeMap
388
+ * @returns {string|null}
389
+ */
390
+ _resolve_type_through_functions(name, scopeMap) {
391
+ let sym = null;
392
+ for (const frame of scopeMap.frames) {
393
+ const s = frame.getSymbol(name);
394
+ if (s) { sym = s; break; }
395
+ }
396
+ if (!sym) return null;
397
+
398
+ if (sym.kind === 'function' || sym.kind === 'method') {
399
+ // return_type is set by explicit annotation or inferred by the analyzer
400
+ if (sym.return_type) return sym.return_type;
401
+ return null;
402
+ }
403
+
404
+ if (sym.kind === 'import' && sym.source_module) {
405
+ const orig = sym.original_name || sym.name;
406
+ return this._get_return_type_from_module(orig, sym.source_module);
407
+ }
408
+
409
+ return null;
410
+ }
411
+
412
+ /**
413
+ * Get the member map for a given type name.
414
+ * Checks builtins, then user class frames. Returns null if unknown.
415
+ * @param {string} type_name
416
+ * @param {import('./scope.js').ScopeMap|null} scopeMap
417
+ * @returns {Map|null}
418
+ */
419
+ _get_members_for_type(type_name, scopeMap) {
420
+ if (!type_name) return null;
421
+ if (this._builtins) {
422
+ const m = this._builtins.getTypeMembers(type_name);
423
+ if (m) return m;
424
+ }
425
+ if (scopeMap) {
426
+ for (const frame of scopeMap.frames) {
427
+ if (frame.kind === 'class' && frame.name === type_name) {
428
+ return frame.symbols;
429
+ }
430
+ }
431
+ }
432
+ return null;
433
+ }
434
+
297
435
  // ---- Dot completions ---------------------------------------------------
298
436
 
299
437
  _dot_completions(scopeMap, position, ctx, monacoKind) {
@@ -361,6 +499,13 @@ export class CompletionEngine {
361
499
  }
362
500
  }
363
501
 
502
+ // Resolve class_name through function return type annotations.
503
+ // e.g. x = getAllServers() → inferred_class='getAllServers' → resolve 'list'
504
+ if (class_name) {
505
+ const resolved = this._resolve_type_through_functions(class_name, scopeMap);
506
+ if (resolved) class_name = resolved;
507
+ }
508
+
364
509
  if (class_name) {
365
510
  for (const frame of scopeMap.frames) {
366
511
  if (frame.kind === 'class' && frame.name === class_name) {
@@ -381,7 +526,13 @@ export class CompletionEngine {
381
526
  // 1.5. Built-in type members — list, str, dict, number.
382
527
  // Used when inferred_class names a built-in type, not a user class.
383
528
  if (!scope_matched && this._builtins && obj_sym && obj_sym.inferred_class) {
384
- const members = this._builtins.getTypeMembers(obj_sym.inferred_class);
529
+ // Re-resolve in case inferred_class itself is a function name.
530
+ let type_name = obj_sym.inferred_class;
531
+ if (scopeMap) {
532
+ const resolved = this._resolve_type_through_functions(type_name, scopeMap);
533
+ if (resolved) type_name = resolved;
534
+ }
535
+ const members = this._builtins.getTypeMembers(type_name);
385
536
  if (members) {
386
537
  scope_matched = true;
387
538
  for (const [name, member] of members) {
@@ -444,6 +595,76 @@ export class CompletionEngine {
444
595
  return items;
445
596
  }
446
597
 
598
+ // ---- Call-result dot completions (e.g. ns.self().attr) -----------------
599
+
600
+ _call_result_completions(position, ctx, monacoKind, scopeMap) {
601
+ const range = word_range(position, ctx.prefix);
602
+ const items = [];
603
+ const seen = new Set();
604
+ const call_path = ctx.callPath;
605
+ const last_dot = call_path.lastIndexOf('.');
606
+
607
+ // --- DTS path (existing logic) ---
608
+ if (this._dts) {
609
+ let member_ti = null;
610
+ if (last_dot > 0) {
611
+ const object_path = call_path.slice(0, last_dot);
612
+ const method_name = call_path.slice(last_dot + 1);
613
+ member_ti = this._dts.getMemberInfo(object_path, method_name);
614
+ } else {
615
+ member_ti = this._dts.getGlobal(call_path);
616
+ }
617
+ if (member_ti && member_ti.return_type) {
618
+ const return_ti = this._dts.getGlobal(resolve_first_type(member_ti.return_type));
619
+ if (return_ti && return_ti.members) {
620
+ for (const [name, member] of return_ti.members) {
621
+ if (!ctx.prefix || name.startsWith(ctx.prefix)) {
622
+ seen.add(name);
623
+ items.push(_dts_member_to_item(member, range, monacoKind));
624
+ }
625
+ }
626
+ }
627
+ }
628
+ }
629
+
630
+ // --- User-defined function return types (scopeMap) ---
631
+ if (scopeMap && last_dot < 0) {
632
+ // Simple call like getAllServers() — look up function in scope
633
+ let sym = null;
634
+ for (const frame of scopeMap.frames) {
635
+ const s = frame.getSymbol(call_path);
636
+ if (s) { sym = s; break; }
637
+ }
638
+ if (sym) {
639
+ let return_type = null;
640
+ if (sym.kind === 'function' || sym.kind === 'method') {
641
+ return_type = sym.return_type;
642
+ } else if (sym.kind === 'import' && sym.source_module) {
643
+ const orig = sym.original_name || sym.name;
644
+ return_type = this._get_return_type_from_module(orig, sym.source_module);
645
+ }
646
+ if (return_type) {
647
+ const members = this._get_members_for_type(return_type, scopeMap);
648
+ if (members) {
649
+ for (const [name, member] of members) {
650
+ if (!seen.has(name) && (!ctx.prefix || name.startsWith(ctx.prefix))) {
651
+ seen.add(name);
652
+ // members may be SymbolInfo (class frame) or TypeInfo/BuiltinInfo
653
+ const item = member.kind === 'function' || member.kind === 'method' ||
654
+ member.kind === 'variable' || member.kind === 'parameter'
655
+ ? symbol_to_item(member, range, monacoKind, '0')
656
+ : _dts_member_to_item(member, range, monacoKind);
657
+ items.push(item);
658
+ }
659
+ }
660
+ }
661
+ }
662
+ }
663
+ }
664
+
665
+ return items;
666
+ }
667
+
447
668
  // ---- from X import completions -----------------------------------------
448
669
 
449
670
  _from_import_completions(position, ctx, monacoKind) {
@@ -16,6 +16,7 @@ const MESSAGES = {
16
16
  'func-in-branch': 'Named functions/classes inside a branch are not allowed in strict mode',
17
17
  'syntax-err': 'Syntax error: {name}',
18
18
  'import-err': 'Import error: {name}',
19
+ 'bad-import': 'Unknown module: "{name}"',
19
20
  'def-after-use': 'The symbol "{name}" is defined (at line {line}) after it is used',
20
21
  'dup-key': 'Duplicate key "{name}" in object literal',
21
22
  'dup-method': 'The method "{name}" was defined previously at line: {line}',
@@ -27,13 +28,13 @@ export const BASE_BUILTINS = (
27
28
  ' eval undefined arguments abs max min enumerate pow callable reversed sum' +
28
29
  ' getattr isFinite setattr hasattr parseInt parseFloat options_object' +
29
30
  ' isNaN JSON Math list set list_wrap ρσ_modules require bool int bin' +
30
- ' float iter Error EvalError set_wrap RangeError ReferenceError SyntaxError' +
31
+ ' float iter Error EvalError set_wrap frozenset RangeError ReferenceError SyntaxError' +
31
32
  ' str TypeError URIError Exception AssertionError IndexError AttributeError KeyError' +
32
33
  ' ValueError ZeroDivisionError map hex filter zip dict dict_wrap UnicodeDecodeError HTMLCollection' +
33
34
  ' NodeList alert console Node Symbol NamedNodeMap ρσ_eslice ρσ_delslice Number' +
34
35
  ' Boolean encodeURIComponent decodeURIComponent setTimeout setInterval' +
35
36
  ' setImmediate clearTimeout clearInterval clearImmediate requestAnimationFrame' +
36
- ' id repr sorted __name__ equals get_module ρσ_str jstype divmod NaN super Ellipsis'
37
+ ' id repr sorted __name__ equals get_module ρσ_str jstype divmod NaN super Ellipsis slice'
37
38
  ).split(' ');
38
39
 
39
40
  // ---------------------------------------------------------------------------
@@ -44,7 +45,7 @@ function has_prop(obj, name) {
44
45
  return Object.prototype.hasOwnProperty.call(obj, name);
45
46
  }
46
47
 
47
- function build_scoped_flags(flags_str) {
48
+ export function build_scoped_flags(flags_str) {
48
49
  const result = Object.create(null);
49
50
  if (!flags_str) return result;
50
51
  flags_str.split(',').forEach(flag => {
@@ -205,7 +206,7 @@ Scope.prototype.messages = function() {
205
206
  // Linter — mirrors the Linter in tools/lint.js
206
207
  // ---------------------------------------------------------------------------
207
208
 
208
- function Linter(RS, toplevel, code, builtins) {
209
+ function Linter(RS, toplevel, code, builtins, knownModules) {
209
210
  this.RS = RS;
210
211
  this.scopes = [];
211
212
  this.walked_scopes = [];
@@ -213,6 +214,7 @@ function Linter(RS, toplevel, code, builtins) {
213
214
  this.branches = [];
214
215
  this.messages = [];
215
216
  this.builtins = builtins;
217
+ this._knownModules = knownModules || null;
216
218
 
217
219
  this.add_binding = function(name, binding_node) {
218
220
  const scope = this.scopes[this.scopes.length - 1];
@@ -240,6 +242,26 @@ function Linter(RS, toplevel, code, builtins) {
240
242
  const name = node.alias ? node.alias.name : node.key.split('.', 1)[0];
241
243
  this.add_binding(name, node.alias || node);
242
244
  }
245
+ // Flag imports of modules not present in the known-module registry.
246
+ // Only active when at least one virtualFile or stdlibFile is registered,
247
+ // to avoid false positives when no module system is configured.
248
+ if (this._knownModules) {
249
+ const root = node.key.split('.')[0];
250
+ if (!has_prop(this._knownModules, root)) {
251
+ const sline = node.start ? node.start.line : 1;
252
+ const scol = node.start ? node.start.col : 0;
253
+ this.messages.push({
254
+ start_line: sline,
255
+ start_col: scol,
256
+ end_line: sline,
257
+ end_col: scol + node.key.length - 1,
258
+ ident: 'bad-import',
259
+ message: MESSAGES['bad-import'].replace('{name}', node.key),
260
+ level: ERROR,
261
+ name: node.key,
262
+ });
263
+ }
264
+ }
243
265
  };
244
266
 
245
267
  this.handle_imported_var = function() {
@@ -312,6 +334,23 @@ function Linter(RS, toplevel, code, builtins) {
312
334
  }
313
335
  };
314
336
 
337
+ this.handle_annotated_assign = function() {
338
+ const node = this.current_node;
339
+ // Variable type annotation: `x: int = value` or `x: int`
340
+ // Register the target as a binding (with or without a value).
341
+ if (node.target instanceof RS.AST_SymbolRef) {
342
+ node.target.lint_visited = true;
343
+ this.current_node = node.target;
344
+ this.add_binding(node.target.name);
345
+ this.current_node = node;
346
+ }
347
+ // Mark annotation symbol refs as visited so they aren't treated as uses
348
+ // of undefined variables (e.g. `int`, `str` are type names, not runtime refs).
349
+ if (node.annotation instanceof RS.AST_SymbolRef) {
350
+ node.annotation.lint_visited = true;
351
+ }
352
+ };
353
+
315
354
  this.handle_vardef = function() {
316
355
  const node = this.current_node;
317
356
  if (node.name instanceof RS.AST_SymbolNonlocal) {
@@ -461,6 +500,7 @@ function Linter(RS, toplevel, code, builtins) {
461
500
  else if (node instanceof RS.AST_Assign) this.handle_assign();
462
501
  else if (node instanceof RS.AST_NamedExpr) this.handle_named_expr();
463
502
  else if (node instanceof RS.AST_VarDef) this.handle_vardef();
503
+ else if (RS.AST_AnnotatedAssign && node instanceof RS.AST_AnnotatedAssign) this.handle_annotated_assign();
464
504
  else if (node instanceof RS.AST_SymbolRef) this.handle_symbol_ref();
465
505
  else if (node instanceof RS.AST_Decorator) this.handle_decorator();
466
506
  else if (node instanceof RS.AST_SymbolFunarg) this.handle_symbol_funarg();
@@ -634,7 +674,17 @@ export class Diagnostics {
634
674
  }
635
675
 
636
676
  // --- Lint ---
637
- const linter = new Linter(RS, toplevel, code, this._builtins);
677
+ // Build known-module set for import validation.
678
+ // Only activates when the caller has registered at least one module.
679
+ let knownModules = null;
680
+ const vf = options.virtualFiles;
681
+ const sf = options.stdlibFiles;
682
+ if ((vf && Object.keys(vf).length > 0) || (sf && Object.keys(sf).length > 0)) {
683
+ knownModules = Object.create(null);
684
+ if (vf) Object.keys(vf).forEach(k => { knownModules[k] = true; });
685
+ if (sf) Object.keys(sf).forEach(k => { knownModules[k] = true; });
686
+ }
687
+ const linter = new Linter(RS, toplevel, code, this._builtins, knownModules);
638
688
  toplevel.walk(linter);
639
689
  messages = linter.resolve(noqa);
640
690
 
@@ -36,6 +36,13 @@ class ModelState {
36
36
  this._timer = null;
37
37
  this._scopeMap = null; // most recent ScopeMap for this model
38
38
 
39
+ // Derive the module name from the model URI so this model's content can
40
+ // be registered in _virtualFiles for cross-file import resolution.
41
+ // e.g. URI path "/scripts/utils.py" → module name "utils"
42
+ const uri_path = (model.uri && (model.uri.path || model.uri.fsPath)) || '';
43
+ const basename = uri_path.split('/').pop() || '';
44
+ this._module_name = basename.replace(/\.pyj?x?$/, '') || null;
45
+
39
46
  // Run immediately on first attach
40
47
  this._schedule(0);
41
48
 
@@ -53,7 +60,13 @@ class ModelState {
53
60
  _run() {
54
61
  const service = this._service;
55
62
  const code = this._model.getValue();
56
- const opts = { virtualFiles: service._virtualFiles };
63
+ const opts = { virtualFiles: service._virtualFiles, stdlibFiles: service._stdlibFiles };
64
+
65
+ // Register this model's content in the shared virtualFiles pool so other
66
+ // open models can resolve cross-file imports from it.
67
+ if (this._module_name) {
68
+ service._virtualFiles[this._module_name] = code;
69
+ }
57
70
 
58
71
  // Diagnostics (syntax errors + lint markers)
59
72
  const markers = service._diagnostics.check(code, {
@@ -69,6 +82,10 @@ class ModelState {
69
82
  dispose() {
70
83
  clearTimeout(this._timer);
71
84
  this._subscription.dispose();
85
+ // Remove this model's content from the virtualFiles pool on detach.
86
+ if (this._module_name) {
87
+ delete this._service._virtualFiles[this._module_name];
88
+ }
72
89
  // Clear markers when detaching
73
90
  this._service._monaco.editor.setModelMarkers(this._model, LANGUAGE_ID, []);
74
91
  }
@@ -108,9 +125,12 @@ class RapydScriptLanguageService {
108
125
  this._builtins = new BuiltinsRegistry();
109
126
  this._builtins.enablePythonizeStrings();
110
127
 
111
- // Merge BASE_BUILTINS + extra globals + DTS globals for completions
128
+ // Merge BASE_BUILTINS + DTS globals for completions.
129
+ // NOTE: _extraBuiltinNames (NS API keys like grow, growthAnalyze, etc.) are
130
+ // intentionally excluded here — they are handled by the DTS registry as dot-
131
+ // completions on ns.* and should NOT appear as free-floating identifier
132
+ // suggestions. They are still used for tokenization and diagnostics suppression.
112
133
  const builtin_names = BASE_BUILTINS
113
- .concat(this._extraBuiltinNames)
114
134
  .concat(this._dts.getGlobalNames());
115
135
 
116
136
  this._completions = new CompletionEngine(this._analyzer, {
@@ -318,8 +338,9 @@ class RapydScriptLanguageService {
318
338
  const new_names = this._dts.getGlobalNames();
319
339
  // Suppress undef warnings for newly added globals
320
340
  this._diagnostics.addGlobals(new_names);
321
- // Rebuild builtin list so completions include new DTS names (preserve extraBuiltins)
322
- const builtin_names = BASE_BUILTINS.concat(this._extraBuiltinNames).concat(new_names);
341
+ // Rebuild builtin list so completions include new DTS names.
342
+ // _extraBuiltinNames are excluded — they are dot-completions only (see constructor).
343
+ const builtin_names = BASE_BUILTINS.concat(new_names);
323
344
  this._completions = new CompletionEngine(this._analyzer, {
324
345
  virtualFiles: this._virtualFiles,
325
346
  stdlibFiles: this._stdlibFiles,
@@ -30,6 +30,9 @@ export class SymbolInfo {
30
30
  this.inferred_class = opts.inferred_class || null; // 'ClassName' when x = ClassName(...)
31
31
  this.dts_call_path = opts.dts_call_path || null; // 'ns.getServer' when x = ns.getServer(...)
32
32
  this.type = null; // Phase 6: TypeInfo
33
+ this.return_type = opts.return_type || null; // annotated return type, e.g. 'list', 'str', 'MyClass'
34
+ this.source_module = opts.source_module || null; // module name when kind='import' (from X import Y)
35
+ this.original_name = opts.original_name || null; // pre-alias name for imports (from X import Y as Z → 'Y')
33
36
  }
34
37
  }
35
38
 
@@ -214,7 +214,7 @@ def print_class(output):
214
214
  output.end_statement()
215
215
 
216
216
  elif is_node_type(stmt, AST_Class):
217
- console.error('Nested classes aren\'t supported yet') # noqa:undef
217
+ pass # nested classes are emitted in the statements section below
218
218
 
219
219
  if not defined_methods['__repr__']:
220
220
  define_default_method('__repr__', def():
@@ -271,9 +271,26 @@ def print_class(output):
271
271
  output.print(JSON.stringify(create_doctring(self.docstrings)))
272
272
  )
273
273
 
274
- # Other statements in the class context
274
+ # Other statements in the class context (including nested class definitions)
275
275
  for stmt in self.statements:
276
- if not is_node_type(stmt, AST_Method):
276
+ if is_node_type(stmt, AST_Class):
277
+ # Print the nested class in full, then attach it to the outer class.
278
+ # Two assignments mirror Python semantics:
279
+ # Outer.Inner — class-level access (Outer.Inner())
280
+ # Outer.prototype.Inner — instance-level access (self.Inner() inside methods)
281
+ output.indent()
282
+ stmt.print(output)
283
+ output.newline()
284
+ nested_name = stmt.name.name
285
+ output.indent()
286
+ self.name.print(output)
287
+ output.print('.' + nested_name + ' = ' + nested_name)
288
+ output.end_statement()
289
+ output.indent()
290
+ self.name.print(output)
291
+ output.print('.prototype.' + nested_name + ' = ' + nested_name)
292
+ output.end_statement()
293
+ elif not is_node_type(stmt, AST_Method):
277
294
  output.indent()
278
295
  stmt.print(output)
279
296
  output.newline()
@@ -2,7 +2,7 @@
2
2
  # License: BSD Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
3
3
  from __python__ import hash_literals
4
4
 
5
- # globals:console,regenerate,writefile
5
+ # globals:console,writefile
6
6
 
7
7
  from utils import noop
8
8
  from parse import PRECEDENCE
@@ -279,7 +279,28 @@ def generate_code():
279
279
  self._do_print(output, "return")
280
280
  )
281
281
  DEFPRINT(AST_Throw, def(self, output):
282
- self._do_print(output, "throw")
282
+ if self.cause:
283
+ # raise X from Y: set __cause__ on exception before throwing
284
+ output.print('var ρσ_throw_tmp')
285
+ output.space()
286
+ output.print('=')
287
+ output.space()
288
+ self.value.print(output)
289
+ output.semicolon()
290
+ output.newline()
291
+ output.indent()
292
+ output.print('ρσ_throw_tmp.__cause__')
293
+ output.space()
294
+ output.print('=')
295
+ output.space()
296
+ self.cause.print(output)
297
+ output.semicolon()
298
+ output.newline()
299
+ output.indent()
300
+ output.print('throw ρσ_throw_tmp')
301
+ output.semicolon()
302
+ else:
303
+ self._do_print(output, "throw")
283
304
  )
284
305
 
285
306
  # -----[ loop control ]-----
@@ -292,6 +313,17 @@ def generate_code():
292
313
  output.semicolon()
293
314
 
294
315
  DEFPRINT(AST_Break, def(self, output):
316
+ # If we are directly inside a for/else labeled block and this break has
317
+ # no explicit label, redirect it to the forelse label so the else clause
318
+ # is skipped.
319
+ if not self.label and output.forelse_stack and output.forelse_stack.length:
320
+ top = output.forelse_stack[output.forelse_stack.length - 1]
321
+ if top:
322
+ output.print('break')
323
+ output.space()
324
+ output.print(top)
325
+ output.semicolon()
326
+ return
295
327
  self._do_print(output, "break")
296
328
  )
297
329
  DEFPRINT(AST_Continue, def(self, output):
@@ -340,7 +372,10 @@ def generate_code():
340
372
  DEFPRINT(AST_If, def(self, output):
341
373
  output.print("if")
342
374
  output.space()
343
- output.with_parens(def(): self.condition.print(output);)
375
+ if self.python_truthiness:
376
+ output.with_parens(def(): output.print('ρσ_bool('), self.condition.print(output), output.print(')');)
377
+ else:
378
+ output.with_parens(def(): self.condition.print(output);)
344
379
  output.space()
345
380
  if self.alternative:
346
381
  make_then(self, output)