rapydscript-ng 0.7.23 → 0.8.0

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 (114) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +72 -26
  3. package/bin/package.json +1 -0
  4. package/bin/rapydscript +65 -62
  5. package/editor-plugins/nvim/rapydscript/README.md +184 -0
  6. package/editor-plugins/nvim/rapydscript/bin/rapydscript.so +0 -0
  7. package/editor-plugins/nvim/rapydscript/ftdetect/rapydscript.lua +10 -0
  8. package/editor-plugins/nvim/rapydscript/lua/rapydscript/health.lua +88 -0
  9. package/editor-plugins/nvim/rapydscript/lua/rapydscript/init.lua +279 -0
  10. package/local-agent.md +28 -0
  11. package/package.json +4 -5
  12. package/release/baselib-plain-pretty.js +448 -326
  13. package/release/baselib-plain-ugly.js +3 -3
  14. package/release/compiler.js +2528 -1672
  15. package/release/signatures.json +17 -17
  16. package/src/ast.pyj +73 -25
  17. package/src/baselib-builtins.pyj +2 -2
  18. package/src/baselib-containers.pyj +153 -151
  19. package/src/baselib-internal.pyj +3 -3
  20. package/src/baselib-str.pyj +5 -5
  21. package/src/lib/aes.pyj +1 -1
  22. package/src/lib/encodings.pyj +1 -1
  23. package/src/lib/pythonize.pyj +1 -1
  24. package/src/lib/re.pyj +2 -2
  25. package/src/lib/traceback.pyj +165 -28
  26. package/src/lib/uuid.pyj +1 -1
  27. package/src/output/codegen.pyj +10 -3
  28. package/src/output/functions.pyj +31 -40
  29. package/src/output/literals.pyj +11 -14
  30. package/src/output/loops.pyj +25 -87
  31. package/src/output/modules.pyj +5 -47
  32. package/src/output/statements.pyj +1 -1
  33. package/src/output/stream.pyj +58 -31
  34. package/src/parse.pyj +777 -427
  35. package/src/tokenizer.pyj +16 -4
  36. package/src/utils.pyj +1 -1
  37. package/test/_treeshake_mod.pyj +30 -0
  38. package/test/_treeshake_side_effect.pyj +9 -0
  39. package/test/ast_serialization.pyj +392 -0
  40. package/test/async.pyj +95 -0
  41. package/test/baselib.pyj +1 -2
  42. package/test/embedded_compiler.pyj +57 -0
  43. package/test/fmt.pyj +201 -0
  44. package/test/generic.pyj +7 -3
  45. package/test/imports.pyj +8 -6
  46. package/test/internationalization.pyj +38 -37
  47. package/test/lint.pyj +120 -115
  48. package/test/lsp.pyj +222 -0
  49. package/test/repl.pyj +70 -65
  50. package/test/sourcemaps.pyj +315 -0
  51. package/test/starargs.pyj +46 -39
  52. package/test/traceback.pyj +263 -0
  53. package/test/treeshaking.pyj +181 -0
  54. package/test/web_repl_export.pyj +88 -0
  55. package/tools/ast_serialize.mjs +557 -0
  56. package/tools/cli.mjs +510 -0
  57. package/tools/compile.mjs +201 -0
  58. package/tools/compiler.mjs +127 -0
  59. package/tools/{completer.js → completer.mjs} +6 -6
  60. package/tools/{embedded_compiler.js → embedded_compiler.mjs} +17 -13
  61. package/tools/export.js +109 -56
  62. package/tools/fmt.mjs +735 -0
  63. package/tools/{gettext.js → gettext.mjs} +46 -53
  64. package/tools/{ini.js → ini.mjs} +9 -10
  65. package/tools/{lint.js → lint.mjs} +107 -56
  66. package/tools/lsp.mjs +914 -0
  67. package/tools/lsp_protocol.mjs +259 -0
  68. package/tools/lsp_symbols.mjs +418 -0
  69. package/tools/{msgfmt.js → msgfmt.mjs} +18 -18
  70. package/tools/{repl.js → repl.mjs} +52 -41
  71. package/tools/{self.js → self.mjs} +56 -46
  72. package/tools/sourcemap.mjs +123 -0
  73. package/tools/test.mjs +152 -0
  74. package/tools/treeshake.mjs +400 -0
  75. package/tools/{utils.js → utils.mjs} +26 -23
  76. package/tools/{web_repl.js → web_repl.mjs} +15 -13
  77. package/tools/web_repl_export.mjs +93 -0
  78. package/tree-sitter/README.md +101 -0
  79. package/tree-sitter/grammar.js +992 -0
  80. package/tree-sitter/package.json +43 -0
  81. package/tree-sitter/queries/rapydscript/highlights.scm +232 -0
  82. package/tree-sitter/queries/rapydscript/indents.scm +64 -0
  83. package/tree-sitter/queries/rapydscript/injections.scm +14 -0
  84. package/tree-sitter/queries/rapydscript/locals.scm +108 -0
  85. package/tree-sitter/src/grammar.json +4976 -0
  86. package/tree-sitter/src/node-types.json +2901 -0
  87. package/tree-sitter/src/parser.c +196465 -0
  88. package/tree-sitter/src/scanner.c +294 -0
  89. package/tree-sitter/src/tree_sitter/alloc.h +54 -0
  90. package/tree-sitter/src/tree_sitter/array.h +330 -0
  91. package/tree-sitter/src/tree_sitter/parser.h +286 -0
  92. package/tree-sitter/test/corpus/chaining.txt +99 -0
  93. package/tree-sitter/test/corpus/classes.txt +147 -0
  94. package/tree-sitter/test/corpus/comprehensions.txt +155 -0
  95. package/tree-sitter/test/corpus/containers.txt +242 -0
  96. package/tree-sitter/test/corpus/control_flow.txt +361 -0
  97. package/tree-sitter/test/corpus/decorators.txt +64 -0
  98. package/tree-sitter/test/corpus/existential.txt +102 -0
  99. package/tree-sitter/test/corpus/functions.txt +293 -0
  100. package/tree-sitter/test/corpus/imports.txt +117 -0
  101. package/tree-sitter/test/corpus/literals.txt +135 -0
  102. package/tree-sitter/test/corpus/operators.txt +296 -0
  103. package/tree-sitter/test/corpus/statements.txt +189 -0
  104. package/tree-sitter/test/corpus/strings.txt +90 -0
  105. package/tree-sitter/test/corpus/subscripts.txt +227 -0
  106. package/tree-sitter/tree-sitter.json +36 -0
  107. package/web-repl/env.js +35 -23
  108. package/web-repl/main.js +8 -8
  109. package/bin/export +0 -75
  110. package/bin/web-repl-export +0 -102
  111. package/tools/cli.js +0 -523
  112. package/tools/compile.js +0 -184
  113. package/tools/compiler.js +0 -84
  114. package/tools/test.js +0 -110
package/src/tokenizer.pyj CHANGED
@@ -81,16 +81,16 @@ PUNC_BEFORE_EXPRESSION = make_predicate(characters("[{(,.;:"))
81
81
 
82
82
  PUNC_CHARS = make_predicate(characters("[]{}(),;:?"))
83
83
 
84
- KEYWORDS = "as assert break class continue def del do elif else except finally for from global if import in is new nonlocal pass raise return yield try while with or and not"
84
+ KEYWORDS = "as assert async await break class continue def del do elif else except finally for from global if import in is new nonlocal pass raise return yield try while with or and not"
85
85
 
86
86
  KEYWORDS_ATOM = "False None True"
87
87
 
88
88
  # see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar
89
89
  RESERVED_WORDS = ("break case class catch const continue debugger default delete do else export extends"
90
90
  " finally for function if import in instanceof new return super switch this throw try typeof var void"
91
- " while with yield enum implements static private package let public protected interface await null true false" )
91
+ " while with yield enum implements static private package let public protected interface null true false" )
92
92
 
93
- KEYWORDS_BEFORE_EXPRESSION = "return yield new del raise elif else if"
93
+ KEYWORDS_BEFORE_EXPRESSION = "return yield new del raise elif else if await"
94
94
 
95
95
  ALL_KEYWORDS = KEYWORDS + " " + KEYWORDS_ATOM
96
96
 
@@ -153,10 +153,12 @@ def is_token(token, type, val):
153
153
 
154
154
  EX_EOF = {}
155
155
 
156
- def tokenizer(raw_text, filename):
156
+ def tokenizer(raw_text, filename, recover_errors):
157
157
  S = {
158
158
  'text': raw_text.replace(/\r\n?|[\n\u2028\u2029]/g, "\n").replace(/\uFEFF/g, ""),
159
159
  'filename': filename,
160
+ 'recover_errors': not not recover_errors, # LSP error-recovery mode (see parse())
161
+ 'recovered_errors': v'[]', # collected errors when recover_errors is on
160
162
  'pos': 0,
161
163
  'tokpos': 0,
162
164
  'line': 1,
@@ -703,6 +705,16 @@ def tokenizer(raw_text, filename):
703
705
  tok.type = stok.type
704
706
  return tok
705
707
 
708
+ if S.recover_errors:
709
+ # In error-recovery mode (used by the LSP) an unrecognized character
710
+ # must not raise, because re-tokenizing from the same position would
711
+ # loop forever (parse_error is raised before the character is
712
+ # consumed). Record it and skip the single offending character so the
713
+ # tokenizer always makes forward progress.
714
+ S.recovered_errors.push({'message': "Unexpected character «" + ch + "»",
715
+ 'line': S.tokline, 'col': S.tokcol, 'pos': S.tokpos, 'is_eof': False})
716
+ next()
717
+ return next_token()
706
718
  parse_error("Unexpected character «" + ch + "»")
707
719
 
708
720
  next_token.context = def(nc):
package/src/utils.pyj CHANGED
@@ -188,5 +188,5 @@ def make_predicate(words):
188
188
  def cache_file_name(src, cache_dir):
189
189
  if cache_dir:
190
190
  src = str.replace(src, '\\', '/')
191
- return cache_dir + '/' + str.lstrip(str.replace(src, '/', '-') + '.json', '-')
191
+ return cache_dir + '/' + str.lstrip(str.replace(src, '/', '-') + '.bin', '-')
192
192
  return src + '-cached'
@@ -0,0 +1,30 @@
1
+ # Helper module for tree shaking tests
2
+
3
+ def func_used():
4
+ return 'used'
5
+
6
+ def func_unused():
7
+ return 'unused'
8
+
9
+ class ClassUsed:
10
+ def __init__(self):
11
+ self.val = 'used_class'
12
+
13
+ class ClassUnused:
14
+ def __init__(self):
15
+ self.val = 'unused_class'
16
+
17
+ module_var = 'module_var_value'
18
+
19
+ def helper_a():
20
+ return 'a'
21
+
22
+ def helper_b():
23
+ return helper_a()
24
+
25
+ @no_prune
26
+ def pinned_in_mod():
27
+ return 'pinned'
28
+
29
+ def not_pinned_in_mod():
30
+ return 'not_pinned'
@@ -0,0 +1,9 @@
1
+ # vim:fileencoding=utf-8
2
+ # Helper module for tree shaking side-effect import tests.
3
+ # Mirrors the calibre initialize.pyj pattern: a module-level function call
4
+ # that patches a global object (here String.prototype) as a side effect.
5
+
6
+ def apply_patch():
7
+ v'String.prototype.__treeshake_test = function() { return "side_effect_ran"; }'
8
+
9
+ apply_patch()
@@ -0,0 +1,392 @@
1
+ # vim:fileencoding=utf-8
2
+ # Test AST serialization/deserialization round-trip.
3
+ # Verifies that ast_to_json → ast_from_json produces an AST that generates
4
+ # identical JavaScript output to the original parsed AST.
5
+ # globals: assrt, RapydScript, fs, compiler_dir
6
+
7
+ # ─── Helpers ──────────────────────────────────────────────────────────────────
8
+
9
+ baselib = fs.readFileSync(compiler_dir + '/baselib-plain-pretty.js', {'encoding': 'utf-8'})
10
+
11
+ def make_output():
12
+ return new RapydScript.OutputStream({
13
+ 'baselib_plain': baselib,
14
+ 'beautify': True,
15
+ 'js_version': 6,
16
+ 'write_name': False,
17
+ 'private_scope': False,
18
+ 'keep_docstrings': False,
19
+ })
20
+
21
+ def ast_to_js(ast):
22
+ out = make_output()
23
+ ast.print(out)
24
+ return out.toString()
25
+
26
+ async def round_trip(src, label):
27
+ """Parse src, generate js1, serialize+deserialize AST, generate js2, assert equal."""
28
+ ast1 = await RapydScript.parse(src, {'filename': label})
29
+ js1 = ast_to_js(ast1)
30
+
31
+ serialized = RapydScript.ast_to_json(ast1)
32
+ assrt.ok(serialized and jstype(serialized) is 'object', label + ': ast_to_json returned an object')
33
+
34
+ ast2 = RapydScript.ast_from_json(serialized)
35
+ js2 = ast_to_js(ast2)
36
+
37
+ assrt.equal(js2, js1, label + ': round-trip JS output mismatch')
38
+
39
+ async def assert_json_valid(src, label):
40
+ """Verify ast_to_json produces a well-formed AST object."""
41
+ ast = await RapydScript.parse(src, {'filename': label})
42
+ data = RapydScript.ast_to_json(ast)
43
+ assrt.equal(data.v, 1, label + ': version field')
44
+ assrt.ok(Array.isArray(data.nodes), label + ': nodes is array')
45
+ assrt.equal(data.root, 0, label + ': root is 0')
46
+ assrt.ok(data.nodes.length > 0, label + ': pool is non-empty')
47
+
48
+ async def run_tests():
49
+ # ─── Test 1: Basic literals and atoms ─────────────────────────────────────────
50
+ # Exercises: AST_Number, AST_String, AST_True, AST_False, AST_Null,
51
+ # AST_NaN, AST_Infinity, AST_Undefined, AST_SimpleStatement, AST_Assign
52
+
53
+ await round_trip("""
54
+ a = None
55
+ b = True
56
+ c = False
57
+ d = 42
58
+ e = 3.14
59
+ f = "hello world"
60
+ g = ''
61
+ """, 'literals')
62
+
63
+ # ─── Test 2: RegExp literal (special serialization) ───────────────────────────
64
+ # Exercises: AST_RegExp with source and flags
65
+
66
+ await round_trip("""
67
+ r1 = /[a-z]+/
68
+ r2 = /[A-Z]+/gi
69
+ r3 = /^\\d{3}-\\d{4}$/m
70
+ """, 'regexp')
71
+
72
+ # ─── Test 3: Operators ─────────────────────────────────────────────────────────
73
+ # Exercises: AST_Binary, AST_UnaryPrefix, AST_Assign, AST_Conditional
74
+
75
+ await round_trip("""
76
+ x = 1 + 2 * 3 - 4 / 5
77
+ y = x ** 2
78
+ z = x // 3
79
+ a = True and False or not True
80
+ b = 1 if a else 2
81
+ c = x > 0 and x < 10
82
+ """, 'operators')
83
+
84
+ # ─── Test 4: Collections ──────────────────────────────────────────────────────
85
+ # Exercises: AST_Array, AST_Object, AST_ObjectKeyVal, AST_Set, AST_SetItem
86
+
87
+ await round_trip("""
88
+ arr = [1, 2, 3, "four"]
89
+ obj = {'a': 1, 'b': True, 'c': None}
90
+ st = {1, 2, 3}
91
+ nested = [[1, 2], [3, 4]]
92
+ """, 'collections')
93
+
94
+ # ─── Test 5: Function definitions ─────────────────────────────────────────────
95
+ # Exercises: AST_Function, AST_ArgsDef (simple + defaults + *args + **kwargs),
96
+ # AST_Return, AST_SymbolFunarg, AST_SymbolDefun, AST_Decorator
97
+
98
+ await round_trip("""
99
+ def simple(x):
100
+ return x
101
+
102
+ def with_defaults(a, b=10, c="hi"):
103
+ return a + b
104
+
105
+ def with_star(a, *args, **kwargs):
106
+ return args
107
+
108
+ def annotated(x, y=0):
109
+ pass
110
+
111
+ def no_args():
112
+ return None
113
+
114
+ def decorator_fn(f):
115
+ return f
116
+
117
+ @decorator_fn
118
+ def decorated(x):
119
+ return x * 2
120
+ """, 'functions')
121
+
122
+ # ─── Test 6: Classes ──────────────────────────────────────────────────────────
123
+ # Exercises: AST_Class, AST_Method (static/getter/setter), AST_SymbolDeclaration,
124
+ # AST_This, dynamic_properties, classvars
125
+
126
+ await round_trip("""
127
+ class Animal:
128
+ count = 0
129
+
130
+ def __init__(self, name):
131
+ self.name = name
132
+ Animal.count += 1
133
+
134
+ def speak(self):
135
+ return "..."
136
+
137
+ @staticmethod
138
+ def create(name):
139
+ return Animal(name)
140
+
141
+ class Dog(Animal):
142
+ def __init__(self, name, breed):
143
+ self.breed = breed
144
+
145
+ def speak(self):
146
+ return "Woof"
147
+
148
+ class Cat(Animal):
149
+ pass
150
+ """, 'classes')
151
+
152
+ # ─── Test 7: Control flow ─────────────────────────────────────────────────────
153
+ # Exercises: AST_If, AST_While, AST_ForIn, AST_Break, AST_Continue,
154
+ # AST_EmptyStatement, AST_Do
155
+
156
+ await round_trip("""
157
+ x = 5
158
+
159
+ if x > 10:
160
+ x = 10
161
+ elif x < 0:
162
+ x = 0
163
+ else:
164
+ pass
165
+
166
+ i = 0
167
+ while i < 5:
168
+ if i == 3:
169
+ break
170
+ i += 1
171
+
172
+ for item in [1, 2, 3]:
173
+ if item == 2:
174
+ continue
175
+ x += item
176
+ """, 'control_flow')
177
+
178
+ # ─── Test 8: Try/except ───────────────────────────────────────────────────────
179
+ # Exercises: AST_Try, AST_Catch, AST_Except, AST_Finally, AST_Else,
180
+ # AST_Throw, AST_SymbolCatch
181
+
182
+ await round_trip("""
183
+ def risky(x):
184
+ try:
185
+ if x < 0:
186
+ raise ValueError("negative")
187
+ return x
188
+ except ValueError as e:
189
+ return -1
190
+ except TypeError as e:
191
+ return -2
192
+ else:
193
+ pass
194
+ finally:
195
+ pass
196
+ """, 'try_except')
197
+
198
+ # ─── Test 9: Comprehensions ───────────────────────────────────────────────────
199
+ # Exercises: AST_ListComprehension, AST_DictComprehension, AST_SetComprehension,
200
+ # AST_GeneratorComprehension
201
+
202
+ await round_trip("""
203
+ nums = [1, 2, 3, 4, 5]
204
+ squares = [x**2 for x in nums if x > 1]
205
+ even_sq = {x: x**2 for x in nums if x % 2 == 0}
206
+ sq_set = {x**2 for x in nums}
207
+ """, 'comprehensions')
208
+
209
+ # ─── Test 10: Property access and calls ───────────────────────────────────────
210
+ # Exercises: AST_Dot, AST_Sub, AST_Call, AST_CallArgs, AST_New,
211
+ # AST_SymbolRef
212
+
213
+ await round_trip("""
214
+ obj = {'a': {'b': [1, 2, 3]}}
215
+ v = obj['a']['b'][0]
216
+ s = "hello".upper()
217
+ n = len([1, 2, 3])
218
+ """, 'access_and_calls')
219
+
220
+ # ─── Test 11: Assert ──────────────────────────────────────────────────────────
221
+ # Exercises: AST_Assert
222
+
223
+ await round_trip("""
224
+ x = 42
225
+ assert x > 0
226
+ assert x < 100, "x must be less than 100"
227
+ """, 'assert')
228
+
229
+ # ─── Test 12: Generator function ──────────────────────────────────────────────
230
+ # Exercises: AST_Yield (is_yield_from=False and True)
231
+
232
+ await round_trip("""
233
+ def counter(n):
234
+ for i in range(n):
235
+ yield i
236
+
237
+ def chained(n):
238
+ yield from counter(n)
239
+ yield 99
240
+ """, 'generators')
241
+
242
+ # ─── Test 13: Verbatim JS ─────────────────────────────────────────────────────
243
+ # Exercises: AST_Verbatim
244
+
245
+ await round_trip("""
246
+ raw = v'typeof window !== "undefined"'
247
+ """, 'verbatim')
248
+
249
+ # ─── Test 14: With statement ──────────────────────────────────────────────────
250
+ # Exercises: AST_With, AST_WithClause, AST_SymbolAlias
251
+
252
+ await round_trip("""
253
+ class CM:
254
+ def __enter__(self):
255
+ return self
256
+ def __exit__(self, t, v, tb):
257
+ pass
258
+
259
+ with CM() as ctx:
260
+ pass
261
+ """, 'with_statement')
262
+
263
+ # ─── Test 15: Nested scopes / closures ────────────────────────────────────────
264
+ # Exercises: nested AST_Function, nonlocal
265
+
266
+ await round_trip("""
267
+ def outer(x):
268
+ y = x * 2
269
+ def inner(z):
270
+ nonlocal y
271
+ y += z
272
+ return y
273
+ return inner
274
+ """, 'closures')
275
+
276
+ # ─── Test 16: JSON format sanity checks ───────────────────────────────────────
277
+ # Verify the serialized JSON structure is well-formed
278
+
279
+ await assert_json_valid("x = 1 + 2", 'json_format_simple')
280
+ await assert_json_valid("r = /[a-z]+/gi", 'json_format_regexp')
281
+ await assert_json_valid("""
282
+ def f(a, b=1):
283
+ return a + b
284
+ """, 'json_format_function')
285
+
286
+ # ─── Test 17: RegExp round-trip value check ───────────────────────────────────
287
+ # Verify the deserialized RegExp has the correct source and flags
288
+
289
+ src17 = "r = /^hello\\sworld$/im"
290
+ ast17 = await RapydScript.parse(src17, {'filename': 'regexp_check'})
291
+ serialized17 = RapydScript.ast_to_json(ast17)
292
+ ast17b = RapydScript.ast_from_json(serialized17)
293
+
294
+ # Navigate to the AST_RegExp node: Toplevel → SimpleStatement → Assign → right
295
+ re_node = ast17b.body[0].body.right
296
+ assrt.ok(re_node.constructor.name is 'AST_RegExp', 'deserialized node is AST_RegExp')
297
+ assrt.ok(v'Object.prototype.toString.call(re_node.value) === "[object RegExp]"', 'value is a RegExp object')
298
+ assrt.equal(re_node.value.source, '^hello\\sworld$', 'regexp source matches')
299
+ assrt.ok(re_node.value.ignoreCase, 'regexp ignoreCase flag set')
300
+ assrt.ok(re_node.value.multiline, 'regexp multiline flag set')
301
+
302
+ # ─── Test 18: Shared reference integrity ──────────────────────────────────────
303
+ # A node appearing in multiple places (e.g. AST_Class.init also in body)
304
+ # should deserialize to the SAME JS object (same reference).
305
+
306
+ src18 = """
307
+ class Foo:
308
+ def __init__(self):
309
+ self.x = 1
310
+ def bar(self):
311
+ return self.x
312
+ """
313
+ ast18 = await RapydScript.parse(src18, {'filename': 'shared_ref'})
314
+ data18 = RapydScript.ast_to_json(ast18)
315
+
316
+ # Check that the JSON pool has no duplicate entries for the same node:
317
+ # Every index should appear at most once as a _n value in the pool.
318
+ # (Shared refs should point to the same pool index, not copy the node.)
319
+ index_uses = {}
320
+ def count_refs(val):
321
+ if Array.isArray(val):
322
+ for el in val:
323
+ count_refs(el)
324
+ elif val and jstype(val) is 'object':
325
+ if Object.keys(val).length is 1 and val._n is not None and jstype(val._n) is 'number':
326
+ index_uses[val._n] = (index_uses[val._n] or 0) + 1
327
+ else:
328
+ for k in Object.keys(val):
329
+ count_refs(val[k])
330
+
331
+ for node_entry in data18.nodes:
332
+ if node_entry:
333
+ count_refs(node_entry.p)
334
+
335
+ # init in AST_Class.p should reference the same index as the __init__ method in body.
336
+ # We just verify that the deserialized AST still round-trips correctly.
337
+ ast18b = RapydScript.ast_from_json(data18)
338
+ assrt.equal(ast_to_js(ast18b), ast_to_js(ast18), 'shared ref round-trip')
339
+
340
+ # ─── Test 19: Splat args (*arg) round-trip ────────────────────────────────────
341
+ # Exercises: is_array flag on call-site *arg nodes.
342
+ # Without fix: is_array is lost in serialization, causing func(*arg) to
343
+ # generate func.apply(self, [arg]) instead of func.apply(self, arg).
344
+
345
+ await round_trip("""
346
+ def call_it(f, args, a, b):
347
+ f(*args)
348
+ f(a, *args)
349
+ f(a, b, *args)
350
+ """, 'splat_args')
351
+
352
+ # ─── Test 20: Large/complex combined source ───────────────────────────────────
353
+ # Full round-trip of a more complex program
354
+
355
+ await round_trip("""
356
+ def fib(n):
357
+ if n <= 1:
358
+ return n
359
+ return fib(n - 1) + fib(n - 2)
360
+
361
+ class Stack:
362
+ def __init__(self):
363
+ self._items = []
364
+
365
+ def push(self, item):
366
+ self._items.append(item)
367
+
368
+ def pop(self):
369
+ if len(self._items) == 0:
370
+ raise IndexError("pop from empty stack")
371
+ return self._items.pop()
372
+
373
+ @property
374
+ def size(self):
375
+ return len(self._items)
376
+
377
+ def process(items, transform=None, filter_fn=None):
378
+ result = []
379
+ for item in items:
380
+ if filter_fn and not filter_fn(item):
381
+ continue
382
+ if transform:
383
+ item = transform(item)
384
+ result.append(item)
385
+ return result
386
+
387
+ numbers = [fib(i) for i in range(10)]
388
+ evens = [x for x in numbers if x % 2 == 0]
389
+ mapping = {x: x**2 for x in evens}
390
+ """, 'complex_program')
391
+
392
+ __test_async_done__ = run_tests()
package/test/async.pyj ADDED
@@ -0,0 +1,95 @@
1
+ # Tests for async/await support
2
+
3
+ async def async_identity(x):
4
+ return x
5
+
6
+ async def async_add(x, y):
7
+ a = await async_identity(x)
8
+ b = await async_identity(y)
9
+ return a + b
10
+
11
+ async def async_raises():
12
+ raise TypeError("async error")
13
+
14
+ async def async_chain(n):
15
+ if n <= 0:
16
+ return 0
17
+ prev = await async_chain(n - 1)
18
+ return prev + n
19
+
20
+ class AsyncCounter:
21
+ def __init__(self):
22
+ self.count = 0
23
+
24
+ async def increment(self, amount):
25
+ val = await async_identity(amount)
26
+ self.count += val
27
+ return self.count
28
+
29
+ async def reset(self):
30
+ self.count = 0
31
+ return self.count
32
+
33
+ async def get(self):
34
+ return await async_identity(self.count)
35
+
36
+ async def run_tests():
37
+ # Basic async function returning a value
38
+ result = await async_identity(42)
39
+ assrt.equal(result, 42, "basic await identity")
40
+
41
+ # Multiple awaits in sequence
42
+ result = await async_add(3, 4)
43
+ assrt.equal(result, 7, "sequential awaits")
44
+
45
+ # Calling an async function without await returns a Promise
46
+ p = async_identity(10)
47
+ assrt.ok(v"p instanceof Promise", "async fn returns Promise")
48
+ resolved = await p
49
+ assrt.equal(resolved, 10, "awaiting stored Promise")
50
+
51
+ # Recursive async functions
52
+ result = await async_chain(5)
53
+ assrt.equal(result, 15, "recursive async (1+2+3+4+5)")
54
+
55
+ # Async class methods
56
+ counter = AsyncCounter()
57
+ result = await counter.increment(5)
58
+ assrt.equal(result, 5, "async method first increment")
59
+ result = await counter.increment(3)
60
+ assrt.equal(result, 8, "async method second increment")
61
+ result = await counter.get()
62
+ assrt.equal(result, 8, "async getter method")
63
+ result = await counter.reset()
64
+ assrt.equal(result, 0, "async reset method")
65
+
66
+ # Exception propagation through async boundaries
67
+ caught = False
68
+ try:
69
+ await async_raises()
70
+ except TypeError as e:
71
+ caught = True
72
+ assrt.ok(str(e).indexOf("async error") >= 0, "exception message preserved")
73
+ assrt.ok(caught, "exception propagated through await")
74
+
75
+ # async def as an expression (assigned to variable)
76
+ fn = async def(x): return x * 2
77
+ result = await fn(7)
78
+ assrt.equal(result, 14, "async lambda expression")
79
+
80
+ # Await in a nested async function
81
+ async def outer(x):
82
+ async def inner(y):
83
+ return await async_identity(y + 1)
84
+ return await inner(x)
85
+ result = await outer(9)
86
+ assrt.equal(result, 10, "nested async functions")
87
+
88
+ # Promise.all equivalent via verbatim JS
89
+ p1 = async_identity(100)
90
+ p2 = async_identity(200)
91
+ results = await v"Promise.all([p1, p2])"
92
+ assrt.equal(results[0], 100, "Promise.all first")
93
+ assrt.equal(results[1], 200, "Promise.all second")
94
+
95
+ __test_async_done__ = run_tests()
package/test/baselib.pyj CHANGED
@@ -13,7 +13,7 @@ class CustomIterable:
13
13
  t = []
14
14
  q = [1,2,3]
15
15
  for x in CustomIterable(q):
16
- t.push(x)
16
+ t.append(x)
17
17
  assrt.deepEqual(q, t)
18
18
 
19
19
  assrt.deepEqual(['a', 'b'], list('ab'))
@@ -137,7 +137,6 @@ a.clear()
137
137
  assrt.equal(a.length, 0)
138
138
  assrt.deepEqual(a.as_array(), a)
139
139
  assrt.ok(a is not a.as_array())
140
- assrt.ok(a.as_array().extend == undefined)
141
140
  a = [1, 2, 1]
142
141
  assrt.equal(a.count(1), 2)
143
142
  a = [3, 2, 4, 1]
@@ -0,0 +1,57 @@
1
+ # globals: assrt, rs_create_embedded_compiler, fs
2
+
3
+ async def run_tests():
4
+ # Test that create_embedded_compiler() works with no arguments
5
+ compiler = await rs_create_embedded_compiler()
6
+
7
+ # compile must be callable and return non-empty JS
8
+ js = await compiler.compile('x = 1 + 2\n')
9
+ assrt.ok(js and js.length > 0, 'compile must return non-empty JS')
10
+
11
+ # Test compiling a class definition
12
+ js2 = await compiler.compile('class Foo:\n def __init__(self, v):\n self.v = v\n')
13
+ assrt.ok(js2 and js2.length > 0, 'compile must handle class definitions')
14
+
15
+ # Test that successive compile calls accumulate class knowledge (streaming mode)
16
+ compiler2 = await rs_create_embedded_compiler()
17
+ await compiler2.compile('class Bar:\n def __init__(self, v):\n self.v = v\n')
18
+ js3 = await compiler2.compile('b = Bar(42)\n')
19
+ assrt.ok(js3.indexOf('Bar') >= 0, 'subsequent compile should know about previously defined class Bar')
20
+
21
+ # Test virtual_file_system: compile code that imports from a virtual module
22
+ vfs_modules = {
23
+ '__vfs__/testmod.pyj': 'def add_one(x):\n return x + 1\n'
24
+ }
25
+ def vfs_read(path, enc):
26
+ if vfs_modules[path]:
27
+ return Promise.resolve(vfs_modules[path])
28
+ return fs.promises.readFile(path, enc)
29
+
30
+ vfs_compiler = await rs_create_embedded_compiler({
31
+ 'virtual_file_system': {'read_file': vfs_read}
32
+ })
33
+ js4 = await vfs_compiler.compile('from testmod import add_one\nresult = add_one(5)\n')
34
+ assrt.ok(js4 and js4.length > 0, 'compile with vfs import must return JS')
35
+ assrt.ok(js4.indexOf('add_one') >= 0, 'compiled JS must reference the imported function')
36
+
37
+ # Test that stdlib imports are served from the bundled stdlib, not from VFS.
38
+ # The strict VFS below throws ENOENT for any path it does not own, so if the
39
+ # compiler were to route a stdlib read through VFS the import would fail.
40
+ strict_vfs_modules = {
41
+ '__vfs__/testmod.pyj': 'def add_one(x):\n return x + 1\n'
42
+ }
43
+ def strict_vfs_read(path, enc):
44
+ if strict_vfs_modules[path]:
45
+ return Promise.resolve(strict_vfs_modules[path])
46
+ err = new Error('ENOENT: ' + path)
47
+ err.code = 'ENOENT'
48
+ return Promise.reject(err)
49
+
50
+ strict_vfs_compiler = await rs_create_embedded_compiler({
51
+ 'virtual_file_system': {'read_file': strict_vfs_read}
52
+ })
53
+ js5 = await strict_vfs_compiler.compile('from math import sqrt\nresult = sqrt(4)\n')
54
+ assrt.ok(js5 and js5.length > 0, 'stdlib import must succeed when VFS is provided (served from bundled cache)')
55
+ assrt.ok(js5.indexOf('sqrt') >= 0, 'compiled JS must reference the stdlib sqrt function')
56
+
57
+ __test_async_done__ = run_tests()