rapydscript-ns 0.8.2 → 0.8.4

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 (141) hide show
  1. package/.agignore +1 -1
  2. package/.github/workflows/ci.yml +38 -38
  3. package/=template.pyj +5 -5
  4. package/CHANGELOG.md +39 -0
  5. package/HACKING.md +103 -103
  6. package/LICENSE +24 -24
  7. package/PYTHON_DIFFERENCES_REPORT.md +291 -0
  8. package/PYTHON_FEATURE_COVERAGE.md +106 -15
  9. package/README.md +831 -52
  10. package/TODO.md +4 -286
  11. package/add-toc-to-readme +2 -2
  12. package/bin/export +75 -75
  13. package/bin/rapydscript +70 -70
  14. package/bin/web-repl-export +102 -102
  15. package/build +2 -2
  16. package/language-service/index.js +4623 -0
  17. package/language-service/language-service.d.ts +40 -0
  18. package/package.json +9 -7
  19. package/publish.py +37 -37
  20. package/release/baselib-plain-pretty.js +2006 -229
  21. package/release/baselib-plain-ugly.js +70 -3
  22. package/release/compiler.js +11554 -3870
  23. package/release/signatures.json +31 -29
  24. package/session.vim +4 -4
  25. package/setup.cfg +2 -2
  26. package/src/ast.pyj +93 -1
  27. package/src/baselib-builtins.pyj +99 -2
  28. package/src/baselib-containers.pyj +107 -4
  29. package/src/baselib-errors.pyj +44 -0
  30. package/src/baselib-internal.pyj +124 -5
  31. package/src/baselib-itertools.pyj +97 -97
  32. package/src/baselib-str.pyj +32 -1
  33. package/src/compiler.pyj +36 -36
  34. package/src/errors.pyj +30 -30
  35. package/src/lib/aes.pyj +646 -646
  36. package/src/lib/collections.pyj +1 -1
  37. package/src/lib/copy.pyj +120 -0
  38. package/src/lib/elementmaker.pyj +83 -83
  39. package/src/lib/encodings.pyj +126 -126
  40. package/src/lib/gettext.pyj +569 -569
  41. package/src/lib/itertools.pyj +580 -580
  42. package/src/lib/math.pyj +193 -193
  43. package/src/lib/numpy.pyj +10 -10
  44. package/src/lib/operator.pyj +11 -11
  45. package/src/lib/pythonize.pyj +20 -20
  46. package/src/lib/random.pyj +118 -118
  47. package/src/lib/re.pyj +470 -470
  48. package/src/lib/react.pyj +74 -0
  49. package/src/lib/traceback.pyj +63 -63
  50. package/src/lib/uuid.pyj +77 -77
  51. package/src/monaco-language-service/analyzer.js +131 -9
  52. package/src/monaco-language-service/builtins.js +17 -2
  53. package/src/monaco-language-service/completions.js +170 -1
  54. package/src/monaco-language-service/diagnostics.js +25 -3
  55. package/src/monaco-language-service/dts.js +550 -550
  56. package/src/monaco-language-service/index.js +17 -0
  57. package/src/monaco-language-service/scope.js +3 -0
  58. package/src/output/classes.pyj +128 -11
  59. package/src/output/codegen.pyj +17 -3
  60. package/src/output/comments.pyj +45 -45
  61. package/src/output/exceptions.pyj +201 -105
  62. package/src/output/functions.pyj +13 -16
  63. package/src/output/jsx.pyj +164 -0
  64. package/src/output/literals.pyj +28 -2
  65. package/src/output/loops.pyj +0 -9
  66. package/src/output/modules.pyj +2 -5
  67. package/src/output/operators.pyj +22 -2
  68. package/src/output/statements.pyj +2 -2
  69. package/src/output/stream.pyj +1 -13
  70. package/src/output/treeshake.pyj +182 -182
  71. package/src/output/utils.pyj +72 -72
  72. package/src/parse.pyj +434 -114
  73. package/src/string_interpolation.pyj +72 -72
  74. package/src/tokenizer.pyj +29 -0
  75. package/src/unicode_aliases.pyj +576 -576
  76. package/src/utils.pyj +192 -192
  77. package/test/_import_one.pyj +37 -37
  78. package/test/_import_two/__init__.pyj +11 -11
  79. package/test/_import_two/level2/deep.pyj +4 -4
  80. package/test/_import_two/other.pyj +6 -6
  81. package/test/_import_two/sub.pyj +13 -13
  82. package/test/aes_vectors.pyj +421 -421
  83. package/test/annotations.pyj +80 -80
  84. package/test/baselib.pyj +4 -4
  85. package/test/classes.pyj +56 -17
  86. package/test/collections.pyj +5 -5
  87. package/test/decorators.pyj +77 -77
  88. package/test/docstrings.pyj +39 -39
  89. package/test/elementmaker_test.pyj +45 -45
  90. package/test/functions.pyj +151 -151
  91. package/test/generators.pyj +41 -41
  92. package/test/generic.pyj +370 -370
  93. package/test/imports.pyj +72 -72
  94. package/test/internationalization.pyj +73 -73
  95. package/test/lint.pyj +164 -164
  96. package/test/loops.pyj +85 -85
  97. package/test/numpy.pyj +734 -734
  98. package/test/omit_function_metadata.pyj +20 -20
  99. package/test/python_compat.pyj +326 -0
  100. package/test/python_features.pyj +129 -29
  101. package/test/regexp.pyj +55 -55
  102. package/test/repl.pyj +121 -121
  103. package/test/scoped_flags.pyj +76 -76
  104. package/test/slice.pyj +105 -0
  105. package/test/str.pyj +25 -0
  106. package/test/unit/fixtures/fibonacci_expected.js +1 -1
  107. package/test/unit/index.js +2296 -71
  108. package/test/unit/language-service-builtins.js +70 -0
  109. package/test/unit/language-service-bundle.js +5 -5
  110. package/test/unit/language-service-completions.js +180 -0
  111. package/test/unit/language-service-dts.js +543 -543
  112. package/test/unit/language-service-hover.js +455 -455
  113. package/test/unit/language-service-index.js +350 -0
  114. package/test/unit/language-service-scope.js +255 -0
  115. package/test/unit/language-service.js +625 -4
  116. package/test/unit/run-language-service.js +1 -0
  117. package/test/unit/web-repl.js +437 -0
  118. package/tools/build-language-service.js +2 -2
  119. package/tools/cli.js +547 -547
  120. package/tools/compile.js +219 -219
  121. package/tools/compiler.js +0 -24
  122. package/tools/completer.js +131 -131
  123. package/tools/embedded_compiler.js +251 -251
  124. package/tools/export.js +3 -37
  125. package/tools/gettext.js +185 -185
  126. package/tools/ini.js +65 -65
  127. package/tools/msgfmt.js +187 -187
  128. package/tools/repl.js +223 -223
  129. package/tools/test.js +118 -118
  130. package/tools/utils.js +128 -128
  131. package/tools/web_repl.js +95 -95
  132. package/try +41 -41
  133. package/web-repl/env.js +196 -74
  134. package/web-repl/index.html +163 -163
  135. package/web-repl/main.js +252 -254
  136. package/web-repl/prism.css +139 -139
  137. package/web-repl/prism.js +113 -113
  138. package/web-repl/rapydscript.js +227 -139
  139. package/web-repl/sha1.js +25 -25
  140. package/hack_demo.pyj +0 -112
  141. package/web-repl/language-service.js +0 -4187
@@ -185,6 +185,84 @@ var TESTS = [
185
185
  js_checks: ["Math.pow(2, 10)", "Math.pow(3, 3)", "Math.pow(10, 0)"],
186
186
  },
187
187
 
188
+ // ── List concatenation ────────────────────────────────────────────────
189
+
190
+ {
191
+ name: "list_concatenation_literals",
192
+ description: "list + list returns a new concatenated list (literal operands)",
193
+ src: [
194
+ "# globals: assrt",
195
+ "result = [1, 2] + [3, 4]",
196
+ "assrt.deepEqual(result, [1, 2, 3, 4])",
197
+ "assrt.equal(result.length, 4)",
198
+ "# originals not mutated",
199
+ "a = [1, 2]",
200
+ "b = [3, 4]",
201
+ "c = a + b",
202
+ "assrt.deepEqual(c, [1, 2, 3, 4])",
203
+ "assrt.deepEqual(a, [1, 2])",
204
+ "assrt.deepEqual(b, [3, 4])",
205
+ ].join("\n"),
206
+ js_checks: ["ρσ_list_add("],
207
+ },
208
+
209
+ {
210
+ name: "list_concatenation_variables",
211
+ description: "list + list works with variable references",
212
+ src: [
213
+ "# globals: assrt",
214
+ "a = [10, 20]",
215
+ "b = [30]",
216
+ "assrt.deepEqual(a + b, [10, 20, 30])",
217
+ "assrt.deepEqual([] + [1], [1])",
218
+ "assrt.deepEqual([1] + [], [1])",
219
+ "assrt.deepEqual([] + [], [])",
220
+ ].join("\n"),
221
+ },
222
+
223
+ {
224
+ name: "list_iadd_extends_in_place",
225
+ description: "list += list extends the list in-place (same object)",
226
+ src: [
227
+ "# globals: assrt",
228
+ "a = [1, 2]",
229
+ "ref = a",
230
+ "a += [3, 4]",
231
+ "assrt.deepEqual(a, [1, 2, 3, 4])",
232
+ "# ref still points to same object, now extended",
233
+ "assrt.deepEqual(ref, [1, 2, 3, 4])",
234
+ "assrt.ok(a is ref)",
235
+ ].join("\n"),
236
+ js_checks: ["ρσ_list_iadd("],
237
+ },
238
+
239
+ {
240
+ name: "list_concat_does_not_break_number_add",
241
+ description: "numbers still add correctly after list-concat helpers are introduced",
242
+ src: [
243
+ "# globals: assrt",
244
+ "assrt.equal(1 + 2, 3)",
245
+ "assrt.equal(3 + 4, 7)",
246
+ "assrt.equal(0 + 0, 0)",
247
+ "x = 10",
248
+ "x += 5",
249
+ "assrt.equal(x, 15)",
250
+ ].join("\n"),
251
+ },
252
+
253
+ {
254
+ name: "list_concat_does_not_break_string_concat",
255
+ description: "string concatenation still works correctly",
256
+ src: [
257
+ "# globals: assrt",
258
+ "assrt.equal('hello' + ' world', 'hello world')",
259
+ "assrt.equal('a' + 'b' + 'c', 'abc')",
260
+ "s = 'foo'",
261
+ "s += 'bar'",
262
+ "assrt.equal(s, 'foobar')",
263
+ ].join("\n"),
264
+ },
265
+
188
266
  {
189
267
  name: "not_operator",
190
268
  description: '"not" compiles to "!"',
@@ -424,8 +502,8 @@ var TESTS = [
424
502
  "increment()",
425
503
  "assrt.equal(counter, 3)",
426
504
  ].join("\n"),
427
- // nonlocal → the outer variable is accessed/modified directly
428
- js_checks: ["counter += 1"],
505
+ // nonlocal → the outer variable is accessed/modified directly via ρσ_list_iadd
506
+ js_checks: ["ρσ_list_iadd(counter,"],
429
507
  },
430
508
 
431
509
  // ── Classes ───────────────────────────────────────────────────────────
@@ -604,6 +682,119 @@ var TESTS = [
604
682
  ].join("\n"),
605
683
  },
606
684
 
685
+ // ── __new__ constructor hook ───────────────────────────────────────────
686
+
687
+ {
688
+ name: "new_basic",
689
+ description: "__new__ is called before __init__; returns an instance of the class",
690
+ src: [
691
+ "# globals: assrt",
692
+ "order = []",
693
+ "class Foo:",
694
+ " def __new__(cls):",
695
+ " order.append('new')",
696
+ " return super().__new__(cls)",
697
+ " def __init__(self):",
698
+ " order.append('init')",
699
+ " self.x = 42",
700
+ "f = Foo()",
701
+ "assrt.deepEqual(order, ['new', 'init'])",
702
+ "assrt.equal(f.x, 42)",
703
+ "assrt.ok(isinstance(f, Foo))",
704
+ ].join("\n"),
705
+ js_checks: [
706
+ "Foo.__new__(Foo, ...arguments)",
707
+ "ρσ_instance instanceof Foo",
708
+ ],
709
+ },
710
+
711
+ {
712
+ name: "new_singleton",
713
+ description: "__new__ can implement the singleton pattern",
714
+ src: [
715
+ "# globals: assrt",
716
+ "class Singleton:",
717
+ " _instance = None",
718
+ " def __new__(cls):",
719
+ " if cls._instance is None:",
720
+ " cls._instance = super().__new__(cls)",
721
+ " return cls._instance",
722
+ " def __init__(self):",
723
+ " pass",
724
+ "a = Singleton()",
725
+ "b = Singleton()",
726
+ "assrt.ok(a is b)",
727
+ "assrt.ok(isinstance(a, Singleton))",
728
+ ].join("\n"),
729
+ },
730
+
731
+ {
732
+ name: "new_returns_other_type",
733
+ description: "__new__ returning a non-class instance skips __init__",
734
+ src: [
735
+ "# globals: assrt",
736
+ "init_called = [False]",
737
+ "class MyInt:",
738
+ " def __new__(cls, val):",
739
+ " return val * 2",
740
+ " def __init__(self, val):",
741
+ " init_called[0] = True",
742
+ "result = MyInt(21)",
743
+ "assrt.equal(result, 42)",
744
+ "assrt.equal(init_called[0], False)",
745
+ ].join("\n"),
746
+ },
747
+
748
+ {
749
+ name: "new_with_args",
750
+ description: "__new__ receives the same args as __init__",
751
+ src: [
752
+ "# globals: assrt",
753
+ "class Point:",
754
+ " def __new__(cls, x, y):",
755
+ " instance = super().__new__(cls)",
756
+ " instance._raw_x = x",
757
+ " return instance",
758
+ " def __init__(self, x, y):",
759
+ " self.x = x",
760
+ " self.y = y",
761
+ "p = Point(3, 4)",
762
+ "assrt.equal(p.x, 3)",
763
+ "assrt.equal(p.y, 4)",
764
+ "assrt.equal(p._raw_x, 3)",
765
+ "assrt.ok(isinstance(p, Point))",
766
+ ].join("\n"),
767
+ },
768
+
769
+ {
770
+ name: "new_subclass_inherits",
771
+ description: "__new__ in parent class with subclass override",
772
+ src: [
773
+ "# globals: assrt",
774
+ "class Base:",
775
+ " def __new__(cls):",
776
+ " instance = super().__new__(cls)",
777
+ " instance.created_by = 'Base.__new__'",
778
+ " return instance",
779
+ " def __init__(self):",
780
+ " pass",
781
+ "class Child(Base):",
782
+ " def __new__(cls):",
783
+ " instance = super().__new__(cls)",
784
+ " instance.child_attr = 'set'",
785
+ " return instance",
786
+ " def __init__(self):",
787
+ " pass",
788
+ "b = Base()",
789
+ "assrt.equal(b.created_by, 'Base.__new__')",
790
+ "c = Child()",
791
+ "assrt.equal(c.created_by, 'Base.__new__')",
792
+ "assrt.equal(c.child_attr, 'set')",
793
+ "assrt.ok(isinstance(c, Child))",
794
+ "assrt.ok(isinstance(c, Base))",
795
+ ].join("\n"),
796
+ },
797
+
607
798
  // ── Verbatim JS ───────────────────────────────────────────────────────
608
799
 
609
800
  {
@@ -697,6 +888,102 @@ assrt.equal(fib(15), 610)
697
888
  js_checks: ["Circle.prototype", "function double(x)"],
698
889
  },
699
890
 
891
+ // ── __import__() ─────────────────────────────────────────────────────
892
+
893
+ {
894
+ name: "__import__-basic",
895
+ description: "__import__(name) returns the module object for an already-imported module",
896
+ src: [
897
+ "# globals: assrt",
898
+ "from mymodule import square",
899
+ "m = __import__('mymodule')",
900
+ "assrt.equal(m.square(4), 16)",
901
+ "assrt.equal(m.square(7), 49)",
902
+ ].join("\n"),
903
+ virtual_files: {
904
+ mymodule: [
905
+ "def square(n):",
906
+ " return n * n",
907
+ ].join("\n"),
908
+ },
909
+ js_checks: ["__import__"],
910
+ },
911
+
912
+ {
913
+ name: "__import__-via-import-stmt",
914
+ description: "__import__(name) also works when module was loaded via 'import x'",
915
+ src: [
916
+ "# globals: assrt",
917
+ "import myutils",
918
+ "m = __import__('myutils')",
919
+ "assrt.equal(m.add(3, 4), 7)",
920
+ ].join("\n"),
921
+ virtual_files: {
922
+ myutils: [
923
+ "def add(a, b):",
924
+ " return a + b",
925
+ ].join("\n"),
926
+ },
927
+ },
928
+
929
+ {
930
+ name: "__import__-dotted-no-fromlist",
931
+ description: "__import__('pkg.sub') without fromlist returns the top-level package",
932
+ src: [
933
+ "# globals: assrt",
934
+ "from pkg.utils import helper",
935
+ "top = __import__('pkg.utils')",
936
+ "assrt.equal(top.name, 'pkg')",
937
+ ].join("\n"),
938
+ virtual_files: {
939
+ "pkg": "name = 'pkg'", // pkg/__init__.pyj content; key is "pkg"
940
+ "pkg/utils": [
941
+ "def helper():",
942
+ " return 99",
943
+ ].join("\n"),
944
+ },
945
+ },
946
+
947
+ {
948
+ name: "__import__-dotted-with-fromlist",
949
+ description: "__import__('pkg.sub', fromlist=['fn']) returns the submodule",
950
+ src: [
951
+ "# globals: assrt",
952
+ "from pkg.utils import helper",
953
+ "sub = __import__('pkg.utils', None, None, ['helper'])",
954
+ "assrt.equal(sub.helper(), 99)",
955
+ ].join("\n"),
956
+ virtual_files: {
957
+ "pkg": "", // pkg/__init__.pyj; key is "pkg"
958
+ "pkg/utils": [
959
+ "def helper():",
960
+ " return 99",
961
+ ].join("\n"),
962
+ },
963
+ },
964
+
965
+ {
966
+ name: "__import__-error-on-missing",
967
+ description: "__import__ raises ModuleNotFoundError for a module not in ρσ_modules",
968
+ src: [
969
+ "# globals: assrt",
970
+ "from mymod import fn",
971
+ "caught = False",
972
+ "try:",
973
+ " __import__('does_not_exist')",
974
+ "except ModuleNotFoundError as e:",
975
+ " caught = True",
976
+ " assrt.equal(e.message, \"No module named 'does_not_exist'\")",
977
+ "assrt.ok(caught)",
978
+ ].join("\n"),
979
+ virtual_files: {
980
+ mymod: [
981
+ "def fn():",
982
+ " return 1",
983
+ ].join("\n"),
984
+ },
985
+ },
986
+
700
987
  // ── Walrus operator (:=) ──────────────────────────────────────────────
701
988
 
702
989
  {
@@ -1093,7 +1380,7 @@ assrt.equal(fib(15), 610)
1093
1380
  " return fn(x)",
1094
1381
  "assrt.equal(apply(lambda x: x * x, 5), 25)",
1095
1382
  "nums = [3, 1, 2]",
1096
- "nums.sort(lambda a, b: a - b)",
1383
+ "nums.sort()",
1097
1384
  "assrt.deepEqual(nums, [1, 2, 3])",
1098
1385
  ].join("\n"),
1099
1386
  js_checks: [],
@@ -2664,15 +2951,14 @@ assrt.equal(fib(15), 610)
2664
2951
 
2665
2952
  {
2666
2953
  name: "operator_overloading_no_flag",
2667
- description: "Without overload_operators flag operators emit native JS (no helpers in user code)",
2954
+ description: "Without overload_operators flag: + uses ρσ_list_add (not ρσ_op_add), * is native",
2668
2955
  src: [
2669
2956
  "# globals: assrt",
2670
2957
  "assrt.equal(2 + 3, 5)",
2671
2958
  "assrt.equal(3 * 4, 12)",
2672
2959
  ].join("\n"),
2673
- // Verify user-code expressions compile to native operators, not helper calls
2674
- js_checks: [/assrt\.equal\(2 \+ 3, 5\)/, /assrt\.equal\(3 \* 4, 12\)/],
2675
- js_not_checks: [],
2960
+ // + should compile to ρσ_list_add (lightweight list/number/string helper)
2961
+ js_checks: ["ρσ_list_add(2, 3)", /assrt\.equal\(3 \* 4, 12\)/],
2676
2962
  },
2677
2963
 
2678
2964
  {
@@ -2874,6 +3160,41 @@ assrt.equal(fib(15), 610)
2874
3160
  },
2875
3161
  },
2876
3162
 
3163
+ {
3164
+ name: "python_flag_truthiness_via_scoped_flags",
3165
+ description: "truthiness flag via scoped_flags wraps if-conditions with ρσ_bool(; plain code does not",
3166
+ run: function() {
3167
+ var src = "if x:\n pass";
3168
+ var src_inline = "from __python__ import truthiness\n" + src;
3169
+ var js_inline = compile(src_inline);
3170
+ var js_flagged = compile_with_flags(src, { truthiness: true });
3171
+ var js_plain = compile(src);
3172
+ assert.ok(/if\s*\(ρσ_bool\(/.test(js_inline),
3173
+ "inline import: expected if(ρσ_bool( in: " + js_inline);
3174
+ assert.ok(/if\s*\(ρσ_bool\(/.test(js_flagged),
3175
+ "scoped_flags: expected if(ρσ_bool( in: " + js_flagged);
3176
+ assert.ok(!/if\s*\(ρσ_bool\(/.test(js_plain),
3177
+ "no flag: if(ρσ_bool( should NOT appear in: " + js_plain);
3178
+ },
3179
+ },
3180
+
3181
+ {
3182
+ name: "python_flag_truthiness_via_python_flags_option",
3183
+ description: "truthiness passed as python_flags string to embedded_compiler wraps if-conditions",
3184
+ run: function() {
3185
+ var make_ec = require("../../tools/embedded_compiler.js");
3186
+ var src = "if x:\n pass";
3187
+ var ec_with = make_ec(RapydScript, baselib, null);
3188
+ var js_with = ec_with.compile(src, { python_flags: "truthiness" });
3189
+ assert.ok(/if\s*\(ρσ_bool\(/.test(js_with),
3190
+ "python_flags='truthiness': expected if(ρσ_bool( in: " + js_with);
3191
+ var ec_without = make_ec(RapydScript, baselib, null);
3192
+ var js_without = ec_without.compile(src, {});
3193
+ assert.ok(!/if\s*\(ρσ_bool\(/.test(js_without),
3194
+ "no python_flags: if(ρσ_bool( should NOT appear in: " + js_without);
3195
+ },
3196
+ },
3197
+
2877
3198
  {
2878
3199
  name: "python_flag_all_flags_runtime",
2879
3200
  description: "overload_operators + overload_getitem work correctly at runtime via scoped_flags",
@@ -2896,80 +3217,1984 @@ assrt.equal(fib(15), 610)
2896
3217
  js_checks: ["ρσ_op_add", "__getitem__"],
2897
3218
  },
2898
3219
 
2899
- ];
3220
+ // ── JSX ───────────────────────────────────────────────────────────────
2900
3221
 
2901
- // ── Runner ───────────────────────────────────────────────────────────────────
3222
+ {
3223
+ name: "jsx_basic_element",
3224
+ description: "JSX: basic element compiles to React.createElement",
3225
+ src: [
3226
+ "from __python__ import jsx",
3227
+ "def render():",
3228
+ " return <div>Hello</div>",
3229
+ ].join("\n"),
3230
+ js_checks: ["React.createElement", '"div"', '"Hello"'],
3231
+ skip_run: true,
3232
+ },
2902
3233
 
2903
- function run_tests(filter) {
2904
- var tests = filter
2905
- ? TESTS.filter(function (t) { return t.name === filter; })
2906
- : TESTS;
3234
+ {
3235
+ name: "jsx_self_closing",
3236
+ description: "JSX: self-closing element compiles to React.createElement",
3237
+ src: [
3238
+ "from __python__ import jsx",
3239
+ "def render():",
3240
+ " return <input type='text' />",
3241
+ ].join("\n"),
3242
+ js_checks: ["React.createElement", '"input"', "type"],
3243
+ skip_run: true,
3244
+ },
2907
3245
 
2908
- if (tests.length === 0) {
2909
- console.error(colored("No test found: " + filter, "red"));
2910
- process.exit(1);
2911
- }
3246
+ {
3247
+ name: "jsx_string_attribute",
3248
+ description: "JSX: string attributes become object properties",
3249
+ src: [
3250
+ "from __python__ import jsx",
3251
+ "def render():",
3252
+ ' return <div className="app" id="root">content</div>',
3253
+ ].join("\n"),
3254
+ js_checks: ["React.createElement", "className", '"app"', "id", '"root"'],
3255
+ skip_run: true,
3256
+ },
2912
3257
 
2913
- var failures = [];
3258
+ {
3259
+ name: "jsx_expression_attribute",
3260
+ description: "JSX: expression attributes compile Python to JS",
3261
+ src: [
3262
+ "from __python__ import jsx",
3263
+ "def render(isActive):",
3264
+ " return <div disabled={not isActive}>content</div>",
3265
+ ].join("\n"),
3266
+ js_checks: ["React.createElement", "disabled", "isActive"],
3267
+ skip_run: true,
3268
+ },
2914
3269
 
2915
- tests.forEach(function (test) {
3270
+ {
3271
+ name: "jsx_boolean_attribute",
3272
+ description: "JSX: boolean attributes (no value) compile to true",
3273
+ src: [
3274
+ "from __python__ import jsx",
3275
+ "def render():",
3276
+ " return <input disabled />",
3277
+ ].join("\n"),
3278
+ js_checks: ["React.createElement", "disabled", "true"],
3279
+ skip_run: true,
3280
+ },
2916
3281
 
2917
- // Custom run function (for tests that need direct JS-level control)
2918
- if (typeof test.run === "function") {
2919
- try {
2920
- test.run();
2921
- } catch (e) {
2922
- failures.push(test.name);
2923
- var msg = e.stack || String(e);
2924
- console.log(colored("FAIL " + test.name, "red") +
2925
- " [run]\n " + msg + "\n");
2926
- return;
2927
- }
2928
- console.log(colored("PASS " + test.name, "green") +
2929
- " – " + test.description);
2930
- return;
2931
- }
3282
+ {
3283
+ name: "jsx_hyphenated_attribute",
3284
+ description: "JSX: hyphenated attribute names are quoted as object keys",
3285
+ src: [
3286
+ "from __python__ import jsx",
3287
+ "def render():",
3288
+ ' return <button aria-label="Close">X</button>',
3289
+ ].join("\n"),
3290
+ js_checks: ["React.createElement", '"aria-label"', '"Close"'],
3291
+ skip_run: true,
3292
+ },
2932
3293
 
2933
- var js;
3294
+ {
3295
+ name: "jsx_expression_child",
3296
+ description: "JSX: expression children compile Python expressions to JS",
3297
+ src: [
3298
+ "from __python__ import jsx",
3299
+ "def render(count):",
3300
+ " return <h1>Count: {count * 2}</h1>",
3301
+ ].join("\n"),
3302
+ js_checks: ["React.createElement", "count * 2"],
3303
+ skip_run: true,
3304
+ },
2934
3305
 
2935
- // 1 – compile RapydScript → JS
2936
- try {
2937
- js = test.virtual_files ? compile_virtual(test.src, test.virtual_files) : compile(test.src);
2938
- } catch (e) {
2939
- failures.push(test.name);
2940
- console.log(colored("FAIL " + test.name, "red") +
2941
- " [compile error]\n " + e + "\n");
2942
- return;
2943
- }
3306
+ {
3307
+ name: "jsx_nested_elements",
3308
+ description: "JSX: nested elements produce nested React.createElement calls",
3309
+ src: [
3310
+ "from __python__ import jsx",
3311
+ "def render():",
3312
+ " return <div><span>inner</span></div>",
3313
+ ].join("\n"),
3314
+ js_checks: ["React.createElement", '"div"', '"span"', '"inner"'],
3315
+ skip_run: true,
3316
+ },
2944
3317
 
2945
- // 2 – verify expected patterns appear in the JS output
2946
- try {
2947
- check_js_patterns(test.name, js, test.js_checks);
2948
- // also check patterns that must NOT appear
2949
- (test.js_not_checks || []).forEach(function (pat) {
2950
- var found = (pat instanceof RegExp) ? pat.test(js) : js.indexOf(pat) !== -1;
2951
- if (found) {
2952
- var desc = (pat instanceof RegExp) ? String(pat) : JSON.stringify(pat);
2953
- throw new Error("compiled JS unexpectedly contains " + desc + "\n in test: " + test.name);
2954
- }
2955
- });
2956
- } catch (e) {
2957
- failures.push(test.name);
2958
- console.debug("Emitted JS:\n" + js + "\n");
2959
- console.log(colored("FAIL " + test.name, "red") +
2960
- " [JS pattern mismatch]\n " + e.message + "\n");
2961
- return;
2962
- }
3318
+ {
3319
+ name: "jsx_fragment",
3320
+ description: "JSX: fragments (<>...</>) compile to React.Fragment",
3321
+ src: [
3322
+ "from __python__ import jsx",
3323
+ "def render():",
3324
+ " return <>",
3325
+ " <span>First</span>",
3326
+ " <span>Second</span>",
3327
+ " </>",
3328
+ ].join("\n"),
3329
+ js_checks: ["React.createElement", "React.Fragment", '"First"', '"Second"'],
3330
+ skip_run: true,
3331
+ },
2963
3332
 
2964
- // 3 – run the JS; assertions embedded in src catch wrong values
2965
- try {
2966
- run_js(js);
2967
- } catch (e) {
2968
- failures.push(test.name);
2969
- var msg = e.stack || String(e);
2970
- console.log(colored("FAIL " + test.name, "red") +
2971
- " [runtime]\n " + msg + "\n");
2972
- return;
3333
+ {
3334
+ name: "jsx_component",
3335
+ description: "JSX: component tags (uppercase) are passed as references",
3336
+ src: [
3337
+ "from __python__ import jsx",
3338
+ "def render():",
3339
+ " return <MyComponent name='test' />",
3340
+ ].join("\n"),
3341
+ js_checks: ["React.createElement", "MyComponent", "name"],
3342
+ skip_run: true,
3343
+ },
3344
+
3345
+ {
3346
+ name: "jsx_dot_component",
3347
+ description: "JSX: dot-notation component tags compile as member expressions",
3348
+ src: [
3349
+ "from __python__ import jsx",
3350
+ "def render():",
3351
+ " return <Router.Route path='/home' />",
3352
+ ].join("\n"),
3353
+ js_checks: ["React.createElement", "Router.Route"],
3354
+ skip_run: true,
3355
+ },
3356
+
3357
+ {
3358
+ name: "jsx_spread_attr",
3359
+ description: "JSX: spread attributes {...props} compile to object spread",
3360
+ src: [
3361
+ "from __python__ import jsx",
3362
+ "def render(props):",
3363
+ " return <div {...props}>content</div>",
3364
+ ].join("\n"),
3365
+ js_checks: ["React.createElement", "...props"],
3366
+ skip_run: true,
3367
+ },
3368
+
3369
+ {
3370
+ name: "jsx_multiline",
3371
+ description: "JSX: multi-line JSX compiles to nested React.createElement calls",
3372
+ src: [
3373
+ "from __python__ import jsx",
3374
+ "def render(title, body):",
3375
+ " return (",
3376
+ " <article>",
3377
+ " <h2>{title}</h2>",
3378
+ " <p>{body}</p>",
3379
+ " </article>",
3380
+ " )",
3381
+ ].join("\n"),
3382
+ js_checks: ["React.createElement", '"article"', '"h2"', '"p"', "title", "body"],
3383
+ skip_run: true,
3384
+ },
3385
+
3386
+ {
3387
+ name: "jsx_no_jsx_without_flag",
3388
+ description: "JSX: < in expression is a comparison without the jsx flag",
3389
+ src: [
3390
+ "# globals: assrt",
3391
+ "x = 5",
3392
+ "assrt.equal(x < 10, True)",
3393
+ ].join("\n"),
3394
+ js_checks: ["x < 10"],
3395
+ },
3396
+
3397
+ // ── JSX whitespace and HTML entity handling ──────────────────────────────
3398
+
3399
+ {
3400
+ name: "jsx_nbsp_entity",
3401
+ description: "JSX: &nbsp; is decoded to a non-breaking space (U+00A0) in the JS string",
3402
+ src: [
3403
+ "from __python__ import jsx",
3404
+ "def render():",
3405
+ " return <p>Hello&nbsp;World</p>",
3406
+ ].join("\n"),
3407
+ js_checks: [/Hello[\u00a0]World/],
3408
+ skip_run: true,
3409
+ },
3410
+
3411
+ {
3412
+ name: "jsx_amp_entity",
3413
+ description: "JSX: &amp; is decoded to & in the JS string",
3414
+ src: [
3415
+ "from __python__ import jsx",
3416
+ "def render():",
3417
+ " return <p>a &amp; b</p>",
3418
+ ].join("\n"),
3419
+ js_checks: [/"a & b"/],
3420
+ skip_run: true,
3421
+ },
3422
+
3423
+ {
3424
+ name: "jsx_lt_gt_entities",
3425
+ description: "JSX: &lt; and &gt; are decoded to < and > in the JS string",
3426
+ src: [
3427
+ "from __python__ import jsx",
3428
+ "def render():",
3429
+ " return <p>1 &lt; 2 &gt; 0</p>",
3430
+ ].join("\n"),
3431
+ js_checks: [/"1 < 2 > 0"/],
3432
+ skip_run: true,
3433
+ },
3434
+
3435
+ {
3436
+ name: "jsx_quot_entity",
3437
+ description: "JSX: &quot; is decoded to \" in the JS string",
3438
+ src: [
3439
+ "from __python__ import jsx",
3440
+ "def render():",
3441
+ " return <p>&quot;quoted&quot;</p>",
3442
+ ].join("\n"),
3443
+ js_checks: [/null, "\\\"quoted\\\""\)/],
3444
+ skip_run: true,
3445
+ },
3446
+
3447
+ {
3448
+ name: "jsx_numeric_entity_decimal",
3449
+ description: "JSX: decimal numeric entity &#160; is decoded to U+00A0",
3450
+ src: [
3451
+ "from __python__ import jsx",
3452
+ "def render():",
3453
+ " return <p>a&#160;b</p>",
3454
+ ].join("\n"),
3455
+ js_checks: [/a[\u00a0]b/],
3456
+ skip_run: true,
3457
+ },
3458
+
3459
+ {
3460
+ name: "jsx_numeric_entity_hex",
3461
+ description: "JSX: hex numeric entity &#x00A0; is decoded to U+00A0",
3462
+ src: [
3463
+ "from __python__ import jsx",
3464
+ "def render():",
3465
+ " return <p>a&#x00A0;b</p>",
3466
+ ].join("\n"),
3467
+ js_checks: [/a[\u00a0]b/],
3468
+ skip_run: true,
3469
+ },
3470
+
3471
+ {
3472
+ name: "jsx_no_double_decode",
3473
+ description: "JSX: &amp;lt; decodes to &lt; (not <), entities decoded in one pass",
3474
+ src: [
3475
+ "from __python__ import jsx",
3476
+ "def render():",
3477
+ " return <p>&amp;lt;</p>",
3478
+ ].join("\n"),
3479
+ js_checks: [/"&lt;"/],
3480
+ skip_run: true,
3481
+ },
3482
+
3483
+ {
3484
+ name: "jsx_inline_spaces_preserved",
3485
+ description: "JSX: spaces within inline text are preserved",
3486
+ src: [
3487
+ "from __python__ import jsx",
3488
+ "def render():",
3489
+ " return <p>Hello World</p>",
3490
+ ].join("\n"),
3491
+ js_checks: [/"Hello World"/],
3492
+ skip_run: true,
3493
+ },
3494
+
3495
+ {
3496
+ name: "jsx_multiline_whitespace_collapsed",
3497
+ description: "JSX: whitespace-only lines between tags are dropped; text lines are preserved",
3498
+ src: [
3499
+ "from __python__ import jsx",
3500
+ "def render():",
3501
+ " return (",
3502
+ " <p>",
3503
+ " Hello World",
3504
+ " </p>",
3505
+ " )",
3506
+ ].join("\n"),
3507
+ js_checks: [/"Hello World"/],
3508
+ skip_run: true,
3509
+ },
3510
+
3511
+ {
3512
+ name: "jsx_single_space_same_line",
3513
+ description: "JSX: a single space on its own between same-line tags is preserved",
3514
+ src: [
3515
+ "from __python__ import jsx",
3516
+ "def render():",
3517
+ " return <span>a </span>",
3518
+ ].join("\n"),
3519
+ js_checks: [/"a "/],
3520
+ skip_run: true,
3521
+ },
3522
+
3523
+ // ── React standard library ───────────────────────────────────────────────
3524
+
3525
+ {
3526
+ name: "react_import_usestate",
3527
+ description: "react lib: from react import useState compiles to React.useState reference",
3528
+ src: [
3529
+ "from react import useState",
3530
+ "def Counter():",
3531
+ " count, setCount = useState(0)",
3532
+ " return count",
3533
+ ].join("\n"),
3534
+ js_checks: ["React.useState", "useState"],
3535
+ skip_run: true,
3536
+ },
3537
+
3538
+ {
3539
+ name: "react_import_multiple_hooks",
3540
+ description: "react lib: multiple hook imports each resolve to React.*",
3541
+ src: [
3542
+ "from react import useState, useEffect, useMemo, useRef, useCallback",
3543
+ "def Component(items):",
3544
+ " count, setCount = useState(0)",
3545
+ " ref = useRef(None)",
3546
+ " result = useMemo(def(): return items.length;, [items])",
3547
+ " def cb():",
3548
+ " setCount(count + 1)",
3549
+ " fn = useCallback(cb, [count])",
3550
+ " useEffect(def(): pass;, [])",
3551
+ " return count",
3552
+ ].join("\n"),
3553
+ js_checks: [
3554
+ "React.useState", "React.useEffect", "React.useMemo",
3555
+ "React.useRef", "React.useCallback",
3556
+ ],
3557
+ skip_run: true,
3558
+ },
3559
+
3560
+ {
3561
+ name: "react_jsx_functional_component",
3562
+ description: "react lib: functional component with useState and JSX",
3563
+ src: [
3564
+ "from __python__ import jsx",
3565
+ "from react import useState",
3566
+ "def Counter():",
3567
+ " count, setCount = useState(0)",
3568
+ " def increment():",
3569
+ " setCount(count + 1)",
3570
+ " return <button onClick={increment}>{count}</button>",
3571
+ ].join("\n"),
3572
+ js_checks: [
3573
+ "React.useState", "React.createElement",
3574
+ '"button"', "increment", "count",
3575
+ ],
3576
+ skip_run: true,
3577
+ },
3578
+
3579
+ {
3580
+ name: "react_use_effect",
3581
+ description: "react lib: useEffect with deps array compiles correctly",
3582
+ src: [
3583
+ "from react import useState, useEffect",
3584
+ "def Timer():",
3585
+ " count, setCount = useState(0)",
3586
+ " def tick():",
3587
+ " setCount(count + 1)",
3588
+ " useEffect(tick, [count])",
3589
+ " return count",
3590
+ ].join("\n"),
3591
+ js_checks: ["React.useState", "React.useEffect", "tick", "count"],
3592
+ skip_run: true,
3593
+ },
3594
+
3595
+ {
3596
+ name: "react_use_context",
3597
+ description: "react lib: createContext and useContext compile correctly",
3598
+ src: [
3599
+ "from react import createContext, useContext",
3600
+ "ThemeContext = createContext('light')",
3601
+ "def ThemedButton():",
3602
+ " theme = useContext(ThemeContext)",
3603
+ " return theme",
3604
+ ].join("\n"),
3605
+ js_checks: [
3606
+ "React.createContext", "React.useContext",
3607
+ "ThemeContext", "light",
3608
+ ],
3609
+ skip_run: true,
3610
+ },
3611
+
3612
+ {
3613
+ name: "react_use_reducer",
3614
+ description: "react lib: useReducer with action dispatch compiles correctly",
3615
+ src: [
3616
+ "from react import useReducer",
3617
+ "def reducer(state, action):",
3618
+ " if action.type == 'increment':",
3619
+ " return state + 1",
3620
+ " return state",
3621
+ "def Counter():",
3622
+ " state, dispatch = useReducer(reducer, 0)",
3623
+ " return state",
3624
+ ].join("\n"),
3625
+ js_checks: ["React.useReducer", "reducer", "dispatch"],
3626
+ skip_run: true,
3627
+ },
3628
+
3629
+ {
3630
+ name: "react_use_ref",
3631
+ description: "react lib: useRef for DOM reference compiles correctly",
3632
+ src: [
3633
+ "from __python__ import jsx",
3634
+ "from react import useRef",
3635
+ "def FocusInput():",
3636
+ " inputRef = useRef(None)",
3637
+ " def handleClick():",
3638
+ " inputRef.current.focus()",
3639
+ " return <input ref={inputRef} />",
3640
+ ].join("\n"),
3641
+ js_checks: [
3642
+ "React.useRef", "inputRef", "current",
3643
+ "React.createElement", '"input"',
3644
+ ],
3645
+ skip_run: true,
3646
+ },
3647
+
3648
+ {
3649
+ name: "react_memo_wrapper",
3650
+ description: "react lib: memo() wraps a component to prevent re-renders",
3651
+ src: [
3652
+ "from __python__ import jsx",
3653
+ "from react import memo",
3654
+ "def Row(props):",
3655
+ " return <div>{props.label}</div>",
3656
+ "MemoRow = memo(Row)",
3657
+ ].join("\n"),
3658
+ js_checks: ["React.memo", "Row", "MemoRow"],
3659
+ skip_run: true,
3660
+ },
3661
+
3662
+ {
3663
+ name: "react_create_context",
3664
+ description: "react lib: createContext creates a context object",
3665
+ src: [
3666
+ "from react import createContext",
3667
+ "UserContext = createContext(None)",
3668
+ ].join("\n"),
3669
+ js_checks: ["React.createContext", "UserContext"],
3670
+ skip_run: true,
3671
+ },
3672
+
3673
+ {
3674
+ name: "react_forward_ref",
3675
+ description: "react lib: forwardRef passes ref to child component",
3676
+ src: [
3677
+ "from __python__ import jsx",
3678
+ "from react import forwardRef",
3679
+ "def FancyInput(props, ref):",
3680
+ " return <input ref={ref} />",
3681
+ "FancyInputWithRef = forwardRef(FancyInput)",
3682
+ ].join("\n"),
3683
+ js_checks: ["React.forwardRef", "FancyInput", "FancyInputWithRef"],
3684
+ skip_run: true,
3685
+ },
3686
+
3687
+ {
3688
+ name: "react_fragment_import",
3689
+ description: "react lib: Fragment import can be used as a component tag",
3690
+ src: [
3691
+ "from __python__ import jsx",
3692
+ "from react import Fragment",
3693
+ "def TwoItems():",
3694
+ " return <Fragment><span>A</span><span>B</span></Fragment>",
3695
+ ].join("\n"),
3696
+ js_checks: ["React.Fragment", "React.createElement", '"span"'],
3697
+ skip_run: true,
3698
+ },
3699
+
3700
+ {
3701
+ name: "react_use_id",
3702
+ description: "react lib: useId (React 18) compiles correctly",
3703
+ src: [
3704
+ "from react import useId",
3705
+ "def LabeledInput():",
3706
+ " id = useId()",
3707
+ " return id",
3708
+ ].join("\n"),
3709
+ js_checks: ["React.useId", "useId"],
3710
+ skip_run: true,
3711
+ },
3712
+
3713
+ {
3714
+ name: "react_use_transition",
3715
+ description: "react lib: useTransition (React 18) compiles correctly",
3716
+ src: [
3717
+ "from react import useState, useTransition",
3718
+ "def SearchInput():",
3719
+ " isPending, startTransition = useTransition()",
3720
+ " query, setQuery = useState('')",
3721
+ " def handleChange(e):",
3722
+ " startTransition(def(): setQuery(e.target.value);)",
3723
+ " return isPending",
3724
+ ].join("\n"),
3725
+ js_checks: ["React.useTransition", "React.useState", "startTransition"],
3726
+ skip_run: true,
3727
+ },
3728
+
3729
+ {
3730
+ name: "react_class_component",
3731
+ description: "react lib: class component importing Component base class",
3732
+ src: [
3733
+ "from __python__ import jsx",
3734
+ "from react import Component",
3735
+ "class Greeter(Component):",
3736
+ " def render(self):",
3737
+ " return <h1>Hello, {self.props.name}</h1>",
3738
+ ].join("\n"),
3739
+ js_checks: [
3740
+ "React.Component", "Greeter", "render",
3741
+ "React.createElement", '"h1"',
3742
+ ],
3743
+ skip_run: true,
3744
+ },
3745
+
3746
+ {
3747
+ name: "react_jsx_list_rendering",
3748
+ description: "react lib: list comprehension renders JSX list of elements",
3749
+ src: [
3750
+ "from __python__ import jsx",
3751
+ "from react import useState",
3752
+ "def TodoList():",
3753
+ " items, setItems = useState(['a', 'b', 'c'])",
3754
+ " return (",
3755
+ " <ul>",
3756
+ " {[<li key={i}>{item}</li> for i, item in enumerate(items)]}",
3757
+ " </ul>",
3758
+ " )",
3759
+ ].join("\n"),
3760
+ js_checks: [
3761
+ "React.useState", "React.createElement",
3762
+ '"ul"', '"li"', "enumerate",
3763
+ ],
3764
+ skip_run: true,
3765
+ },
3766
+
3767
+ // Binding-pattern tests: verify that imported names are wired to React.* in
3768
+ // the compiled output via both the module export and the var binding.
3769
+
3770
+ {
3771
+ name: "react_binding_memo",
3772
+ description: "react lib: memo is exported from module as React.memo and bound via var",
3773
+ src: [
3774
+ "from react import memo",
3775
+ "def A(): return 1",
3776
+ "B = memo(A)",
3777
+ ].join("\n"),
3778
+ // module init: ρσ_modules.react.memo = <something>; and React.memo assignment
3779
+ // import binding: var memo = ρσ_modules.react.memo
3780
+ js_checks: [
3781
+ "React.memo",
3782
+ "ρσ_modules.react.memo",
3783
+ "var memo",
3784
+ "memo(A)",
3785
+ ],
3786
+ skip_run: true,
3787
+ },
3788
+
3789
+ {
3790
+ name: "react_binding_usestate",
3791
+ description: "react lib: useState is exported from module as React.useState and bound via var",
3792
+ src: [
3793
+ "from react import useState",
3794
+ "def C():",
3795
+ " n, setN = useState(0)",
3796
+ " return n",
3797
+ ].join("\n"),
3798
+ js_checks: [
3799
+ "React.useState",
3800
+ "ρσ_modules.react.useState",
3801
+ "var useState",
3802
+ "useState(0)",
3803
+ ],
3804
+ skip_run: true,
3805
+ },
3806
+
3807
+ {
3808
+ name: "react_binding_useeffect",
3809
+ description: "react lib: useEffect is exported from module as React.useEffect and bound via var",
3810
+ src: [
3811
+ "from react import useEffect",
3812
+ "def D():",
3813
+ " def run(): pass",
3814
+ " useEffect(run, [])",
3815
+ ].join("\n"),
3816
+ js_checks: [
3817
+ "React.useEffect",
3818
+ "ρσ_modules.react.useEffect",
3819
+ "var useEffect",
3820
+ "useEffect(run",
3821
+ ],
3822
+ skip_run: true,
3823
+ },
3824
+
3825
+ {
3826
+ name: "react_binding_forwardref",
3827
+ description: "react lib: forwardRef is exported from module as React.forwardRef and bound via var",
3828
+ src: [
3829
+ "from __python__ import jsx",
3830
+ "from react import forwardRef",
3831
+ "FancyInput = forwardRef(def(props, ref): return <input ref={ref}/>;)",
3832
+ ].join("\n"),
3833
+ js_checks: [
3834
+ "React.forwardRef",
3835
+ "ρσ_modules.react.forwardRef",
3836
+ "var forwardRef",
3837
+ "forwardRef(",
3838
+ ],
3839
+ skip_run: true,
3840
+ },
3841
+
3842
+ {
3843
+ name: "react_binding_all_hooks",
3844
+ description: "react lib: every imported hook produces a var binding to ρσ_modules.react.*",
3845
+ src: [
3846
+ "from react import useState, useEffect, useContext, useReducer, useCallback, useMemo, useRef, useLayoutEffect, useId",
3847
+ ].join("\n"),
3848
+ js_checks: [
3849
+ "React.useState", "React.useEffect", "React.useContext",
3850
+ "React.useReducer", "React.useCallback", "React.useMemo",
3851
+ "React.useRef", "React.useLayoutEffect", "React.useId",
3852
+ "var useState", "var useEffect", "var useContext",
3853
+ "var useReducer", "var useCallback", "var useMemo",
3854
+ "var useRef", "var useLayoutEffect", "var useId",
3855
+ ],
3856
+ skip_run: true,
3857
+ },
3858
+
3859
+ {
3860
+ name: "react_counter_example",
3861
+ description: "react lib: full counter component from TODO example compiles correctly",
3862
+ src: [
3863
+ "from __python__ import jsx",
3864
+ "from react import useState, useEffect, memo",
3865
+ "def Counter(props):",
3866
+ " count, setCount = useState(props.initial or 0)",
3867
+ " def increment():",
3868
+ " setCount(count + 1)",
3869
+ " def decrement():",
3870
+ " setCount(count - 1)",
3871
+ " def log_change():",
3872
+ " pass",
3873
+ " useEffect(log_change, [count])",
3874
+ " return (",
3875
+ " <div className='counter'>",
3876
+ " <h2>{props.title}</h2>",
3877
+ " <button onClick={decrement}>-</button>",
3878
+ " <span>{count}</span>",
3879
+ " <button onClick={increment}>+</button>",
3880
+ " </div>",
3881
+ " )",
3882
+ "Counter = memo(Counter)",
3883
+ ].join("\n"),
3884
+ js_checks: [
3885
+ // hooks are wired correctly
3886
+ "React.useState", "React.useEffect", "React.memo",
3887
+ "var useState", "var useEffect", "var memo",
3888
+ // JSX output
3889
+ "React.createElement", '"div"', '"button"', '"span"', '"h2"',
3890
+ // logic
3891
+ "increment", "decrement", "count",
3892
+ ],
3893
+ skip_run: true,
3894
+ },
3895
+
3896
+ {
3897
+ name: "react_lazy_suspense",
3898
+ description: "react lib: lazy and Suspense compile correctly",
3899
+ src: [
3900
+ "from __python__ import jsx",
3901
+ "from react import lazy, Suspense",
3902
+ "def Fallback():",
3903
+ " return <div>Loading...</div>",
3904
+ "def App():",
3905
+ " return <Suspense fallback={<Fallback/>}></Suspense>",
3906
+ ].join("\n"),
3907
+ js_checks: [
3908
+ "React.lazy", "React.Suspense",
3909
+ "var lazy", "var Suspense",
3910
+ "React.createElement",
3911
+ ],
3912
+ skip_run: true,
3913
+ },
3914
+
3915
+ {
3916
+ name: "react_use_callback_deps",
3917
+ description: "react lib: useCallback dependency array passes through correctly",
3918
+ src: [
3919
+ "from react import useState, useCallback",
3920
+ "def Form():",
3921
+ " value, setValue = useState('')",
3922
+ " def onChange(e):",
3923
+ " setValue(e.target.value)",
3924
+ " handler = useCallback(onChange, [value])",
3925
+ " return handler",
3926
+ ].join("\n"),
3927
+ js_checks: [
3928
+ "React.useCallback", "React.useState",
3929
+ "var useCallback", "var useState",
3930
+ "useCallback(onChange",
3931
+ ],
3932
+ skip_run: true,
3933
+ },
3934
+
3935
+ {
3936
+ name: "react_use_memo_deps",
3937
+ description: "react lib: useMemo dependency array passes through correctly",
3938
+ src: [
3939
+ "from react import useState, useMemo",
3940
+ "def Expensive(items):",
3941
+ " count, setCount = useState(0)",
3942
+ " def compute(): return items.length * count",
3943
+ " result = useMemo(compute, [items, count])",
3944
+ " return result",
3945
+ ].join("\n"),
3946
+ js_checks: [
3947
+ "React.useMemo", "React.useState",
3948
+ "var useMemo", "var useState",
3949
+ "useMemo(compute",
3950
+ ],
3951
+ skip_run: true,
3952
+ },
3953
+
3954
+ // ── JSON support ─────────────────────────────────────────────────────────
3955
+
3956
+ {
3957
+ name: "json_stringify_dict",
3958
+ description: "JSON.stringify on a Python dict produces valid JSON",
3959
+ src: [
3960
+ "# globals: assrt",
3961
+ "from __python__ import dict_literals",
3962
+ "d = {'key': 'value', 'num': 42}",
3963
+ "s = JSON.stringify(d)",
3964
+ "assrt.equal(jstype(s), 'string')",
3965
+ "parsed = JSON.parse(s)",
3966
+ "assrt.ok(isinstance(parsed, dict))",
3967
+ "assrt.equal(parsed.get('key'), 'value')",
3968
+ "assrt.equal(parsed.get('num'), 42)",
3969
+ ].join("\n"),
3970
+ },
3971
+
3972
+ {
3973
+ name: "json_parse_returns_dict",
3974
+ description: "JSON.parse returns ρσ_dict instances for objects",
3975
+ src: [
3976
+ "# globals: assrt",
3977
+ "parsed = JSON.parse('{\"a\": 1, \"b\": 2}')",
3978
+ "assrt.ok(isinstance(parsed, dict))",
3979
+ "assrt.equal(parsed.get('a'), 1)",
3980
+ "assrt.equal(parsed.get('b'), 2)",
3981
+ "assrt.equal(len(parsed), 2)",
3982
+ ].join("\n"),
3983
+ // JSON.parse in RapydScript must compile to ρσ_json_parse (not the native global)
3984
+ js_checks: ["ρσ_json_parse"],
3985
+ },
3986
+
3987
+ {
3988
+ name: "json_stringify_nested_dict",
3989
+ description: "JSON.stringify and parse handle nested dicts correctly",
3990
+ src: [
3991
+ "# globals: assrt",
3992
+ "from __python__ import dict_literals",
3993
+ "outer = {'inner': {'x': 1, 'y': 2}}",
3994
+ "s = JSON.stringify(outer)",
3995
+ "parsed = JSON.parse(s)",
3996
+ "assrt.ok(isinstance(parsed, dict))",
3997
+ "inner = parsed.get('inner')",
3998
+ "assrt.ok(isinstance(inner, dict))",
3999
+ "assrt.equal(inner.get('x'), 1)",
4000
+ "assrt.equal(inner.get('y'), 2)",
4001
+ ].join("\n"),
4002
+ },
4003
+
4004
+ {
4005
+ name: "json_dict_with_list_values",
4006
+ description: "JSON.stringify/parse handles dicts with list values",
4007
+ src: [
4008
+ "# globals: assrt",
4009
+ "from __python__ import dict_literals",
4010
+ "d = {'items': [1, 2, 3], 'name': 'test'}",
4011
+ "s = JSON.stringify(d)",
4012
+ "parsed = JSON.parse(s)",
4013
+ "assrt.ok(isinstance(parsed, dict))",
4014
+ "items = parsed.get('items')",
4015
+ "assrt.ok(Array.isArray(items))",
4016
+ "assrt.equal(items[0], 1)",
4017
+ "assrt.equal(items[2], 3)",
4018
+ ].join("\n"),
4019
+ },
4020
+
4021
+ {
4022
+ name: "json_dict_null_bool_values",
4023
+ description: "JSON.stringify/parse handles None, True, False values",
4024
+ src: [
4025
+ "# globals: assrt",
4026
+ "from __python__ import dict_literals",
4027
+ "d = {'a': None, 'b': True, 'c': False}",
4028
+ "s = JSON.stringify(d)",
4029
+ "parsed = JSON.parse(s)",
4030
+ "assrt.ok(isinstance(parsed, dict))",
4031
+ "assrt.equal(parsed.get('a'), None)",
4032
+ "assrt.equal(parsed.get('b'), True)",
4033
+ "assrt.equal(parsed.get('c'), False)",
4034
+ ].join("\n"),
4035
+ },
4036
+
4037
+ {
4038
+ name: "json_parse_array_of_dicts",
4039
+ description: "JSON.parse converts objects inside arrays to dicts",
4040
+ src: [
4041
+ "# globals: assrt",
4042
+ "arr = JSON.parse('[{\"x\": 1}, {\"y\": 2}]')",
4043
+ "assrt.ok(Array.isArray(arr))",
4044
+ "assrt.ok(isinstance(arr[0], dict))",
4045
+ "assrt.ok(isinstance(arr[1], dict))",
4046
+ "assrt.equal(arr[0].get('x'), 1)",
4047
+ "assrt.equal(arr[1].get('y'), 2)",
4048
+ ].join("\n"),
4049
+ },
4050
+
4051
+ {
4052
+ name: "json_roundtrip_dict_comprehension",
4053
+ description: "JSON round-trip works with dict comprehensions",
4054
+ src: [
4055
+ "# globals: assrt",
4056
+ "from __python__ import dict_literals",
4057
+ "d = {str(i): i * i for i in range(4)}",
4058
+ "s = JSON.stringify(d)",
4059
+ "parsed = JSON.parse(s)",
4060
+ "assrt.ok(isinstance(parsed, dict))",
4061
+ "assrt.equal(parsed.get('0'), 0)",
4062
+ "assrt.equal(parsed.get('2'), 4)",
4063
+ "assrt.equal(parsed.get('3'), 9)",
4064
+ ].join("\n"),
4065
+ },
4066
+
4067
+ // ── __hash__ dunder ───────────────────────────────────────────────────
4068
+
4069
+ {
4070
+ name: "hash_basic",
4071
+ description: "def __hash__ in a class is dispatched by hash() builtin",
4072
+ src: [
4073
+ "# globals: assrt",
4074
+ "class Point:",
4075
+ " def __init__(self, x, y):",
4076
+ " self.x = x",
4077
+ " self.y = y",
4078
+ " def __hash__(self):",
4079
+ " return hash(self.x) ^ hash(self.y)",
4080
+ "p1 = Point(1, 2)",
4081
+ "p2 = Point(1, 2)",
4082
+ "p3 = Point(3, 4)",
4083
+ "assrt.equal(hash(p1), hash(p2))",
4084
+ "assrt.notEqual(hash(p1), hash(p3))",
4085
+ ].join("\n"),
4086
+ js_checks: ["Point.prototype.__hash__"],
4087
+ },
4088
+
4089
+ {
4090
+ name: "hash_identity",
4091
+ description: "class without __hash__ gets a stable identity hash",
4092
+ src: [
4093
+ "# globals: assrt",
4094
+ "class Foo:",
4095
+ " def __init__(self, x):",
4096
+ " self.x = x",
4097
+ "a = Foo(1)",
4098
+ "b = Foo(1)",
4099
+ "h_a1 = hash(a)",
4100
+ "h_a2 = hash(a)",
4101
+ "h_b = hash(b)",
4102
+ "assrt.equal(h_a1, h_a2)",
4103
+ "assrt.notEqual(h_a1, h_b)",
4104
+ ].join("\n"),
4105
+ },
4106
+
4107
+ {
4108
+ name: "hash_unhashable_via_eq",
4109
+ description: "class that defines __eq__ without __hash__ becomes unhashable (TypeError)",
4110
+ src: [
4111
+ "# globals: assrt",
4112
+ "class Bar:",
4113
+ " def __init__(self, v):",
4114
+ " self.v = v",
4115
+ " def __eq__(self, other):",
4116
+ " return self.v == other.v",
4117
+ "b = Bar(1)",
4118
+ "caught = False",
4119
+ "try:",
4120
+ " hash(b)",
4121
+ "except TypeError:",
4122
+ " caught = True",
4123
+ "assrt.ok(caught, 'hash(Bar()) should raise TypeError')",
4124
+ ].join("\n"),
4125
+ js_checks: [".prototype.__hash__ = null"],
4126
+ },
4127
+
4128
+ {
4129
+ name: "hash_explicit_eq_and_hash",
4130
+ description: "class that defines both __eq__ and __hash__ is hashable",
4131
+ src: [
4132
+ "# globals: assrt",
4133
+ "class Key:",
4134
+ " def __init__(self, v):",
4135
+ " self.v = v",
4136
+ " def __eq__(self, other):",
4137
+ " return self.v == other.v",
4138
+ " def __hash__(self):",
4139
+ " return hash(self.v)",
4140
+ "k1 = Key('x')",
4141
+ "k2 = Key('x')",
4142
+ "assrt.equal(hash(k1), hash(k2))",
4143
+ ].join("\n"),
4144
+ js_checks: ["Key.prototype.__hash__"],
4145
+ },
4146
+
4147
+ {
4148
+ name: "hash_primitives",
4149
+ description: "hash() of primitives follows Python semantics",
4150
+ src: [
4151
+ "# globals: assrt",
4152
+ "assrt.equal(hash(None), 0)",
4153
+ "assrt.equal(hash(True), 1)",
4154
+ "assrt.equal(hash(False), 0)",
4155
+ "assrt.equal(hash(42), 42)",
4156
+ "assrt.equal(hash(42.0), 42)",
4157
+ "assrt.equal(jstype(hash('hello')), 'number')",
4158
+ "assrt.equal(hash('hello'), hash('hello'))",
4159
+ ].join("\n"),
4160
+ },
4161
+
4162
+ // ── __getattr__ / __setattr__ / __delattr__ / __getattribute__ ────────────
4163
+
4164
+ {
4165
+ name: "getattr_dunder_basic",
4166
+ description: "__getattr__ is called as fallback when an attribute is not found",
4167
+ src: [
4168
+ "# globals: assrt",
4169
+ "class Bag:",
4170
+ " def __getattr__(self, name):",
4171
+ " return 'missing_' + name",
4172
+ "b = Bag()",
4173
+ "# Missing attributes fall back to __getattr__.",
4174
+ "assrt.equal(b.foo, 'missing_foo')",
4175
+ "assrt.equal(b.bar, 'missing_bar')",
4176
+ ].join("\n"),
4177
+ js_checks: ["ρσ_attr_proxy_handler"],
4178
+ },
4179
+
4180
+ {
4181
+ name: "setattr_dunder_basic",
4182
+ description: "__setattr__ intercepts all attribute assignments including those in __init__",
4183
+ src: [
4184
+ "# globals: assrt",
4185
+ "class Recorder:",
4186
+ " def __init__(self):",
4187
+ " # Each assignment goes through __setattr__.",
4188
+ " self.x = 10",
4189
+ " def __setattr__(self, name, value):",
4190
+ " # Store doubled numeric values via bypass.",
4191
+ " if jstype(value) is 'number':",
4192
+ " object.__setattr__(self, name, value * 2)",
4193
+ " else:",
4194
+ " object.__setattr__(self, name, value)",
4195
+ "r = Recorder()",
4196
+ "# x was doubled by __setattr__ during __init__.",
4197
+ "assrt.equal(r.x, 20, 'x doubled by __setattr__ in __init__')",
4198
+ "r.y = 7",
4199
+ "assrt.equal(r.y, 14, 'y doubled by __setattr__')",
4200
+ "r.name = 'hello'",
4201
+ "assrt.equal(r.name, 'hello', 'string stored as-is')",
4202
+ ].join("\n"),
4203
+ js_checks: ["ρσ_attr_proxy_handler", "ρσ_object_setattr"],
4204
+ },
4205
+
4206
+ {
4207
+ name: "delattr_dunder_basic",
4208
+ description: "__delattr__ intercepts del obj.attr",
4209
+ src: [
4210
+ "# globals: assrt",
4211
+ "class Guarded:",
4212
+ " def __init__(self):",
4213
+ " # Track deleted names.",
4214
+ " object.__setattr__(self, 'deleted', [])",
4215
+ " def __setattr__(self, name, value):",
4216
+ " object.__setattr__(self, name, value)",
4217
+ " def __delattr__(self, name):",
4218
+ " self.deleted.append(name)",
4219
+ " object.__delattr__(self, name)",
4220
+ "g = Guarded()",
4221
+ "g.x = 5",
4222
+ "assrt.equal(g.x, 5)",
4223
+ "del g.x",
4224
+ "assrt.ok(g.deleted.indexOf('x') >= 0, 'x recorded as deleted by __delattr__')",
4225
+ "# After deletion the attribute is gone (returns undefined).",
4226
+ "assrt.equal(g.x, undefined, 'x is gone after del')",
4227
+ ].join("\n"),
4228
+ js_checks: ["ρσ_attr_proxy_handler"],
4229
+ },
4230
+
4231
+ {
4232
+ name: "getattribute_dunder_basic",
4233
+ description: "__getattribute__ overrides ALL attribute access",
4234
+ src: [
4235
+ "# globals: assrt",
4236
+ "class AllCaps:",
4237
+ " def __init__(self):",
4238
+ " object.__setattr__(self, 'value', 'hello')",
4239
+ " def __getattribute__(self, name):",
4240
+ " # Use object.__getattribute__ (compiles to ρσ_object_getattr) to bypass the hook.",
4241
+ " raw = object.__getattribute__(self, name)",
4242
+ " if jstype(raw) is 'string':",
4243
+ " return raw.toUpperCase()",
4244
+ " return raw",
4245
+ "a = AllCaps()",
4246
+ "assrt.equal(a.value, 'HELLO', '__getattribute__ transforms string values')",
4247
+ ].join("\n"),
4248
+ js_checks: ["ρσ_attr_proxy_handler"],
4249
+ },
4250
+
4251
+ {
4252
+ name: "getattribute_with_getattr_fallback",
4253
+ description: "__getattribute__ raising AttributeError falls back to __getattr__",
4254
+ src: [
4255
+ "# globals: assrt",
4256
+ "class Fallback:",
4257
+ " def __init__(self):",
4258
+ " object.__setattr__(self, 'real', 1)",
4259
+ " def __getattribute__(self, name):",
4260
+ " if name is 'real':",
4261
+ " return object.__getattribute__(self, name)",
4262
+ " raise AttributeError(name)",
4263
+ " def __getattr__(self, name):",
4264
+ " return 'fallback'",
4265
+ "f = Fallback()",
4266
+ "assrt.equal(f.real, 1, 'real attribute via __getattribute__')",
4267
+ "assrt.equal(f.anything, 'fallback', 'unknown attribute via __getattr__ fallback')",
4268
+ ].join("\n"),
4269
+ js_checks: ["ρσ_attr_proxy_handler"],
4270
+ },
4271
+
4272
+ {
4273
+ name: "setattr_object_setattr_bypass",
4274
+ description: "object.__setattr__ bypasses __setattr__ to avoid infinite recursion",
4275
+ src: [
4276
+ "# globals: assrt",
4277
+ "class Doubler:",
4278
+ " def __init__(self):",
4279
+ " self.x = 5",
4280
+ " def __setattr__(self, name, value):",
4281
+ " # Double all numeric values, then store directly.",
4282
+ " if jstype(value) is 'number':",
4283
+ " object.__setattr__(self, name, value * 2)",
4284
+ " else:",
4285
+ " object.__setattr__(self, name, value)",
4286
+ "d = Doubler()",
4287
+ "assrt.equal(d.x, 10, 'value doubled by __setattr__')",
4288
+ "d.y = 3",
4289
+ "assrt.equal(d.y, 6)",
4290
+ "d.label = 'alice'",
4291
+ "assrt.equal(d.label, 'alice', 'string value stored as-is')",
4292
+ ].join("\n"),
4293
+ },
4294
+
4295
+ {
4296
+ name: "attr_dunders_inheritance",
4297
+ description: "__getattr__ is inherited by subclasses",
4298
+ src: [
4299
+ "# globals: assrt",
4300
+ "class Base:",
4301
+ " def __getattr__(self, name):",
4302
+ " return 'from_base'",
4303
+ "class Child(Base):",
4304
+ " def __init__(self):",
4305
+ " self.own = 'child_own'",
4306
+ "c = Child()",
4307
+ "assrt.equal(c.own, 'child_own', 'own attribute still direct')",
4308
+ "assrt.equal(c.missing, 'from_base', '__getattr__ inherited from Base')",
4309
+ ].join("\n"),
4310
+ js_checks: ["ρσ_attr_proxy_handler"],
4311
+ },
4312
+
4313
+ {
4314
+ name: "attr_dunders_getattr_with_setattr",
4315
+ description: "__setattr__ stores via bypass, __getattr__ reads back",
4316
+ src: [
4317
+ "# globals: assrt",
4318
+ "class AttrStore:",
4319
+ " def __init__(self):",
4320
+ " object.__setattr__(self, '_store', {})",
4321
+ " def __setattr__(self, name, value):",
4322
+ " self._store[name] = value",
4323
+ " def __getattr__(self, name):",
4324
+ " if name in self._store:",
4325
+ " return self._store[name]",
4326
+ " raise AttributeError(name)",
4327
+ "d = AttrStore()",
4328
+ "d.x = 1",
4329
+ "d.y = 2",
4330
+ "assrt.equal(d.x, 1)",
4331
+ "assrt.equal(d.y, 2)",
4332
+ "caught = False",
4333
+ "try:",
4334
+ " _ = d.z",
4335
+ "except AttributeError:",
4336
+ " caught = True",
4337
+ "assrt.ok(caught, 'missing attr raises AttributeError')",
4338
+ ].join("\n"),
4339
+ },
4340
+
4341
+ // ── __class_getitem__ ─────────────────────────────────────────────────
4342
+
4343
+ {
4344
+ name: "class_getitem_basic",
4345
+ description: "Class[item] calls __class_getitem__(cls, item) and returns the result",
4346
+ src: [
4347
+ "# globals: assrt",
4348
+ "class Box:",
4349
+ " def __class_getitem__(cls, item):",
4350
+ " return cls.__name__ + '[' + str(item) + ']'",
4351
+ "assrt.equal(Box[42], 'Box[42]')",
4352
+ "assrt.equal(Box['x'], 'Box[x]')",
4353
+ ].join("\n"),
4354
+ js_checks: ["Box.__class_getitem__("],
4355
+ },
4356
+
4357
+ {
4358
+ name: "class_getitem_cls_is_class",
4359
+ description: "__class_getitem__ receives the class as cls; can return it",
4360
+ src: [
4361
+ "# globals: assrt",
4362
+ "class Stack:",
4363
+ " def __class_getitem__(cls, item):",
4364
+ " return cls",
4365
+ "assrt.ok(Stack[int] is Stack)",
4366
+ "assrt.ok(Stack[str] is Stack)",
4367
+ ].join("\n"),
4368
+ },
4369
+
4370
+ {
4371
+ name: "class_getitem_subclass_inherits",
4372
+ description: "subclass without __class_getitem__ inherits it from parent; cls is the subclass",
4373
+ src: [
4374
+ "# globals: assrt",
4375
+ "class Base:",
4376
+ " def __class_getitem__(cls, item):",
4377
+ " return cls.__name__ + '<' + str(item) + '>'",
4378
+ "class Child(Base):",
4379
+ " pass",
4380
+ "assrt.equal(Base[42], 'Base<42>')",
4381
+ "assrt.equal(Child[42], 'Child<42>')",
4382
+ ].join("\n"),
4383
+ },
4384
+
4385
+ {
4386
+ name: "class_getitem_subclass_overrides",
4387
+ description: "subclass can override __class_getitem__",
4388
+ src: [
4389
+ "# globals: assrt",
4390
+ "class Base:",
4391
+ " def __class_getitem__(cls, item):",
4392
+ " return 'base'",
4393
+ "class Child(Base):",
4394
+ " def __class_getitem__(cls, item):",
4395
+ " return 'child'",
4396
+ "assrt.equal(Base[1], 'base')",
4397
+ "assrt.equal(Child[1], 'child')",
4398
+ ].join("\n"),
4399
+ },
4400
+
4401
+ {
4402
+ name: "class_getitem_classvar",
4403
+ description: "__class_getitem__ can access class variables via cls",
4404
+ src: [
4405
+ "# globals: assrt",
4406
+ "class Tagged:",
4407
+ " prefix = 'Tag'",
4408
+ " def __class_getitem__(cls, item):",
4409
+ " return cls.prefix + ':' + str(item)",
4410
+ "assrt.equal(Tagged['int'], 'Tag:int')",
4411
+ ].join("\n"),
4412
+ },
4413
+
4414
+ {
4415
+ name: "class_getitem_builtin_name",
4416
+ description: "Built-in types int/str/float/bool have .__name__ so they work as __class_getitem__ arguments",
4417
+ src: [
4418
+ "# globals: assrt",
4419
+ "class TypedList:",
4420
+ " prefix = 'TypedList'",
4421
+ " def __class_getitem__(cls, item):",
4422
+ " return cls.prefix + '[' + item.__name__ + ']'",
4423
+ "assrt.equal(TypedList[int], 'TypedList[int]')",
4424
+ "assrt.equal(TypedList[str], 'TypedList[str]')",
4425
+ "assrt.equal(TypedList[float], 'TypedList[float]')",
4426
+ "assrt.equal(TypedList[bool], 'TypedList[bool]')",
4427
+ ].join("\n"),
4428
+ },
4429
+
4430
+ // ── __init_subclass__ hook ────────────────────────────────────────────
4431
+
4432
+ {
4433
+ name: "init_subclass_basic",
4434
+ description: "__init_subclass__ is called when a subclass is created",
4435
+ src: [
4436
+ "# globals: assrt",
4437
+ "log = []",
4438
+ "class Base:",
4439
+ " def __init_subclass__(cls, **kwargs):",
4440
+ " log.append(cls.__name__)",
4441
+ "class Child(Base):",
4442
+ " pass",
4443
+ "class GrandChild(Child):",
4444
+ " pass",
4445
+ "assrt.deepEqual(log, ['Child', 'GrandChild'])",
4446
+ ].join("\n"),
4447
+ js_checks: ['.__init_subclass__.call(Child)', '.__init_subclass__.call(GrandChild)'],
4448
+ },
4449
+
4450
+ {
4451
+ name: "init_subclass_cls_is_subclass",
4452
+ description: "__init_subclass__ receives the subclass as cls",
4453
+ src: [
4454
+ "# globals: assrt",
4455
+ "received = []",
4456
+ "class Base:",
4457
+ " def __init_subclass__(cls, **kwargs):",
4458
+ " received.append(cls)",
4459
+ "class Child(Base):",
4460
+ " pass",
4461
+ "assrt.equal(received.length, 1)",
4462
+ "assrt.equal(received[0], Child)",
4463
+ ].join("\n"),
4464
+ },
4465
+
4466
+ {
4467
+ name: "init_subclass_kwargs",
4468
+ description: "keyword arguments from class header are passed to __init_subclass__",
4469
+ src: [
4470
+ "# globals: assrt",
4471
+ "log = []",
4472
+ "class Base:",
4473
+ " def __init_subclass__(cls, tag=None, **kwargs):",
4474
+ " log.append(tag)",
4475
+ "class Child(Base, tag='alpha'):",
4476
+ " pass",
4477
+ "class Other(Base, tag='beta'):",
4478
+ " pass",
4479
+ "assrt.deepEqual(log, ['alpha', 'beta'])",
4480
+ ].join("\n"),
4481
+ js_checks: ["ρσ_isc_kw"],
4482
+ },
4483
+
4484
+ {
4485
+ name: "init_subclass_super_chain",
4486
+ description: "super().__init_subclass__ propagates to grandparent",
4487
+ src: [
4488
+ "# globals: assrt",
4489
+ "calls = []",
4490
+ "class GrandParent:",
4491
+ " def __init_subclass__(cls, **kwargs):",
4492
+ " calls.append('GrandParent:' + cls.__name__)",
4493
+ "class Parent(GrandParent):",
4494
+ " def __init_subclass__(cls, **kwargs):",
4495
+ " super().__init_subclass__(**kwargs)",
4496
+ " calls.append('Parent:' + cls.__name__)",
4497
+ "class Child(Parent):",
4498
+ " pass",
4499
+ "assrt.deepEqual(calls, ['GrandParent:Parent', 'GrandParent:Child', 'Parent:Child'])",
4500
+ ].join("\n"),
4501
+ },
4502
+
4503
+ {
4504
+ name: "init_subclass_set_classvar",
4505
+ description: "__init_subclass__ can set class variables on the subclass",
4506
+ src: [
4507
+ "# globals: assrt",
4508
+ "class Registry:",
4509
+ " _registry = []",
4510
+ " def __init_subclass__(cls, **kwargs):",
4511
+ " cls._registered = True",
4512
+ " Registry._registry.append(cls.__name__)",
4513
+ "class A(Registry):",
4514
+ " pass",
4515
+ "class B(Registry):",
4516
+ " pass",
4517
+ "assrt.equal(A._registered, True)",
4518
+ "assrt.equal(B._registered, True)",
4519
+ "assrt.deepEqual(Registry._registry, ['A', 'B'])",
4520
+ ].join("\n"),
4521
+ },
4522
+
4523
+ {
4524
+ name: "init_subclass_no_hook_no_call",
4525
+ description: "no __init_subclass__ defined: class definition works normally",
4526
+ src: [
4527
+ "# globals: assrt",
4528
+ "class Base:",
4529
+ " pass",
4530
+ "class Child(Base):",
4531
+ " pass",
4532
+ "c = Child()",
4533
+ "assrt.ok(isinstance(c, Child))",
4534
+ ].join("\n"),
4535
+ },
4536
+
4537
+ // ── except* / ExceptionGroup ──────────────────────────────────────────
4538
+
4539
+ {
4540
+ name: "except_star_basic",
4541
+ description: "except* catches matching exceptions from an ExceptionGroup",
4542
+ src: [
4543
+ "# globals: assrt",
4544
+ 'eg = ExceptionGroup("errors", [ValueError("bad"), ValueError("again")])',
4545
+ "caught_ve = []",
4546
+ "try:",
4547
+ " raise eg",
4548
+ "except* ValueError as g:",
4549
+ " for e in g.exceptions:",
4550
+ " caught_ve.append(str(e))",
4551
+ "assrt.equal(len(caught_ve), 2)",
4552
+ 'assrt.ok(caught_ve[0].indexOf("bad") >= 0)',
4553
+ 'assrt.ok(caught_ve[1].indexOf("again") >= 0)',
4554
+ ].join("\n"),
4555
+ js_checks: ["ExceptionGroup", "ρσ_eg_exceptions"],
4556
+ },
4557
+
4558
+ {
4559
+ name: "except_star_multiple_handlers",
4560
+ description: "multiple except* clauses each receive their matching sub-group",
4561
+ src: [
4562
+ "# globals: assrt",
4563
+ 'eg = ExceptionGroup("mixed", [ValueError("v1"), TypeError("t1"), ValueError("v2")])',
4564
+ "val_count = 0",
4565
+ "type_count = 0",
4566
+ "try:",
4567
+ " raise eg",
4568
+ "except* ValueError as g:",
4569
+ " val_count = len(g.exceptions)",
4570
+ "except* TypeError as g:",
4571
+ " type_count = len(g.exceptions)",
4572
+ "assrt.equal(val_count, 2)",
4573
+ "assrt.equal(type_count, 1)",
4574
+ ].join("\n"),
4575
+ },
4576
+
4577
+ {
4578
+ name: "except_star_unmatched_reraise",
4579
+ description: "unmatched exceptions from an ExceptionGroup are re-raised",
4580
+ src: [
4581
+ "# globals: assrt",
4582
+ 'eg = ExceptionGroup("mixed", [ValueError("v"), KeyError("k")])',
4583
+ "caught = False",
4584
+ "reraised = False",
4585
+ "try:",
4586
+ " try:",
4587
+ " raise eg",
4588
+ " except* ValueError as g:",
4589
+ " caught = True",
4590
+ "except ExceptionGroup as outer:",
4591
+ " reraised = True",
4592
+ " assrt.equal(len(outer.exceptions), 1)",
4593
+ " assrt.ok(isinstance(outer.exceptions[0], KeyError))",
4594
+ "assrt.ok(caught)",
4595
+ "assrt.ok(reraised)",
4596
+ ].join("\n"),
4597
+ },
4598
+
4599
+ {
4600
+ name: "except_star_non_group",
4601
+ description: "except* also handles a plain (non-ExceptionGroup) exception",
4602
+ src: [
4603
+ "# globals: assrt",
4604
+ "caught = False",
4605
+ "try:",
4606
+ ' raise ValueError("plain")',
4607
+ "except* ValueError as g:",
4608
+ " caught = True",
4609
+ " assrt.ok(isinstance(g, ValueError))",
4610
+ "assrt.ok(caught)",
4611
+ ].join("\n"),
4612
+ },
4613
+
4614
+ {
4615
+ name: "except_star_bare",
4616
+ description: "bare except* catches all remaining exceptions",
4617
+ src: [
4618
+ "# globals: assrt",
4619
+ 'eg = ExceptionGroup("all", [ValueError("v"), TypeError("t")])',
4620
+ "total = 0",
4621
+ "try:",
4622
+ " raise eg",
4623
+ "except* as g:",
4624
+ " total = len(g.exceptions)",
4625
+ "assrt.equal(total, 2)",
4626
+ ].join("\n"),
4627
+ },
4628
+
4629
+ {
4630
+ name: "except_star_exception_group_class",
4631
+ description: "ExceptionGroup class has correct attributes and subgroup/split methods",
4632
+ src: [
4633
+ "# globals: assrt",
4634
+ 'eg = ExceptionGroup("demo", [ValueError("v"), TypeError("t"), ValueError("v2")])',
4635
+ "assrt.equal(eg.message, 'demo')",
4636
+ "assrt.equal(len(eg.exceptions), 3)",
4637
+ "sub = eg.subgroup(ValueError)",
4638
+ "assrt.equal(len(sub.exceptions), 2)",
4639
+ "parts = eg.split(ValueError)",
4640
+ "assrt.equal(len(parts[0].exceptions), 2)",
4641
+ "assrt.equal(len(parts[1].exceptions), 1)",
4642
+ ].join("\n"),
4643
+ },
4644
+
4645
+ // ── * and ** unpacking operators ──────────────────────────────────────────
4646
+
4647
+ {
4648
+ name: "list_spread_basic",
4649
+ description: "list spread: [*a, 1, 2] flattens iterable into a new list",
4650
+ src: [
4651
+ "# globals: assrt",
4652
+ "a = [1, 2, 3]",
4653
+ "b = [*a, 4, 5]",
4654
+ "assrt.deepEqual(b, [1, 2, 3, 4, 5])",
4655
+ ].join("\n"),
4656
+ js_checks: [/\.\.\./],
4657
+ },
4658
+
4659
+ {
4660
+ name: "list_spread_middle",
4661
+ description: "list spread: spread in the middle and at both ends",
4662
+ src: [
4663
+ "# globals: assrt",
4664
+ "a = [2, 3]",
4665
+ "b = [1, *a, 4]",
4666
+ "assrt.deepEqual(b, [1, 2, 3, 4])",
4667
+ "x = [10, 20]",
4668
+ "y = [30, 40]",
4669
+ "z = [*x, *y]",
4670
+ "assrt.deepEqual(z, [10, 20, 30, 40])",
4671
+ ].join("\n"),
4672
+ },
4673
+
4674
+ {
4675
+ name: "list_spread_string",
4676
+ description: "list spread: *string unpacks characters",
4677
+ src: [
4678
+ "# globals: assrt",
4679
+ "chars = [*'abc', 'd']",
4680
+ "assrt.deepEqual(chars, ['a', 'b', 'c', 'd'])",
4681
+ ].join("\n"),
4682
+ },
4683
+
4684
+ {
4685
+ name: "list_spread_first",
4686
+ description: "list spread: spread as the very first element",
4687
+ src: [
4688
+ "# globals: assrt",
4689
+ "a = [1, 2]",
4690
+ "b = [*a, 3]",
4691
+ "assrt.deepEqual(b, [1, 2, 3])",
4692
+ "c = [*a]",
4693
+ "assrt.deepEqual(c, [1, 2])",
4694
+ ].join("\n"),
4695
+ },
4696
+
4697
+ {
4698
+ name: "set_spread_basic",
4699
+ description: "set spread: {*a, 1} builds a set from iterable and literals",
4700
+ src: [
4701
+ "# globals: assrt",
4702
+ "a = [1, 2, 3]",
4703
+ "s = {*a, 4}",
4704
+ "assrt.ok(isinstance(s, set))",
4705
+ "assrt.equal(len(s), 4)",
4706
+ "assrt.ok(s.has(1))",
4707
+ "assrt.ok(s.has(4))",
4708
+ ].join("\n"),
4709
+ js_checks: ["ρσ_set(["],
4710
+ },
4711
+
4712
+ {
4713
+ name: "set_spread_multiple",
4714
+ description: "set spread: multiple spreads merge iterables into a set",
4715
+ src: [
4716
+ "# globals: assrt",
4717
+ "a = [1, 2]",
4718
+ "b = [3, 4]",
4719
+ "s = {*a, *b}",
4720
+ "assrt.equal(len(s), 4)",
4721
+ "assrt.ok(s.has(2))",
4722
+ "assrt.ok(s.has(3))",
4723
+ ].join("\n"),
4724
+ },
4725
+
4726
+ {
4727
+ name: "kwargs_spread_expr",
4728
+ description: "**expr in function call accepts arbitrary expression, not just symbol",
4729
+ src: [
4730
+ "# globals: assrt",
4731
+ "def f(a=0, b=0, c=0):",
4732
+ " return a + b + c",
4733
+ "opts = {'a': 1, 'b': 2, 'c': 3}",
4734
+ "assrt.equal(f(**opts), 6)",
4735
+ ].join("\n"),
4736
+ },
4737
+
4738
+ {
4739
+ name: "kwargs_spread_getattr",
4740
+ description: "**obj.attr in function call spreads attribute access result",
4741
+ src: [
4742
+ "# globals: assrt",
4743
+ "class Cfg:",
4744
+ " params = {'x': 10, 'y': 20}",
4745
+ "def add(x=0, y=0):",
4746
+ " return x + y",
4747
+ "assrt.equal(add(**Cfg.params), 30)",
4748
+ ].join("\n"),
4749
+ },
4750
+
4751
+ {
4752
+ name: "star_in_call_existing",
4753
+ description: "*args in function call: existing behaviour still works",
4754
+ src: [
4755
+ "# globals: assrt",
4756
+ "def f(a, b, c):",
4757
+ " return a + b + c",
4758
+ "args = [1, 2, 3]",
4759
+ "assrt.equal(f(*args), 6)",
4760
+ ].join("\n"),
4761
+ },
4762
+
4763
+ // ── tuple type ────────────────────────────────────────────────────────────
4764
+
4765
+ {
4766
+ name: "tuple_from_list",
4767
+ description: "tuple() converts a list to a plain array",
4768
+ src: [
4769
+ "# globals: assrt",
4770
+ "t = tuple([1, 2, 3])",
4771
+ "assrt.equal(t[0], 1)",
4772
+ "assrt.equal(t[1], 2)",
4773
+ "assrt.equal(t[2], 3)",
4774
+ "assrt.equal(len(t), 3)",
4775
+ ].join("\n"),
4776
+ },
4777
+
4778
+ {
4779
+ name: "tuple_from_string",
4780
+ description: "tuple() converts a string to an array of characters",
4781
+ src: [
4782
+ "# globals: assrt",
4783
+ "t = tuple('abc')",
4784
+ "assrt.equal(t[0], 'a')",
4785
+ "assrt.equal(t[1], 'b')",
4786
+ "assrt.equal(t[2], 'c')",
4787
+ "assrt.equal(len(t), 3)",
4788
+ ].join("\n"),
4789
+ },
4790
+
4791
+ {
4792
+ name: "tuple_empty",
4793
+ description: "tuple() with no args returns an empty array",
4794
+ src: [
4795
+ "# globals: assrt",
4796
+ "t = tuple()",
4797
+ "assrt.equal(len(t), 0)",
4798
+ ].join("\n"),
4799
+ },
4800
+
4801
+ {
4802
+ name: "tuple_annotation_variable",
4803
+ description: "tuple used as a variable type annotation with paren notation compiles and runs",
4804
+ src: [
4805
+ "# globals: assrt",
4806
+ "coords: tuple = (10, 20)",
4807
+ "assrt.equal(coords[0], 10)",
4808
+ "assrt.equal(coords[1], 20)",
4809
+ ].join("\n"),
4810
+ js_checks: ["coords = [10, 20]"],
4811
+ },
4812
+
4813
+ {
4814
+ name: "tuple_annotation_function_arg",
4815
+ description: "tuple used as a function argument type annotation works",
4816
+ src: [
4817
+ "# globals: assrt",
4818
+ "def first(t: tuple):",
4819
+ " return t[0]",
4820
+ "assrt.equal(first([7, 8, 9]), 7)",
4821
+ ].join("\n"),
4822
+ },
4823
+
4824
+ {
4825
+ name: "tuple_iterable",
4826
+ description: "tuple() result is iterable with for-in",
4827
+ src: [
4828
+ "# globals: assrt",
4829
+ "t = tuple([10, 20, 30])",
4830
+ "total = 0",
4831
+ "for v in t:",
4832
+ " total += v",
4833
+ "assrt.equal(total, 60)",
4834
+ ].join("\n"),
4835
+ },
4836
+
4837
+ {
4838
+ name: "list_spread_is_list",
4839
+ description: "result of [*a] is a proper Python list with list methods",
4840
+ src: [
4841
+ "# globals: assrt",
4842
+ "a = [1, 2]",
4843
+ "b = [*a, 3]",
4844
+ "assrt.ok(isinstance(b, list))",
4845
+ "b.append(4)",
4846
+ "assrt.equal(len(b), 4)",
4847
+ ].join("\n"),
4848
+ },
4849
+
4850
+ // ── copy ──────────────────────────────────────────────────────────────
4851
+
4852
+ {
4853
+ name: "copy_primitives",
4854
+ description: "copy.copy returns primitives unchanged",
4855
+ src: [
4856
+ "# globals: assrt",
4857
+ "from copy import copy",
4858
+ "assrt.equal(copy(42), 42)",
4859
+ "assrt.equal(copy('hello'), 'hello')",
4860
+ "assrt.equal(copy(True), True)",
4861
+ "assrt.equal(copy(None), None)",
4862
+ ].join("\n"),
4863
+ },
4864
+
4865
+ {
4866
+ name: "copy_list_shallow",
4867
+ description: "copy.copy of a list returns a shallow copy",
4868
+ src: [
4869
+ "# globals: assrt",
4870
+ "from copy import copy",
4871
+ "orig = [1, [2, 3], 4]",
4872
+ "c = copy(orig)",
4873
+ "assrt.equal(len(c), 3)",
4874
+ "assrt.equal(c[0], 1)",
4875
+ // shallow: inner list is the same object
4876
+ "assrt.ok(c[1] is orig[1])",
4877
+ // modifying the copy does not affect the original
4878
+ "c.append(5)",
4879
+ "assrt.equal(len(orig), 3)",
4880
+ "assrt.equal(len(c), 4)",
4881
+ ].join("\n"),
4882
+ },
4883
+
4884
+ {
4885
+ name: "copy_dict_shallow",
4886
+ description: "copy.copy of a dict returns a shallow copy",
4887
+ src: [
4888
+ "# globals: assrt",
4889
+ "from __python__ import dict_literals, overload_getitem",
4890
+ "from copy import copy",
4891
+ "inner = [99]",
4892
+ "orig = {'a': 1, 'b': inner}",
4893
+ "c = copy(orig)",
4894
+ "assrt.equal(c['a'], 1)",
4895
+ // shallow: inner list is the same object
4896
+ "assrt.ok(c['b'] is orig['b'])",
4897
+ "c['x'] = 100",
4898
+ "assrt.ok(not ('x' in orig))",
4899
+ ].join("\n"),
4900
+ },
4901
+
4902
+ {
4903
+ name: "copy_set_shallow",
4904
+ description: "copy.copy of a set returns an independent copy",
4905
+ src: [
4906
+ "# globals: assrt",
4907
+ "from copy import copy",
4908
+ "orig = {1, 2, 3}",
4909
+ "c = copy(orig)",
4910
+ "assrt.equal(len(c), 3)",
4911
+ "c.add(4)",
4912
+ "assrt.equal(len(orig), 3)",
4913
+ "assrt.equal(len(c), 4)",
4914
+ ].join("\n"),
4915
+ },
4916
+
4917
+ {
4918
+ name: "copy_class_instance_shallow",
4919
+ description: "copy.copy of a class instance is shallow",
4920
+ src: [
4921
+ "# globals: assrt",
4922
+ "from copy import copy",
4923
+ "class Point:",
4924
+ " def __init__(self, x, y):",
4925
+ " self.x = x",
4926
+ " self.y = y",
4927
+ "p = Point(1, 2)",
4928
+ "p.data = [10, 20]",
4929
+ "q = copy(p)",
4930
+ "assrt.equal(q.x, 1)",
4931
+ "assrt.equal(q.y, 2)",
4932
+ "assrt.ok(q is not p)",
4933
+ // shallow: mutable attribute is the same object
4934
+ "assrt.ok(q.data is p.data)",
4935
+ ].join("\n"),
4936
+ },
4937
+
4938
+ {
4939
+ name: "copy_custom_copy_hook",
4940
+ description: "__copy__ method is called by copy.copy",
4941
+ src: [
4942
+ "# globals: assrt",
4943
+ "from copy import copy",
4944
+ "class MyObj:",
4945
+ " def __init__(self, val):",
4946
+ " self.val = val",
4947
+ " self.copy_called = False",
4948
+ " def __copy__(self):",
4949
+ " result = MyObj(self.val * 2)",
4950
+ " return result",
4951
+ "obj = MyObj(5)",
4952
+ "c = copy(obj)",
4953
+ "assrt.equal(c.val, 10)",
4954
+ ].join("\n"),
4955
+ },
4956
+
4957
+ {
4958
+ name: "deepcopy_list_nested",
4959
+ description: "copy.deepcopy of a list with nested lists returns independent copies",
4960
+ src: [
4961
+ "# globals: assrt",
4962
+ "from copy import deepcopy",
4963
+ "orig = [1, [2, 3], [4, [5, 6]]]",
4964
+ "d = deepcopy(orig)",
4965
+ "assrt.equal(d[0], 1)",
4966
+ "assrt.equal(d[1][0], 2)",
4967
+ "assrt.equal(d[2][1][0], 5)",
4968
+ // deep: inner lists are different objects
4969
+ "assrt.ok(d[1] is not orig[1])",
4970
+ "assrt.ok(d[2][1] is not orig[2][1])",
4971
+ // mutating copy does not affect original
4972
+ "d[1].append(99)",
4973
+ "assrt.equal(len(orig[1]), 2)",
4974
+ ].join("\n"),
4975
+ },
4976
+
4977
+ {
4978
+ name: "deepcopy_dict_nested",
4979
+ description: "copy.deepcopy of a dict with nested dicts returns independent copies",
4980
+ src: [
4981
+ "# globals: assrt",
4982
+ "from __python__ import dict_literals, overload_getitem",
4983
+ "from copy import deepcopy",
4984
+ "orig = {'a': {'x': 1}, 'b': [2, 3]}",
4985
+ "d = deepcopy(orig)",
4986
+ "assrt.equal(d['a']['x'], 1)",
4987
+ "assrt.ok(d['a'] is not orig['a'])",
4988
+ "assrt.ok(d['b'] is not orig['b'])",
4989
+ "d['a']['x'] = 99",
4990
+ "assrt.equal(orig['a']['x'], 1)",
4991
+ ].join("\n"),
4992
+ },
4993
+
4994
+ {
4995
+ name: "deepcopy_circular",
4996
+ description: "copy.deepcopy handles circular references without infinite recursion",
4997
+ src: [
4998
+ "# globals: assrt",
4999
+ "from copy import deepcopy",
5000
+ "a = [1, 2]",
5001
+ "a.push(a) # circular reference",
5002
+ "b = deepcopy(a)",
5003
+ "assrt.equal(b[0], 1)",
5004
+ "assrt.equal(b[1], 2)",
5005
+ "assrt.ok(b[2] is b)", // circularity preserved in the copy
5006
+ "assrt.ok(b is not a)",
5007
+ ].join("\n"),
5008
+ },
5009
+
5010
+ {
5011
+ name: "deepcopy_custom_hook",
5012
+ description: "__deepcopy__(memo) method is called by copy.deepcopy",
5013
+ src: [
5014
+ "# globals: assrt",
5015
+ "from copy import deepcopy",
5016
+ "class Node:",
5017
+ " def __init__(self, val):",
5018
+ " self.val = val",
5019
+ " self.children = []",
5020
+ " def __deepcopy__(self, memo):",
5021
+ " result = Node(self.val * 10)",
5022
+ " return result",
5023
+ "n = Node(7)",
5024
+ "m = deepcopy(n)",
5025
+ "assrt.equal(m.val, 70)",
5026
+ "assrt.ok(m is not n)",
5027
+ ].join("\n"),
5028
+ },
5029
+
5030
+ {
5031
+ name: "deepcopy_class_instance",
5032
+ description: "copy.deepcopy of a class instance deeply copies instance attributes",
5033
+ src: [
5034
+ "# globals: assrt",
5035
+ "from copy import deepcopy",
5036
+ "class Box:",
5037
+ " def __init__(self, items):",
5038
+ " self.items = items",
5039
+ "b = Box([1, 2, 3])",
5040
+ "c = deepcopy(b)",
5041
+ "assrt.ok(c is not b)",
5042
+ "assrt.ok(c.items is not b.items)",
5043
+ "c.items.append(4)",
5044
+ "assrt.equal(len(b.items), 3)",
5045
+ ].join("\n"),
5046
+ },
5047
+
5048
+ // ── str.expandtabs ────────────────────────────────────────────────────
5049
+
5050
+ {
5051
+ name: "expandtabs_default",
5052
+ description: "str.expandtabs() with default tabsize=8 replaces tabs with spaces",
5053
+ src: [
5054
+ "# globals: assrt",
5055
+ 'assrt.equal(str.expandtabs("\\t"), " ")',
5056
+ 'assrt.equal(str.expandtabs("a\\tb"), "a b")',
5057
+ 'assrt.equal(str.expandtabs("ab\\tc"), "ab c")',
5058
+ ].join("\n"),
5059
+ js_checks: ["expandtabs"],
5060
+ },
5061
+
5062
+ {
5063
+ name: "expandtabs_custom_tabsize",
5064
+ description: "str.expandtabs(tabsize) respects a custom tabsize",
5065
+ src: [
5066
+ "# globals: assrt",
5067
+ 'assrt.equal(str.expandtabs("\\t", 4), " ")',
5068
+ 'assrt.equal(str.expandtabs("a\\tb", 4), "a b")',
5069
+ 'assrt.equal(str.expandtabs("abc\\td", 4), "abc d")',
5070
+ 'assrt.equal(str.expandtabs("ab\\tcd", 4), "ab cd")',
5071
+ ].join("\n"),
5072
+ js_checks: [],
5073
+ },
5074
+
5075
+ {
5076
+ name: "expandtabs_tabsize_zero",
5077
+ description: "str.expandtabs(0) removes all tab characters",
5078
+ src: [
5079
+ "# globals: assrt",
5080
+ 'assrt.equal(str.expandtabs("a\\tb\\tc", 0), "abc")',
5081
+ 'assrt.equal(str.expandtabs("\\t\\t", 0), "")',
5082
+ ].join("\n"),
5083
+ js_checks: [],
5084
+ },
5085
+
5086
+ {
5087
+ name: "expandtabs_newline_resets_column",
5088
+ description: "str.expandtabs() resets column counter at newlines",
5089
+ src: [
5090
+ "# globals: assrt",
5091
+ 'assrt.equal(str.expandtabs("a\\n\\tb", 4), "a\\n b")',
5092
+ 'assrt.equal(str.expandtabs("abc\\n\\td", 4), "abc\\n d")',
5093
+ ].join("\n"),
5094
+ js_checks: [],
5095
+ },
5096
+
5097
+ {
5098
+ name: "expandtabs_instance_method",
5099
+ description: "expandtabs works as an instance method via str.prototype",
5100
+ src: [
5101
+ "# globals: assrt",
5102
+ "from pythonize import strings",
5103
+ "strings()",
5104
+ 'assrt.equal("\\t".expandtabs(), " ")',
5105
+ 'assrt.equal("a\\tb".expandtabs(4), "a b")',
5106
+ ].join("\n"),
5107
+ js_checks: [],
5108
+ },
5109
+
5110
+ {
5111
+ name: "expandtabs_no_tabs",
5112
+ description: "str.expandtabs() returns string unchanged when no tabs present",
5113
+ src: [
5114
+ "# globals: assrt",
5115
+ 'assrt.equal(str.expandtabs("hello world"), "hello world")',
5116
+ 'assrt.equal(str.expandtabs(""), "")',
5117
+ ].join("\n"),
5118
+ js_checks: [],
5119
+ },
5120
+
5121
+ ];
5122
+
5123
+ // ── Runner ───────────────────────────────────────────────────────────────────
5124
+
5125
+ function run_tests(filter) {
5126
+ var tests = filter
5127
+ ? TESTS.filter(function (t) { return t.name === filter; })
5128
+ : TESTS;
5129
+
5130
+ if (tests.length === 0) {
5131
+ console.error(colored("No test found: " + filter, "red"));
5132
+ process.exit(1);
5133
+ }
5134
+
5135
+ var failures = [];
5136
+
5137
+ tests.forEach(function (test) {
5138
+
5139
+ // Custom run function (for tests that need direct JS-level control)
5140
+ if (typeof test.run === "function") {
5141
+ try {
5142
+ test.run();
5143
+ } catch (e) {
5144
+ failures.push(test.name);
5145
+ var msg = e.stack || String(e);
5146
+ console.log(colored("FAIL " + test.name, "red") +
5147
+ " [run]\n " + msg + "\n");
5148
+ return;
5149
+ }
5150
+ console.log(colored("PASS " + test.name, "green") +
5151
+ " – " + test.description);
5152
+ return;
5153
+ }
5154
+
5155
+ var js;
5156
+
5157
+ // 1 – compile RapydScript → JS
5158
+ try {
5159
+ js = test.virtual_files ? compile_virtual(test.src, test.virtual_files) : compile(test.src);
5160
+ } catch (e) {
5161
+ failures.push(test.name);
5162
+ console.log(colored("FAIL " + test.name, "red") +
5163
+ " [compile error]\n " + e + "\n");
5164
+ return;
5165
+ }
5166
+
5167
+ // 2 – verify expected patterns appear in the JS output
5168
+ try {
5169
+ check_js_patterns(test.name, js, test.js_checks);
5170
+ // also check patterns that must NOT appear
5171
+ (test.js_not_checks || []).forEach(function (pat) {
5172
+ var found = (pat instanceof RegExp) ? pat.test(js) : js.indexOf(pat) !== -1;
5173
+ if (found) {
5174
+ var desc = (pat instanceof RegExp) ? String(pat) : JSON.stringify(pat);
5175
+ throw new Error("compiled JS unexpectedly contains " + desc + "\n in test: " + test.name);
5176
+ }
5177
+ });
5178
+ } catch (e) {
5179
+ failures.push(test.name);
5180
+ console.debug("Emitted JS:\n" + js + "\n");
5181
+ console.log(colored("FAIL " + test.name, "red") +
5182
+ " [JS pattern mismatch]\n " + e.message + "\n");
5183
+ return;
5184
+ }
5185
+
5186
+ // 3 – run the JS; assertions embedded in src catch wrong values
5187
+ // (skipped for tests that produce JSX or other non-executable output)
5188
+ if (!test.skip_run) {
5189
+ try {
5190
+ run_js(js);
5191
+ } catch (e) {
5192
+ failures.push(test.name);
5193
+ var msg = e.stack || String(e);
5194
+ console.log(colored("FAIL " + test.name, "red") +
5195
+ " [runtime]\n " + msg + "\n");
5196
+ return;
5197
+ }
2973
5198
  }
2974
5199
 
2975
5200
  console.log(colored("PASS " + test.name, "green") +