rapydscript-ng 0.7.24 → 0.8.1

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 (132) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +80 -26
  3. package/bin/build.ts +117 -0
  4. package/bin/package.json +1 -0
  5. package/bin/rapydscript +65 -62
  6. package/build_wheels.py +133 -0
  7. package/editor-plugins/README.md +80 -0
  8. package/editor-plugins/nvim.lua +321 -0
  9. package/local-agent.md +28 -0
  10. package/package.json +4 -5
  11. package/publish.py +27 -17
  12. package/release/baselib-plain-pretty.js +448 -326
  13. package/release/baselib-plain-ugly.js +3 -3
  14. package/release/compiler.js +9027 -8139
  15. package/release/signatures.json +27 -27
  16. package/release/stdlib_modules.json +1 -0
  17. package/src/ast.pyj +428 -280
  18. package/src/baselib-builtins.pyj +49 -28
  19. package/src/baselib-containers.pyj +241 -226
  20. package/src/baselib-errors.pyj +8 -1
  21. package/src/baselib-internal.pyj +38 -23
  22. package/src/baselib-itertools.pyj +13 -7
  23. package/src/baselib-str.pyj +59 -84
  24. package/src/compiler.pyj +4 -4
  25. package/src/errors.pyj +3 -4
  26. package/src/lib/aes.pyj +46 -32
  27. package/src/lib/elementmaker.pyj +11 -9
  28. package/src/lib/encodings.pyj +15 -5
  29. package/src/lib/gettext.pyj +9 -4
  30. package/src/lib/math.pyj +106 -45
  31. package/src/lib/operator.pyj +10 -10
  32. package/src/lib/pythonize.pyj +2 -1
  33. package/src/lib/random.pyj +28 -21
  34. package/src/lib/re.pyj +492 -19
  35. package/src/lib/traceback.pyj +171 -29
  36. package/src/lib/uuid.pyj +2 -2
  37. package/src/output/classes.pyj +28 -27
  38. package/src/output/codegen.pyj +87 -83
  39. package/src/output/comments.pyj +8 -8
  40. package/src/output/exceptions.pyj +20 -21
  41. package/src/output/functions.pyj +66 -64
  42. package/src/output/literals.pyj +24 -23
  43. package/src/output/loops.pyj +84 -142
  44. package/src/output/modules.pyj +55 -68
  45. package/src/output/operators.pyj +40 -29
  46. package/src/output/statements.pyj +21 -15
  47. package/src/output/stream.pyj +82 -56
  48. package/src/output/utils.pyj +12 -8
  49. package/src/parse.pyj +971 -620
  50. package/src/string_interpolation.pyj +5 -3
  51. package/src/tokenizer.pyj +188 -148
  52. package/src/utils.pyj +32 -15
  53. package/test/_treeshake_mod.pyj +30 -0
  54. package/test/_treeshake_side_effect.pyj +9 -0
  55. package/test/ast_serialization.pyj +392 -0
  56. package/test/async.pyj +95 -0
  57. package/test/baselib.pyj +1 -2
  58. package/test/embedded_compiler.pyj +57 -0
  59. package/test/fmt.pyj +291 -0
  60. package/test/generic.pyj +16 -3
  61. package/test/imports.pyj +8 -6
  62. package/test/internationalization.pyj +38 -37
  63. package/test/lint.pyj +120 -117
  64. package/test/lsp.pyj +257 -0
  65. package/test/repl.pyj +70 -65
  66. package/test/sourcemaps.pyj +315 -0
  67. package/test/starargs.pyj +46 -39
  68. package/test/str.pyj +8 -0
  69. package/test/traceback.pyj +263 -0
  70. package/test/treeshaking.pyj +181 -0
  71. package/test/web_repl_export.pyj +88 -0
  72. package/tools/ast_serialize.mjs +557 -0
  73. package/tools/cli.mjs +531 -0
  74. package/tools/compile.mjs +205 -0
  75. package/tools/compiler.mjs +159 -0
  76. package/tools/{completer.js → completer.mjs} +6 -6
  77. package/tools/{embedded_compiler.js → embedded_compiler.mjs} +17 -13
  78. package/tools/export.js +109 -56
  79. package/tools/fmt.mjs +975 -0
  80. package/tools/{gettext.js → gettext.mjs} +46 -53
  81. package/tools/ini.mjs +175 -0
  82. package/tools/{lint.js → lint.mjs} +78 -55
  83. package/tools/lsp.mjs +964 -0
  84. package/tools/lsp_protocol.mjs +259 -0
  85. package/tools/lsp_symbols.mjs +418 -0
  86. package/tools/{msgfmt.js → msgfmt.mjs} +18 -18
  87. package/tools/{repl.js → repl.mjs} +56 -42
  88. package/tools/{self.js → self.mjs} +71 -46
  89. package/tools/sourcemap.mjs +123 -0
  90. package/tools/test.mjs +252 -0
  91. package/tools/treeshake.mjs +400 -0
  92. package/tools/{utils.js → utils.mjs} +26 -23
  93. package/tools/{web_repl.js → web_repl.mjs} +15 -13
  94. package/tools/web_repl_export.mjs +97 -0
  95. package/tree-sitter/README.md +101 -0
  96. package/tree-sitter/grammar.js +992 -0
  97. package/tree-sitter/package.json +43 -0
  98. package/tree-sitter/queries/highlights.scm +232 -0
  99. package/tree-sitter/queries/indents.scm +64 -0
  100. package/tree-sitter/queries/injections.scm +14 -0
  101. package/tree-sitter/queries/locals.scm +108 -0
  102. package/tree-sitter/src/grammar.json +4976 -0
  103. package/tree-sitter/src/node-types.json +2901 -0
  104. package/tree-sitter/src/parser.c +196465 -0
  105. package/tree-sitter/src/scanner.c +294 -0
  106. package/tree-sitter/src/tree_sitter/alloc.h +54 -0
  107. package/tree-sitter/src/tree_sitter/array.h +330 -0
  108. package/tree-sitter/src/tree_sitter/parser.h +286 -0
  109. package/tree-sitter/test/corpus/chaining.txt +99 -0
  110. package/tree-sitter/test/corpus/classes.txt +147 -0
  111. package/tree-sitter/test/corpus/comprehensions.txt +155 -0
  112. package/tree-sitter/test/corpus/containers.txt +242 -0
  113. package/tree-sitter/test/corpus/control_flow.txt +361 -0
  114. package/tree-sitter/test/corpus/decorators.txt +64 -0
  115. package/tree-sitter/test/corpus/existential.txt +102 -0
  116. package/tree-sitter/test/corpus/functions.txt +293 -0
  117. package/tree-sitter/test/corpus/imports.txt +117 -0
  118. package/tree-sitter/test/corpus/literals.txt +135 -0
  119. package/tree-sitter/test/corpus/operators.txt +296 -0
  120. package/tree-sitter/test/corpus/statements.txt +189 -0
  121. package/tree-sitter/test/corpus/strings.txt +90 -0
  122. package/tree-sitter/test/corpus/subscripts.txt +227 -0
  123. package/tree-sitter/tree-sitter.json +36 -0
  124. package/web-repl/env.js +35 -23
  125. package/web-repl/main.js +8 -8
  126. package/bin/export +0 -75
  127. package/bin/web-repl-export +0 -102
  128. package/tools/cli.js +0 -523
  129. package/tools/compile.js +0 -184
  130. package/tools/compiler.js +0 -84
  131. package/tools/ini.js +0 -65
  132. package/tools/test.js +0 -110
package/tools/test.mjs ADDED
@@ -0,0 +1,252 @@
1
+ /*
2
+ * test.js
3
+ * Copyright (C) 2015 Kovid Goyal <kovid at kovidgoyal.net>
4
+ *
5
+ * Distributed under terms of the BSD license.
6
+ */
7
+
8
+ import path from 'path';
9
+ import fs from 'fs';
10
+ import assert from 'assert';
11
+ import os from 'os';
12
+ import vm from 'vm';
13
+ import { createRequire } from 'module';
14
+ import { create_compiler } from './compiler.mjs';
15
+ import embedded_compiler_factory from './embedded_compiler.mjs';
16
+ import * as utils from './utils.mjs';
17
+ import { gettext as rs_gettext, entry_to_string as rs_entry_to_string } from './gettext.mjs';
18
+ import { parse as rs_msgfmt_parse, build as rs_msgfmt_build } from './msgfmt.mjs';
19
+ import rs_repl_fn from './repl.mjs';
20
+ import { generate_source_map as rs_generate_source_map } from './sourcemap.mjs';
21
+
22
+ const require = createRequire(import.meta.url);
23
+ const RapydScript = await create_compiler();
24
+ var colored = utils.safe_colored;
25
+
26
+ export default async function(argv, base_path, src_path, lib_path) {
27
+ // run all tests and exit
28
+ var failures = [];
29
+ var compiler_dir = path.join(base_path, 'dev');
30
+ if (!await utils.path_exists(path.join(compiler_dir, 'compiler.js'))) compiler_dir = path.join(base_path, 'release');
31
+ var test_dir = path.join(base_path, 'test');
32
+ var baselib = await fs.promises.readFile(path.join(lib_path, 'baselib-plain-pretty.js'), 'utf-8');
33
+ var files;
34
+ var deep_eq = assert.deepEqual;
35
+ assert.deepEqual = function(a, b, message) {
36
+ // Compare array objects that have extra properties as simple arrays
37
+ if (Array.isArray(a) && Array.isArray(b)) {
38
+ if (a === b) return;
39
+ if (a.length !== b.length) throw new assert.AssertionError({actual:a, expected:b, operator:'deepEqual', stackStartFunction:assert.deepEqual});
40
+ for(var i=0; i < a.length; i++) assert.deepEqual(a[i], b[i], message);
41
+ } else if (a !== undefined && a !== null && typeof a.__eq__ === 'function') {
42
+ if (!a.__eq__(b)) throw new assert.AssertionError({actual:a, expected:b, operator:'deepEqual', stackStartFunction:assert.deepEqual});
43
+ } else return deep_eq(a, b, message);
44
+ };
45
+
46
+ if (argv.files.length) {
47
+ files = argv.files.map(fname => fname + '.pyj');
48
+ } else {
49
+ files = (await fs.promises.readdir(test_dir)).filter(function(name){
50
+ return /^[^_].*\.pyj$/.test(name);
51
+ });
52
+ }
53
+
54
+ for (const file of files) {
55
+ let filepath = path.join(test_dir, file);
56
+ let failed = false;
57
+ let src;
58
+ let ast;
59
+ try {
60
+ src = await fs.promises.readFile(filepath, "utf-8");
61
+ ast = await RapydScript.parse(src, {
62
+ filename: file,
63
+ toplevel: ast,
64
+ basedir: test_dir,
65
+ libdir: path.join(src_path, 'lib'),
66
+ });
67
+ } catch(e) {
68
+ failures.push(file);
69
+ failed = true;
70
+ console.log(colored(file, 'red') + ': ' + e + "\n\n");
71
+ continue;
72
+ }
73
+
74
+ // generate output
75
+ var output = new RapydScript.OutputStream({
76
+ baselib_plain: baselib,
77
+ beautify: true,
78
+ js_version: 6,
79
+ keep_docstrings: true,
80
+ });
81
+ ast.print(output);
82
+
83
+ // test that output performs correct JS operations
84
+ var jsfile = path.join(os.tmpdir(), file + '-es6.js');
85
+ var code = output.toString();
86
+ try {
87
+ const unhandled = [];
88
+ const on_rejection = (reason) => unhandled.push(reason);
89
+ process.on('unhandledRejection', on_rejection);
90
+ try {
91
+ // __test_async_done__ is a sandbox global that async test files can assign
92
+ // their top-level Promise to (e.g. `__test_async_done__ = run_tests()`).
93
+ // Assigning to an existing global inside a strict-mode IIFE is legal, so
94
+ // this lets us properly await tests whose async Promise is otherwise buried
95
+ // inside a synchronous wrapper IIFE and never returned from the script.
96
+ const sandbox = {
97
+ 'assrt':assert,
98
+ '__name__': jsfile,
99
+ 'require':require,
100
+ 'fs':fs,
101
+ 'RapydScript':RapydScript,
102
+ 'console':console,
103
+ 'compiler_dir': compiler_dir,
104
+ 'test_path':test_dir,
105
+ 'Buffer': Buffer,
106
+ 'rs_gettext': rs_gettext,
107
+ 'rs_entry_to_string': rs_entry_to_string,
108
+ 'rs_msgfmt': { parse: rs_msgfmt_parse, build: rs_msgfmt_build },
109
+ 'rs_repl': rs_repl_fn,
110
+ 'rs_generate_source_map': rs_generate_source_map,
111
+ 'rs_create_embedded_compiler': async function(opts) {
112
+ // Mirror what the browser bundle does: compiler + embedded baselib → factory.
113
+ // Tests may pass { virtual_file_system } to override readfile/writefile.
114
+ var compiler = await create_compiler(opts);
115
+ return await embedded_compiler_factory(compiler, baselib, undefined, undefined, undefined, undefined);
116
+ },
117
+ '__test_async_done__': null,
118
+ };
119
+ let result = vm.runInNewContext(code, sandbox, {'filename':jsfile});
120
+ if (result && typeof result.then === 'function') {
121
+ await result;
122
+ }
123
+ // Await any async Promise that the test file registered via __test_async_done__.
124
+ if (sandbox.__test_async_done__ && typeof sandbox.__test_async_done__.then === 'function') {
125
+ await sandbox.__test_async_done__;
126
+ }
127
+ // Drain one event-loop tick so Node.js can fire unhandledRejection
128
+ // for any async work that was not captured by either mechanism above.
129
+ await new Promise(resolve => setImmediate(resolve));
130
+ if (unhandled.length) throw unhandled[0];
131
+ } finally {
132
+ process.removeListener('unhandledRejection', on_rejection);
133
+ }
134
+ } catch (e) {
135
+ failures.push(file);
136
+ failed = true;
137
+ await fs.promises.writeFile(jsfile, code);
138
+ console.error('Failed running: ' + colored(jsfile, 'red'));
139
+ if (e.stack)
140
+ console.error(colored(file, 'red') + ":\n" + e.stack + "\n\n");
141
+ else
142
+ console.error(colored(file, 'red') + ": " + e + "\n\n");
143
+ }
144
+ if (!failed) console.log(colored(file, 'green') + ": test completed successfully\n");
145
+ else { console.log(colored(file, 'red') + ":\ttest failed\n"); }
146
+ }
147
+ // Run the bun standalone binary test when doing a full test suite run and bun
148
+ // is available. Individual file runs (argv.files.length > 0) skip it.
149
+ if (!argv.files.length) {
150
+ const bun_test_name = 'bun_standalone_binary';
151
+ let bun_ok = false;
152
+ try {
153
+ bun_ok = await run_bun_standalone_test(base_path, colored);
154
+ } catch (e) {
155
+ console.error(colored(bun_test_name, 'red') + ': unexpected error: ' + (e.stack || e));
156
+ }
157
+ if (bun_ok) {
158
+ console.log(colored(bun_test_name, 'green') + ': test completed successfully\n');
159
+ } else {
160
+ failures.push(bun_test_name);
161
+ console.log(colored(bun_test_name, 'red') + ':\ttest failed\n');
162
+ }
163
+ }
164
+
165
+ if (failures.length) {
166
+ console.log(colored('There were ' + failures.length + ' test failure(s):', 'red'));
167
+ console.log.apply(console, failures);
168
+ } else console.log(colored('All tests passed!', 'green'));
169
+ process.exit((failures.length) ? 1 : 0);
170
+ }
171
+
172
+ async function run_bun_standalone_test(base_path, colored) {
173
+ const { spawnSync } = await import('child_process');
174
+ const test_name = 'bun_standalone_binary';
175
+
176
+ // Check if bun is available.
177
+ const bun_check = spawnSync('bun', ['--version'], { encoding: 'utf-8' });
178
+ if (bun_check.error || bun_check.status !== 0) {
179
+ console.log(colored(test_name, 'yellow') + ': bun not found, skipping');
180
+ return true;
181
+ }
182
+
183
+ // Generate bin/rapydscript.mjs via build.ts.
184
+ const build_ts = path.join(base_path, 'bin', 'build.ts');
185
+ const gen_result = spawnSync('bun', [build_ts], { encoding: 'utf-8', cwd: base_path });
186
+ if (gen_result.status !== 0) {
187
+ console.error(colored(test_name, 'red') + ': build.ts failed:\n' + (gen_result.stderr || gen_result.stdout));
188
+ return false;
189
+ }
190
+
191
+ // Compile the standalone binary.
192
+ const binary_path = path.join(os.tmpdir(), 'rapydscript-bun-test-binary');
193
+ const entry_mjs = path.join(base_path, 'bin', 'rapydscript.mjs');
194
+ const compile_result = spawnSync(
195
+ 'bun', ['build', entry_mjs, '--compile', '--outfile', binary_path],
196
+ { encoding: 'utf-8', cwd: base_path }
197
+ );
198
+ if (compile_result.status !== 0) {
199
+ console.error(colored(test_name, 'red') + ': bun build --compile failed:\n' + (compile_result.stderr || compile_result.stdout));
200
+ return false;
201
+ }
202
+
203
+ // Create test fixtures in a temp directory.
204
+ const tmp_dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rs-bun-test-'));
205
+ try {
206
+ // A local module the test will import from the filesystem.
207
+ await fs.promises.writeFile(path.join(tmp_dir, 'greeter.pyj'),
208
+ 'def greet(name):\n return "Hello, " + name\n');
209
+
210
+ // Main source: imports a stdlib module (re) AND the local module.
211
+ const src_file = path.join(tmp_dir, 'main.pyj');
212
+ await fs.promises.writeFile(src_file, [
213
+ 'import re',
214
+ 'from greeter import greet',
215
+ '',
216
+ 'pattern = re.compile(r"world", re.I)',
217
+ 'result = pattern.sub("RapydScript", "Hello world")',
218
+ 'if result != "Hello RapydScript":',
219
+ ' raise AssertionError("re.sub gave: " + result)',
220
+ '',
221
+ 'msg = greet("world")',
222
+ 'if msg != "Hello, world":',
223
+ ' raise AssertionError("greet gave: " + msg)',
224
+ ].join('\n'));
225
+
226
+ const out_file = path.join(tmp_dir, 'main.js');
227
+
228
+ // Compile main.pyj with the standalone binary.
229
+ const run_result = spawnSync(
230
+ binary_path, [src_file, '--output', out_file],
231
+ { encoding: 'utf-8', cwd: tmp_dir }
232
+ );
233
+ if (run_result.status !== 0) {
234
+ console.error(colored(test_name, 'red') + ': standalone binary compilation failed:\n' +
235
+ (run_result.stderr || run_result.stdout));
236
+ return false;
237
+ }
238
+
239
+ // Execute the compiled output with node to verify correctness.
240
+ const js_code = await fs.promises.readFile(out_file, 'utf-8');
241
+ try {
242
+ vm.runInNewContext(js_code, { console }, { filename: out_file });
243
+ } catch (e) {
244
+ console.error(colored(test_name, 'red') + ': compiled output threw:\n' + (e.stack || e));
245
+ return false;
246
+ }
247
+
248
+ return true;
249
+ } finally {
250
+ await fs.promises.rm(tmp_dir, { recursive: true, force: true });
251
+ }
252
+ }
@@ -0,0 +1,400 @@
1
+ /*
2
+ * treeshake.js - Dead code elimination for RapydScript
3
+ * Copyright (C) 2024 Kovid Goyal <kovid at kovidgoyal.net>
4
+ *
5
+ * Distributed under terms of the BSD license.
6
+ */
7
+
8
+ var has_prop = Object.prototype.hasOwnProperty.call.bind(Object.prototype.hasOwnProperty);
9
+
10
+ // Node-type check via constructor name. Works across Node.js VM context
11
+ // boundaries because it relies on string comparison rather than instanceof.
12
+ function node_type(node) {
13
+ return node && node.constructor && node.constructor.name;
14
+ }
15
+ function is_function(node) { return node_type(node) === 'AST_Function'; }
16
+ function is_class(node) { return node_type(node) === 'AST_Class'; }
17
+ function is_imports(node) { return node_type(node) === 'AST_Imports'; }
18
+ function is_symref(node) { return node_type(node) === 'AST_SymbolRef'; }
19
+
20
+ // Returns true when a decorator node is the special @no_prune annotation.
21
+ // @no_prune is a tree-shaking annotation, not a runtime function — it pins the
22
+ // decorated symbol so the shaker never removes it, and is stripped from output.
23
+ function is_no_prune_decorator(dec) {
24
+ return dec && dec.expression && is_symref(dec.expression) &&
25
+ dec.expression.name === 'no_prune';
26
+ }
27
+
28
+ // Returns true when stmt (an AST_Function or AST_Class) carries @no_prune.
29
+ function node_is_pinned(stmt) {
30
+ if (!stmt.decorators || !stmt.decorators.length) return false;
31
+ for (var i = 0; i < stmt.decorators.length; i++) {
32
+ if (is_no_prune_decorator(stmt.decorators[i])) return true;
33
+ }
34
+ return false;
35
+ }
36
+
37
+ // Returns true when a module's immediately-executing body contains at least one
38
+ // function call — a conservative signal that the module has externally
39
+ // observable side effects (e.g. patching String.prototype).
40
+ function has_side_effect_calls(mod) {
41
+ if (!mod) return false;
42
+ // Use pre-computed flag if available (avoids body access for lazy modules).
43
+ if (typeof mod._has_side_effects === 'boolean') return mod._has_side_effects;
44
+ if (!mod.body) return false;
45
+ var found = false;
46
+ mod.body.forEach(function(stmt) {
47
+ if (found) return;
48
+ if (is_function(stmt) || is_class(stmt) || is_imports(stmt)) return;
49
+ var walker = new TreeWalker(function(n) {
50
+ if (node_type(n) === 'AST_Call') { found = true; return true; }
51
+ });
52
+ stmt.walk(walker);
53
+ });
54
+ return found;
55
+ }
56
+
57
+ // Minimal TreeWalker compatible with the AST's _walk() protocol.
58
+ // If the callback returns truthy, descend is suppressed.
59
+ function noop() {}
60
+ function TreeWalker(callback) { this.visit = callback; }
61
+ TreeWalker.prototype._visit = function(node, descend) {
62
+ var ret = this.visit(node, descend || noop);
63
+ if (!ret && descend) descend.call(node);
64
+ return ret;
65
+ };
66
+
67
+ /*
68
+ * tree_shake(toplevel)
69
+ *
70
+ * Performs dead code elimination on the parsed AST.
71
+ *
72
+ * Removes:
73
+ * - Unused top-level function and class definitions from all modules
74
+ * - Import statements for modules that are completely unused
75
+ * - Unused argnames from "from X import a, b" statements
76
+ *
77
+ * A definition is "live" if it is transitively reachable from any
78
+ * immediately-executing (non-def/class/import) top-level statement,
79
+ * starting from __main__ and following import references.
80
+ *
81
+ * Modifies the AST in-place and returns the modified toplevel.
82
+ *
83
+ * Cached modules (is_cached=true, empty body): their pre-built IIFE output
84
+ * cannot be modified internally, but the entire module can still be removed.
85
+ */
86
+ function tree_shake(toplevel) {
87
+
88
+ // ── 1. Collect all modules ────────────────────────────────────────────
89
+ var all_modules = {};
90
+ all_modules['__main__'] = toplevel;
91
+ Object.keys(toplevel.imports).forEach(function(mid) {
92
+ all_modules[mid] = toplevel.imports[mid];
93
+ });
94
+
95
+ // ── 2. Per-module index tables (built lazily when a module is first marked live) ──
96
+ //
97
+ // top_defs[mid] : {name -> ast_node} (top-level defs only)
98
+ // import_bindings[mid] : {local_name -> {source_module, imported_name, is_namespace}}
99
+ //
100
+ // Building lazily means dead modules' bodies are never accessed, enabling
101
+ // lazy AST deserialization: ast_from_json() is skipped for dead modules.
102
+
103
+ var top_defs = {};
104
+ var import_bindings = {};
105
+ // pinned_defs[mid][name] = true when the def carries @no_prune
106
+ var pinned_defs = {};
107
+
108
+ function ensure_module_indexed(mid) {
109
+ if (has_prop(top_defs, mid)) return; // already indexed
110
+ top_defs[mid] = {};
111
+ import_bindings[mid] = {};
112
+ pinned_defs[mid] = {};
113
+ var mod = all_modules[mid];
114
+ if (!mod) return;
115
+
116
+ // Use precomputed index when available (v7+ caches): avoids body access,
117
+ // so ast_from_json is not triggered during the fixpoint iteration.
118
+ var idx = mod._cache_index;
119
+ if (idx) {
120
+ Object.keys(idx.top_defs || {}).forEach(function(name) {
121
+ var di = idx.top_defs[name];
122
+ // Store a plain index entry ({_idx:true, refs:[...]}) so mark_def_live
123
+ // can push refs directly without needing the AST node.
124
+ top_defs[mid][name] = {_idx: true, refs: di.refs || []};
125
+ if (di.pinned) pinned_defs[mid][name] = true;
126
+ });
127
+ Object.keys(idx.import_bindings || {}).forEach(function(local) {
128
+ var b = idx.import_bindings[local];
129
+ import_bindings[mid][local] = {
130
+ source_module: b.source,
131
+ imported_name: b.imported_name,
132
+ is_namespace: b.is_namespace,
133
+ };
134
+ });
135
+ return;
136
+ }
137
+
138
+ if (!mod.body) return;
139
+ mod.body.forEach(function(stmt) {
140
+ if ((is_function(stmt) || is_class(stmt)) && stmt.name) {
141
+ top_defs[mid][stmt.name.name] = stmt;
142
+ if (node_is_pinned(stmt)) {
143
+ pinned_defs[mid][stmt.name.name] = true;
144
+ }
145
+ } else if (is_imports(stmt)) {
146
+ stmt.imports.forEach(function(imp) {
147
+ if (imp.argnames && imp.argnames.length) {
148
+ // from X import a [as b], c [as d], …
149
+ imp.argnames.forEach(function(arg) {
150
+ var local = arg.alias ? arg.alias.name : arg.name;
151
+ import_bindings[mid][local] = {
152
+ source_module: imp.key,
153
+ imported_name: arg.name,
154
+ is_namespace: false,
155
+ };
156
+ });
157
+ } else {
158
+ // import X [as Y] or import X.Y [as Z]
159
+ var local;
160
+ if (imp.alias) {
161
+ local = imp.alias.name;
162
+ } else {
163
+ local = imp.key.split('.')[0];
164
+ }
165
+ import_bindings[mid][local] = {
166
+ source_module: imp.key,
167
+ imported_name: null,
168
+ is_namespace: true,
169
+ };
170
+ }
171
+ });
172
+ }
173
+ });
174
+ }
175
+
176
+ // ── 3. Liveness tracking ─────────────────────────────────────────────
177
+ var live_modules = {}; // {mid -> true}
178
+ var live_defs = {}; // {mid -> {def_name -> true}} (initialised lazily)
179
+ var used_bindings = {}; // {mid -> {local_name -> true}} (initialised lazily)
180
+
181
+ var work_queue = [];
182
+
183
+ // Collect all AST_SymbolRef names reachable from `node`.
184
+ // Reuse a single TreeWalker (JS is single-threaded; no re-entrancy risk).
185
+ var _collect_refs_target = null;
186
+ var _collect_refs_walker = new TreeWalker(function(n) {
187
+ if (is_symref(n)) _collect_refs_target[n.name] = true;
188
+ });
189
+ function collect_refs(node, refs) {
190
+ _collect_refs_target = refs;
191
+ node.walk(_collect_refs_walker);
192
+ _collect_refs_target = null;
193
+ }
194
+
195
+ // Mark a module as live; seed its immediately-executing code into the queue
196
+ function mark_module_live(mod_id) {
197
+ if (live_modules[mod_id]) return;
198
+ live_modules[mod_id] = true;
199
+ if (!live_defs[mod_id]) live_defs[mod_id] = {};
200
+ if (!used_bindings[mod_id]) used_bindings[mod_id] = {};
201
+ ensure_module_indexed(mod_id);
202
+ var mod = all_modules[mod_id];
203
+ if (!mod) return;
204
+ // @no_prune defs are unconditionally live whenever their module is live
205
+ Object.keys(pinned_defs[mod_id]).forEach(function(name) {
206
+ mark_def_live(mod_id, name);
207
+ });
208
+
209
+ // Fast path: use precomputed index — no body access, no AST deserialization.
210
+ var idx = mod._cache_index;
211
+ if (idx) {
212
+ if (idx.exec_refs && idx.exec_refs.length) {
213
+ work_queue.push({mod_id: mod_id, refs: idx.exec_refs});
214
+ }
215
+ // Check namespace imports for side-effect-bearing modules.
216
+ // has_side_effect_calls uses mod._has_side_effects for v6+ modules,
217
+ // so this never triggers body access on cached modules.
218
+ Object.keys(import_bindings[mod_id]).forEach(function(local) {
219
+ var b = import_bindings[mod_id][local];
220
+ if (!b.is_namespace) return;
221
+ var parts = b.source_module.split('.');
222
+ var partial = '';
223
+ for (var p = 0; p < parts.length; p++) {
224
+ partial = (p === 0) ? parts[0] : partial + '.' + parts[p];
225
+ if (has_side_effect_calls(all_modules[partial])) {
226
+ mark_module_live(partial);
227
+ }
228
+ }
229
+ });
230
+ return;
231
+ }
232
+
233
+ if (!mod.body) return;
234
+ mod.body.forEach(function(stmt) {
235
+ if (!is_function(stmt) && !is_class(stmt) && !is_imports(stmt)) {
236
+ work_queue.push({mod_id: mod_id, node: stmt});
237
+ } else if (is_imports(stmt)) {
238
+ // "import X" always executes X's module code. When X has
239
+ // immediately-executing function calls that may have globally
240
+ // observable side effects (e.g. patching String.prototype),
241
+ // mark X live even if its local name is never referenced.
242
+ stmt.imports.forEach(function(imp) {
243
+ if (imp.argnames && imp.argnames.length) return;
244
+ var parts = imp.key.split('.');
245
+ var partial = '';
246
+ for (var p = 0; p < parts.length; p++) {
247
+ partial = (p === 0) ? parts[0] : partial + '.' + parts[p];
248
+ if (has_side_effect_calls(all_modules[partial])) {
249
+ mark_module_live(partial);
250
+ }
251
+ }
252
+ });
253
+ }
254
+ });
255
+ }
256
+
257
+ // Mark a top-level def as live; enqueue its body for scanning
258
+ function mark_def_live(mod_id, def_name) {
259
+ if (!live_defs[mod_id]) live_defs[mod_id] = {};
260
+ if (live_defs[mod_id][def_name]) return;
261
+ live_defs[mod_id][def_name] = true;
262
+ if (top_defs[mod_id] && has_prop(top_defs[mod_id], def_name)) {
263
+ var entry = top_defs[mod_id][def_name];
264
+ if (entry._idx) {
265
+ // Precomputed index entry: push refs directly, no AST walk needed.
266
+ work_queue.push({mod_id: mod_id, refs: entry.refs});
267
+ } else {
268
+ work_queue.push({mod_id: mod_id, node: entry});
269
+ }
270
+ }
271
+ }
272
+
273
+ // For namespace imports (import X [as Y]), we cannot statically determine
274
+ // which properties of X are accessed. Mark every top-level def in mod_id
275
+ // as live so that X.any_func() calls work correctly at runtime.
276
+ function mark_all_defs_live(mod_id) {
277
+ if (!top_defs[mod_id]) return;
278
+ Object.keys(top_defs[mod_id]).forEach(function(def_name) {
279
+ mark_def_live(mod_id, def_name);
280
+ });
281
+ }
282
+
283
+ // Process a single symbol reference found in live code of `mod_id`
284
+ function process_ref(mod_id, name) {
285
+ // Local top-level def?
286
+ if (top_defs[mod_id] && has_prop(top_defs[mod_id], name)) {
287
+ mark_def_live(mod_id, name);
288
+ return;
289
+ }
290
+ // Import binding?
291
+ if (import_bindings[mod_id] && has_prop(import_bindings[mod_id], name)) {
292
+ if (!used_bindings[mod_id]) used_bindings[mod_id] = {};
293
+ used_bindings[mod_id][name] = true;
294
+ var b = import_bindings[mod_id][name];
295
+ // Mark every partial module in the path as live (e.g. A, A.B, A.B.C).
296
+ // For namespace imports also mark ALL top-level defs in each partial
297
+ // module live, since we cannot know which properties are accessed via
298
+ // the namespace object (e.g. A.func(), A.B.other()).
299
+ var parts = b.source_module.split('.');
300
+ var partial = '';
301
+ for (var p = 0; p < parts.length; p++) {
302
+ partial = (p === 0) ? parts[0] : partial + '.' + parts[p];
303
+ mark_module_live(partial);
304
+ if (b.is_namespace) mark_all_defs_live(partial);
305
+ }
306
+ // For "from X import foo" mark only the specific def in X
307
+ if (!b.is_namespace && b.imported_name) {
308
+ mark_def_live(b.source_module, b.imported_name);
309
+ }
310
+ }
311
+ // Built-in, global, or unknown → ignore
312
+ }
313
+
314
+ // ── 4. Seed from __main__ ─────────────────────────────────────────────
315
+ mark_module_live('__main__');
316
+
317
+ // ── 5. Fixed-point iteration ──────────────────────────────────────────
318
+ // Use pop() (O(1)) instead of shift() (O(n)) — order doesn't matter for correctness.
319
+ while (work_queue.length > 0) {
320
+ var item = work_queue.pop();
321
+ if (item.refs) {
322
+ // Precomputed ref list from the cache index — no AST walk needed.
323
+ item.refs.forEach(function(name) {
324
+ process_ref(item.mod_id, name);
325
+ });
326
+ } else {
327
+ var refs = {};
328
+ collect_refs(item.node, refs);
329
+ Object.keys(refs).forEach(function(name) {
330
+ process_ref(item.mod_id, name);
331
+ });
332
+ }
333
+ }
334
+
335
+ // ── 6. Prune dead code from live modules ──────────────────────────────
336
+ Object.keys(all_modules).forEach(function(mid) {
337
+ if (!live_modules[mid]) return; // dead modules handled below
338
+
339
+ var mod = all_modules[mid];
340
+ if (!mod || !mod.body) return;
341
+
342
+ var ld = live_defs[mid] || {};
343
+ var ub = used_bindings[mid] || {};
344
+ var td = top_defs[mid] || {};
345
+ var new_body = [];
346
+ mod.body.forEach(function(stmt) {
347
+ if ((is_function(stmt) || is_class(stmt)) && stmt.name) {
348
+ if (!ld[stmt.name.name]) return; // dead def
349
+ // Strip @no_prune — it is a tree-shaking annotation, not a
350
+ // runtime function, so it must not appear in compiled output.
351
+ if (stmt.decorators && stmt.decorators.length) {
352
+ stmt.decorators = stmt.decorators.filter(function(d) {
353
+ return !is_no_prune_decorator(d);
354
+ });
355
+ }
356
+ } else if (is_imports(stmt)) {
357
+ var live_imps = [];
358
+ stmt.imports.forEach(function(imp) {
359
+ if (!live_modules[imp.key]) return; // entire source module dead
360
+
361
+ if (imp.argnames && imp.argnames.length) {
362
+ // "from X import a, b" – keep only used argnames
363
+ var live_args = imp.argnames.filter(function(arg) {
364
+ var local = arg.alias ? arg.alias.name : arg.name;
365
+ return !!ub[local];
366
+ });
367
+ if (live_args.length === 0) return; // no argnames used
368
+ imp.argnames = live_args;
369
+ }
370
+ // "import X [as Y]": keep when module is live
371
+ live_imps.push(imp);
372
+ });
373
+ if (live_imps.length === 0) return; // every import in this stmt dead
374
+ stmt.imports = live_imps;
375
+ }
376
+ new_body.push(stmt);
377
+ });
378
+ mod.body = new_body;
379
+
380
+ // Update exports: remove entries for dead defs
381
+ if (mod.exports) {
382
+ mod.exports = mod.exports.filter(function(sym) {
383
+ var name = sym.name;
384
+ if (!has_prop(td, name)) return true; // variable – always keep
385
+ return !!ld[name];
386
+ });
387
+ }
388
+ });
389
+
390
+ // ── 7. Remove dead modules from the shared imports map ───────────────
391
+ Object.keys(toplevel.imports).forEach(function(mod_id) {
392
+ if (!live_modules[mod_id]) {
393
+ delete toplevel.imports[mod_id];
394
+ }
395
+ });
396
+
397
+ return toplevel;
398
+ }
399
+
400
+ export default tree_shake;