rapydscript-ng 0.7.24 → 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.
- package/CHANGELOG.md +19 -0
- package/README.md +72 -26
- package/bin/package.json +1 -0
- package/bin/rapydscript +65 -62
- package/editor-plugins/nvim/rapydscript/README.md +184 -0
- package/editor-plugins/nvim/rapydscript/bin/rapydscript.so +0 -0
- package/editor-plugins/nvim/rapydscript/ftdetect/rapydscript.lua +10 -0
- package/editor-plugins/nvim/rapydscript/lua/rapydscript/health.lua +88 -0
- package/editor-plugins/nvim/rapydscript/lua/rapydscript/init.lua +279 -0
- package/local-agent.md +28 -0
- package/package.json +4 -5
- package/release/baselib-plain-pretty.js +448 -326
- package/release/baselib-plain-ugly.js +3 -3
- package/release/compiler.js +2528 -1672
- package/release/signatures.json +17 -17
- package/src/ast.pyj +73 -25
- package/src/baselib-builtins.pyj +2 -2
- package/src/baselib-containers.pyj +153 -151
- package/src/baselib-internal.pyj +3 -3
- package/src/baselib-str.pyj +5 -5
- package/src/lib/aes.pyj +1 -1
- package/src/lib/encodings.pyj +1 -1
- package/src/lib/pythonize.pyj +1 -1
- package/src/lib/re.pyj +2 -2
- package/src/lib/traceback.pyj +165 -28
- package/src/lib/uuid.pyj +1 -1
- package/src/output/codegen.pyj +10 -3
- package/src/output/functions.pyj +31 -40
- package/src/output/literals.pyj +11 -14
- package/src/output/loops.pyj +25 -87
- package/src/output/modules.pyj +5 -47
- package/src/output/statements.pyj +1 -1
- package/src/output/stream.pyj +58 -31
- package/src/parse.pyj +777 -427
- package/src/tokenizer.pyj +16 -4
- package/src/utils.pyj +1 -1
- package/test/_treeshake_mod.pyj +30 -0
- package/test/_treeshake_side_effect.pyj +9 -0
- package/test/ast_serialization.pyj +392 -0
- package/test/async.pyj +95 -0
- package/test/baselib.pyj +1 -2
- package/test/embedded_compiler.pyj +57 -0
- package/test/fmt.pyj +201 -0
- package/test/generic.pyj +7 -3
- package/test/imports.pyj +8 -6
- package/test/internationalization.pyj +38 -37
- package/test/lint.pyj +120 -117
- package/test/lsp.pyj +222 -0
- package/test/repl.pyj +70 -65
- package/test/sourcemaps.pyj +315 -0
- package/test/starargs.pyj +46 -39
- package/test/traceback.pyj +263 -0
- package/test/treeshaking.pyj +181 -0
- package/test/web_repl_export.pyj +88 -0
- package/tools/ast_serialize.mjs +557 -0
- package/tools/cli.mjs +510 -0
- package/tools/compile.mjs +201 -0
- package/tools/compiler.mjs +127 -0
- package/tools/{completer.js → completer.mjs} +6 -6
- package/tools/{embedded_compiler.js → embedded_compiler.mjs} +17 -13
- package/tools/export.js +109 -56
- package/tools/fmt.mjs +735 -0
- package/tools/{gettext.js → gettext.mjs} +46 -53
- package/tools/{ini.js → ini.mjs} +9 -10
- package/tools/{lint.js → lint.mjs} +78 -55
- package/tools/lsp.mjs +914 -0
- package/tools/lsp_protocol.mjs +259 -0
- package/tools/lsp_symbols.mjs +418 -0
- package/tools/{msgfmt.js → msgfmt.mjs} +18 -18
- package/tools/{repl.js → repl.mjs} +52 -41
- package/tools/{self.js → self.mjs} +56 -46
- package/tools/sourcemap.mjs +123 -0
- package/tools/test.mjs +152 -0
- package/tools/treeshake.mjs +400 -0
- package/tools/{utils.js → utils.mjs} +26 -23
- package/tools/{web_repl.js → web_repl.mjs} +15 -13
- package/tools/web_repl_export.mjs +93 -0
- package/tree-sitter/README.md +101 -0
- package/tree-sitter/grammar.js +992 -0
- package/tree-sitter/package.json +43 -0
- package/tree-sitter/queries/rapydscript/highlights.scm +232 -0
- package/tree-sitter/queries/rapydscript/indents.scm +64 -0
- package/tree-sitter/queries/rapydscript/injections.scm +14 -0
- package/tree-sitter/queries/rapydscript/locals.scm +108 -0
- package/tree-sitter/src/grammar.json +4976 -0
- package/tree-sitter/src/node-types.json +2901 -0
- package/tree-sitter/src/parser.c +196465 -0
- package/tree-sitter/src/scanner.c +294 -0
- package/tree-sitter/src/tree_sitter/alloc.h +54 -0
- package/tree-sitter/src/tree_sitter/array.h +330 -0
- package/tree-sitter/src/tree_sitter/parser.h +286 -0
- package/tree-sitter/test/corpus/chaining.txt +99 -0
- package/tree-sitter/test/corpus/classes.txt +147 -0
- package/tree-sitter/test/corpus/comprehensions.txt +155 -0
- package/tree-sitter/test/corpus/containers.txt +242 -0
- package/tree-sitter/test/corpus/control_flow.txt +361 -0
- package/tree-sitter/test/corpus/decorators.txt +64 -0
- package/tree-sitter/test/corpus/existential.txt +102 -0
- package/tree-sitter/test/corpus/functions.txt +293 -0
- package/tree-sitter/test/corpus/imports.txt +117 -0
- package/tree-sitter/test/corpus/literals.txt +135 -0
- package/tree-sitter/test/corpus/operators.txt +296 -0
- package/tree-sitter/test/corpus/statements.txt +189 -0
- package/tree-sitter/test/corpus/strings.txt +90 -0
- package/tree-sitter/test/corpus/subscripts.txt +227 -0
- package/tree-sitter/tree-sitter.json +36 -0
- package/web-repl/env.js +35 -23
- package/web-repl/main.js +8 -8
- package/bin/export +0 -75
- package/bin/web-repl-export +0 -102
- package/tools/cli.js +0 -523
- package/tools/compile.js +0 -184
- package/tools/compiler.js +0 -84
- 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
|
|
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, '/', '-') + '.
|
|
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.
|
|
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()
|