rapydscript-ns 0.8.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +1351 -141
  3. package/TODO.md +12 -6
  4. package/language-service/index.js +184 -26
  5. package/package.json +1 -1
  6. package/release/baselib-plain-pretty.js +5895 -1928
  7. package/release/baselib-plain-ugly.js +140 -3
  8. package/release/compiler.js +16282 -5408
  9. package/release/signatures.json +25 -22
  10. package/src/ast.pyj +94 -1
  11. package/src/baselib-builtins.pyj +362 -3
  12. package/src/baselib-bytes.pyj +664 -0
  13. package/src/baselib-containers.pyj +99 -0
  14. package/src/baselib-errors.pyj +45 -1
  15. package/src/baselib-internal.pyj +346 -49
  16. package/src/baselib-itertools.pyj +17 -4
  17. package/src/baselib-str.pyj +46 -4
  18. package/src/lib/abc.pyj +317 -0
  19. package/src/lib/copy.pyj +120 -0
  20. package/src/lib/dataclasses.pyj +532 -0
  21. package/src/lib/enum.pyj +125 -0
  22. package/src/lib/pythonize.pyj +1 -1
  23. package/src/lib/re.pyj +35 -1
  24. package/src/lib/react.pyj +74 -0
  25. package/src/lib/typing.pyj +577 -0
  26. package/src/monaco-language-service/builtins.js +19 -4
  27. package/src/monaco-language-service/diagnostics.js +40 -19
  28. package/src/output/classes.pyj +161 -25
  29. package/src/output/codegen.pyj +16 -2
  30. package/src/output/exceptions.pyj +97 -1
  31. package/src/output/functions.pyj +87 -5
  32. package/src/output/jsx.pyj +164 -0
  33. package/src/output/literals.pyj +28 -2
  34. package/src/output/loops.pyj +5 -2
  35. package/src/output/modules.pyj +1 -1
  36. package/src/output/operators.pyj +108 -36
  37. package/src/output/statements.pyj +2 -2
  38. package/src/output/stream.pyj +1 -0
  39. package/src/parse.pyj +496 -128
  40. package/src/tokenizer.pyj +38 -4
  41. package/test/abc.pyj +291 -0
  42. package/test/arithmetic_nostrict.pyj +88 -0
  43. package/test/arithmetic_types.pyj +169 -0
  44. package/test/baselib.pyj +91 -0
  45. package/test/bytes.pyj +467 -0
  46. package/test/classes.pyj +1 -0
  47. package/test/comparison_ops.pyj +173 -0
  48. package/test/dataclasses.pyj +253 -0
  49. package/test/enum.pyj +134 -0
  50. package/test/eval_exec.pyj +56 -0
  51. package/test/format.pyj +148 -0
  52. package/test/object.pyj +64 -0
  53. package/test/python_compat.pyj +17 -15
  54. package/test/python_features.pyj +89 -21
  55. package/test/regexp.pyj +29 -1
  56. package/test/tuples.pyj +96 -0
  57. package/test/typing.pyj +469 -0
  58. package/test/unit/index.js +2292 -70
  59. package/test/unit/language-service.js +674 -4
  60. package/test/unit/web-repl.js +1106 -0
  61. package/test/vars_locals_globals.pyj +94 -0
  62. package/tools/cli.js +11 -0
  63. package/tools/compile.js +5 -0
  64. package/tools/embedded_compiler.js +15 -4
  65. package/tools/lint.js +16 -19
  66. package/tools/repl.js +1 -1
  67. package/web-repl/env.js +122 -0
  68. package/web-repl/main.js +1 -3
  69. package/web-repl/rapydscript.js +125 -3
  70. package/PYTHON_DIFFERENCES_REPORT.md +0 -291
  71. package/PYTHON_FEATURE_COVERAGE.md +0 -200
  72. package/hack_demo.pyj +0 -112
package/README.md CHANGED
@@ -6,11 +6,14 @@ RapydScript
6
6
  [![Current Release](https://img.shields.io/npm/v/rapydscript-ns)](https://www.npmjs.com/package/rapydscript-ns)
7
7
  [![Known Vulnerabilities](https://snyk.io/test/github/ficocelliguy/rapydscript-ns/badge.svg)](https://snyk.io/test/github/ficocelliguy/rapydscript-ns)
8
8
 
9
- This is a fork of the original RapydScript that adds many new (not always
10
- backwards compatible) features. For more on the forking, [see the bottom of this file](#reasons-for-the-fork)
11
-
9
+ RapydScript is a pre-compiler for Javascript that uses syntax [identical](#python-feature-coverage) to modern Python. It transpiles
10
+ to native JS (with source maps) that reads like your Python code, but runs natively in the browser or node.
12
11
  [Try RapydScript-ns live via an in-browser REPL!](https://ficocelliguy.github.io/rapydscript-ns/)
13
12
 
13
+ This is a [fork of the original RapydScript](#reasons-for-the-fork) that adds many new features. The most notable
14
+ change is that all the Python features that are optional in RapydScript are now enabled by default.
15
+
16
+
14
17
  <!-- START doctoc generated TOC please keep comment here to allow auto update -->
15
18
  <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
16
19
  **Contents**
@@ -41,6 +44,7 @@ backwards compatible) features. For more on the forking, [see the bottom of this
41
44
  - [Extended Subscript Syntax](#extended-subscript-syntax)
42
45
  - [Variable Type Annotations](#variable-type-annotations)
43
46
  - [Regular Expressions](#regular-expressions)
47
+ - [JSX Support](#jsx-support)
44
48
  - [Creating DOM trees easily](#creating-dom-trees-easily)
45
49
  - [Classes](#classes)
46
50
  - [External Classes](#external-classes)
@@ -70,6 +74,7 @@ backwards compatible) features. For more on the forking, [see the bottom of this
70
74
  - [Virtual modules](#virtual-modules)
71
75
  - [Source maps](#source-maps)
72
76
  - [Running the tests](#running-the-tests)
77
+ - [Python Feature Coverage](#python-feature-coverage)
73
78
  - [Reasons for the fork](#reasons-for-the-fork)
74
79
 
75
80
  <!-- END doctoc generated TOC please keep comment here to allow auto update -->
@@ -82,20 +87,17 @@ RapydScript (pronounced 'RapidScript') is a pre-compiler for JavaScript,
82
87
  similar to CoffeeScript, but with cleaner, more readable syntax. The syntax is
83
88
  almost identical to Python, but RapydScript has a focus on performance and
84
89
  interoperability with external JavaScript libraries. This means that the
85
- JavaScript that RapydScript generates is performant and quite close to hand
90
+ JavaScript that RapydScript generates is performant and quite close to hand-
86
91
  written JavaScript.
87
92
 
88
93
  RapydScript allows to write your front-end in Python without the overhead that
89
- other similar frameworks introduce (the performance is the same as with pure
90
- JavaScript). To those familiar with CoffeeScript, RapydScript is like
91
- CoffeeScript, but inspired by Python's readability rather than Ruby's
92
- cleverness. To those familiar with Pyjamas, RapydScript brings many of the same
93
- features and support for Python syntax without the same overhead. Don't worry
94
- if you've never used either of the above-mentioned compilers, if you've ever
95
- had to write your code in pure JavaScript you'll appreciate RapydScript.
96
- RapydScript combines the best features of Python as well as JavaScript,
97
- bringing you features most other Pythonic JavaScript replacements overlook.
98
- Here are a few features of RapydScript:
94
+ other similar frameworks introduce - the performance is the same as with pure
95
+ JavaScript. To those familiar with CoffeeScript, RapydScript is similar, but
96
+ inspired by Python's readability rather than Ruby's cleverness. To those familiar
97
+ with Pyjamas, RapydScript brings many of the same features and support for Python
98
+ syntax without the same overhead. RapydScript combines the best features of Python
99
+ as well as JavaScript, bringing you features most other Pythonic JavaScript
100
+ replacements overlook. Here are a few features of RapydScript:
99
101
 
100
102
  - classes that work and feel similar to Python
101
103
  - an import system for modules/packages that works just like Python's
@@ -445,7 +447,6 @@ Class decorators are also supported with the caveat that the class properties
445
447
  must be accessed via the prototype property. For example:
446
448
 
447
449
  ```py
448
-
449
450
  def add_x(cls):
450
451
  cls.prototype.x = 1
451
452
 
@@ -698,6 +699,29 @@ head, *mid, tail = [1, 2, 3, 4, 5] # head=1, mid=[2, 3, 4], tail=5
698
699
 
699
700
  Starred assignment works with any iterable, including generators and strings (which are unpacked character by character). The starred variable always receives a list, even if it captures zero elements.
700
701
 
702
+ **Explicit tuple literals** using parentheses work the same as in Python and compile to JavaScript arrays:
703
+
704
+ ```py
705
+ empty = () # []
706
+ single = (42,) # [42] — trailing comma required for single-element tuple
707
+ pair = (1, 2) # [1, 2]
708
+ triple = ('a', 'b', 'c') # ['a', 'b', 'c']
709
+ nested = ((1, 2), (3, 4)) # [[1, 2], [3, 4]]
710
+ ```
711
+
712
+ A parenthesised expression without a trailing comma is **not** a tuple — `(x)` is just `x`. Add a comma to make it one: `(x,)`.
713
+
714
+ Tuple literals work naturally everywhere arrays do: as return values, function arguments, in `isinstance` checks, and in destructuring assignments:
715
+
716
+ ```py
717
+ def bounding_box(points):
718
+ return (min(p[0] for p in points), max(p[0] for p in points))
719
+
720
+ ok = isinstance(value, (int, str)) # tuple of types
721
+
722
+ (a, b), c = (1, 2), 3
723
+ ```
724
+
701
725
  Operators and keywords
702
726
  ------------------------
703
727
 
@@ -805,9 +829,9 @@ No special flag is required. The `+` operator compiles to a lightweight helper
805
829
 
806
830
  Sets in RapydScript are identical to those in python. You can create them using
807
831
  set literals or comprehensions and all set operations are supported. You can
808
- store any object in a set, the only caveat is that RapydScript does not support
809
- the ``__hash__()`` method, so if you store an arbitrary object as opposed to a
810
- primitive type, object equality will be via the ``is`` operator.
832
+ store any object in a set. For primitive types (strings, numbers) the value is
833
+ used for equality; for class instances, object identity (``is``) is used by
834
+ default unless the class defines a ``__hash__`` method.
811
835
 
812
836
  Note that sets are not a subclass of the ES 6 JavaScript Set object, however,
813
837
  they do use this object as a backend, when available. You can create a set from
@@ -835,8 +859,8 @@ a in s # True
835
859
  [1, 2] in s # False
836
860
  ```
837
861
 
838
- This is because, as noted above, object equality is via the ```is```
839
- operator, not hashes.
862
+ This is because list identity (not value) determines set membership for mutable
863
+ objects. Define ``__hash__`` on your own classes to control set/dict membership.
840
864
 
841
865
  ### Dicts
842
866
 
@@ -855,11 +879,11 @@ differences between RapydScript dicts and Python dicts.
855
879
  RapydScript dict objects in ```for..in``` loops.
856
880
 
857
881
  Fortunately, there is a builtin ```dict``` type that behaves just like Python's
858
- ```dict``` with all the same methods. The only caveat is that you have to add
859
- a special line to your RapydScript code to use these dicts, as shown below:
882
+ ```dict``` with all the same methods. The ``dict_literals`` and
883
+ ``overload_getitem`` flags are **on by default**, so dict literals and the
884
+ ``[]`` operator already behave like Python:
860
885
 
861
886
  ```py
862
- from __python__ import dict_literals, overload_getitem
863
887
  a = {1:1, 2:2}
864
888
  a[1] # == 1
865
889
  a[3] = 3
@@ -867,17 +891,10 @@ list(a.keys()) == [1, 2, 3]
867
891
  a['3'] # raises a KeyError as this is a proper python dict, not a JavaScript object
868
892
  ```
869
893
 
870
- The special line, called a *scoped flag* tells the compiler that from
871
- that point on, you want it to treat dict literals and the getitem operator `[]`
872
- as they are treated in python, not JavaScript.
873
-
874
- The scoped flags are local to each scope, that means that if you use it in a
875
- module, it will only affect code in that module, it you use it in a function,
876
- it will only affect code in that function. In fact, you can even use it to
877
- surround a few lines of code, like this:
894
+ These are *scoped flags* local to the scope where they appear. You can
895
+ disable them for a region of code using the ``no_`` prefix:
878
896
 
879
897
  ```py
880
- from __python__ import dict_literals, overload_getitem
881
898
  a = {1:1, 2:2}
882
899
  isinstance(a, dict) == True
883
900
  from __python__ import no_dict_literals, no_overload_getitem
@@ -885,6 +902,63 @@ a = {1:1, 2:2}
885
902
  isinstance(a, dict) == False # a is a normal JavaScript object
886
903
  ```
887
904
 
905
+ ### List spread literals
906
+
907
+ RapydScript supports Python's `*expr` spread syntax inside list literals.
908
+ One or more `*expr` items can appear anywhere, interleaved with ordinary
909
+ elements:
910
+
911
+ ```py
912
+ a = [1, 2, 3]
913
+ b = [4, 5]
914
+
915
+ # Spread at the end
916
+ result = [0, *a] # [0, 1, 2, 3]
917
+
918
+ # Spread in the middle
919
+ result = [0, *a, *b, 6] # [0, 1, 2, 3, 4, 5, 6]
920
+
921
+ # Copy a list
922
+ copy = [*a] # [1, 2, 3]
923
+
924
+ # Unpack a string
925
+ chars = [*'hello'] # ['h', 'e', 'l', 'l', 'o']
926
+ ```
927
+
928
+ Spread works on any iterable (lists, strings, generators, `range()`).
929
+ The result is always a new Python list. Translates to JavaScript's
930
+ `[...expr]` spread syntax.
931
+
932
+ ### Set spread literals
933
+
934
+ The same `*expr` syntax works inside set literals `{...}`:
935
+
936
+ ```py
937
+ a = [1, 2, 3]
938
+ b = [3, 4, 5]
939
+
940
+ s = {*a, *b} # set([1, 2, 3, 4, 5]) — duplicates removed
941
+ s2 = {*a, 10} # set([1, 2, 3, 10])
942
+ ```
943
+
944
+ Translates to `ρσ_set([...a, ...b])`.
945
+
946
+ ### `**expr` in function calls
947
+
948
+ `**expr` in a function call now accepts any expression, not just a plain
949
+ variable name:
950
+
951
+ ```py
952
+ def f(x=0, y=0):
953
+ return x + y
954
+
955
+ opts = {'x': 10, 'y': 20}
956
+ f(**opts) # 30 (variable — always worked)
957
+ f(**{'x': 1, 'y': 2}) # 3 (dict literal)
958
+ f(**cfg.defaults) # uses attribute access result
959
+ f(**get_opts()) # uses function call result
960
+ ```
961
+
888
962
  ### Dict merge literals
889
963
 
890
964
  RapydScript supports Python's `{**d1, **d2}` dict merge (spread) syntax.
@@ -904,11 +978,10 @@ result = {**defaults, 'weight': 5}
904
978
  # result == {'color': 'blue', 'size': 10, 'weight': 5}
905
979
  ```
906
980
 
907
- This works for both plain JavaScript-object dicts (the default) and Python
908
- `dict` objects (enabled via `from __python__ import dict_literals`):
981
+ This works for both plain JavaScript-object dicts and Python `dict` objects
982
+ (``dict_literals`` is on by default):
909
983
 
910
984
  ```py
911
- from __python__ import dict_literals, overload_getitem
912
985
  pd1 = {'a': 1}
913
986
  pd2 = {'b': 2}
914
987
  merged = {**pd1, **pd2} # isinstance(merged, dict) == True
@@ -919,12 +992,10 @@ and `dict.update()` for Python dicts.
919
992
 
920
993
  ### Dict merge operator `|` and `|=` (Python 3.9+)
921
994
 
922
- When `from __python__ import overload_operators, dict_literals` is active,
923
- Python dicts support the `|` (merge) and `|=` (update in-place) operators:
995
+ Python dicts support the `|` (merge) and `|=` (update in-place) operators
996
+ (requires ``overload_operators`` and ``dict_literals``, both on by default):
924
997
 
925
998
  ```py
926
- from __python__ import overload_operators, dict_literals
927
-
928
999
  d1 = {'x': 1, 'y': 2}
929
1000
  d2 = {'y': 99, 'z': 3}
930
1001
 
@@ -939,17 +1010,15 @@ d1 |= d2 # d1 is now {'x': 1, 'y': 99, 'z': 3}
939
1010
  `d1 |= d2` merges `d2` into `d1` and returns `d1`.
940
1011
 
941
1012
  Without `overload_operators` the `|` symbol is bitwise OR — use
942
- `{**d1, **d2}` spread syntax as a flag-free alternative.
1013
+ `{**d1, **d2}` spread syntax as an alternative if the flag is disabled.
943
1014
 
944
1015
 
945
1016
  ### Arithmetic operator overloading
946
1017
 
947
1018
  RapydScript supports Python-style arithmetic operator overloading via the
948
- ``overload_operators`` scoped flag:
1019
+ ``overload_operators`` flag, which is **on by default**:
949
1020
 
950
1021
  ```py
951
- from __python__ import overload_operators
952
-
953
1022
  class Vector:
954
1023
  def __init__(self, x, y):
955
1024
  self.x = x
@@ -990,31 +1059,52 @@ The supported dunder methods are:
990
1059
  Augmented assignment (``+=``, ``-=``, etc.) first tries the in-place method
991
1060
  (``__iadd__``, ``__isub__``, …) and then falls back to the binary method.
992
1061
 
993
- If neither operand defines the relevant dunder method the operation falls back
994
- to the native JavaScript operator, so plain numbers, strings, and booleans
995
- continue to work as expected with no performance penalty when no dunder method
996
- is defined.
1062
+ If neither operand defines the relevant dunder method, the operation enforces
1063
+ Python-style type compatibility before falling back to the native JavaScript
1064
+ operator:
1065
+
1066
+ - ``number`` ± ``number`` → allowed (including ``bool``, which is treated as an integer subclass)
1067
+ - ``str`` + ``str`` → allowed
1068
+ - ``str`` * ``int`` and ``list`` * ``int`` → allowed (and also with ``bool`` in place of ``int``)
1069
+ - Anything else raises ``TypeError`` with a Python-style message, e.g.:
1070
+
1071
+ ```py
1072
+ 1 + 'x' # TypeError: unsupported operand type(s) for +: 'int' and 'str'
1073
+ 'a' - 1 # TypeError: unsupported operand type(s) for -: 'str' and 'int'
1074
+ [1] + 'b' # TypeError: unsupported operand type(s) for +: 'list' and 'str'
1075
+ ```
1076
+
1077
+ This type-checking is controlled by the ``strict_arithmetic`` flag, which is
1078
+ **on by default** when ``overload_operators`` is active. To revert to
1079
+ JavaScript's silent coercion behaviour (e.g. ``1 + 'x'`` → ``'1x'``) without
1080
+ disabling dunder dispatch, use:
1081
+
1082
+ ```py
1083
+ from __python__ import no_strict_arithmetic
1084
+ ```
1085
+
1086
+ When ``overload_operators`` is
1087
+ disabled (``from __python__ import no_overload_operators``) the operators
1088
+ compile directly to JavaScript and no type checking is performed.
997
1089
 
998
1090
  When `overload_operators` is active, string and list repetition with `*` works just like Python:
999
1091
 
1000
1092
  ```py
1001
- from __python__ import overload_operators
1002
1093
  'ha' * 3 # 'hahaha'
1003
1094
  3 * 'ha' # 'hahaha'
1004
1095
  [0] * 4 # [0, 0, 0, 0]
1005
1096
  [1, 2] * 2 # [1, 2, 1, 2]
1006
1097
  ```
1007
1098
 
1008
- Because the dispatch adds one or two property lookups per operation, the flag
1009
- is **opt-in** rather than always-on. Enable it only in the files or scopes
1010
- where you need it.
1099
+ Because the dispatch adds one or two property lookups per operation, you can
1100
+ disable it in scopes where it is not needed with
1101
+ ``from __python__ import no_overload_operators``.
1011
1102
 
1012
1103
  The ``collections.Counter`` class defines ``__add__``, ``__sub__``, ``__or__``,
1013
1104
  and ``__and__``. With ``overload_operators`` you can use the natural operator
1014
1105
  syntax:
1015
1106
 
1016
1107
  ```py
1017
- from __python__ import overload_getitem, overload_operators
1018
1108
  from collections import Counter
1019
1109
 
1020
1110
  c1 = Counter('aab')
@@ -1032,9 +1122,35 @@ RapydScript dicts (but not arbitrary javascript objects). You can also define
1032
1122
  the ``__eq__(self, other)`` method in your classes to have these operators work
1033
1123
  for your own types.
1034
1124
 
1035
- RapydScript does not overload the ordering operators ```(>, <, >=,
1036
- <=)``` as doing so would be a big performance impact (function calls in
1037
- JavaScript are very slow). So using them on containers is useless.
1125
+ The ordering operators ``<``, ``>``, ``<=``, ``>=`` dispatch to Python-style
1126
+ dunder methods and compare lists lexicographically just like Python:
1127
+
1128
+ ```py
1129
+ from __python__ import overload_operators # on by default
1130
+
1131
+ # List comparison — lexicographic order
1132
+ assert [1, 2] < [1, 3] # True (first differing element: 2 < 3)
1133
+ assert [1, 2] < [1, 2, 0] # True (prefix is smaller)
1134
+ assert [2] > [1, 99] # True (first element dominates)
1135
+
1136
+ # Works with custom __lt__ / __gt__ / __le__ / __ge__ on objects
1137
+ class Version:
1138
+ def __init__(self, major, minor):
1139
+ self.major = major
1140
+ self.minor = minor
1141
+ def __lt__(self, other):
1142
+ return (self.major, self.minor) < (other.major, other.minor)
1143
+
1144
+ v1 = Version(1, 5)
1145
+ v2 = Version(2, 0)
1146
+ assert v1 < v2 # dispatches to __lt__
1147
+
1148
+ # Incompatible types raise TypeError, just like Python
1149
+ try:
1150
+ result = [1] < 42
1151
+ except TypeError as e:
1152
+ print(e) # '<' not supported between instances of 'list' and 'int'
1153
+ ```
1038
1154
 
1039
1155
  Chained comparisons work just like Python — each middle operand is evaluated only once:
1040
1156
 
@@ -1042,18 +1158,13 @@ Chained comparisons work just like Python — each middle operand is evaluated o
1042
1158
  # All of these work correctly, including mixed-direction chains
1043
1159
  assert 1 < 2 < 3 # True
1044
1160
  assert 1 < 2 > 0 # True (1<2 AND 2>0)
1045
- assert 1 < 2 > 3 == False # 1<2 AND 2>3 = True AND False = False
1161
+ assert [1] < [2] < [3] # True (lexicographic chain)
1046
1162
  ```
1047
1163
 
1048
1164
  ### Python Truthiness and `__bool__`
1049
1165
 
1050
- By default RapydScript uses JavaScript truthiness, where empty arrays `[]` and
1051
- empty objects `{}` are **truthy**. Activate full Python truthiness semantics
1052
- with:
1053
-
1054
- ```py
1055
- from __python__ import truthiness
1056
- ```
1166
+ RapydScript uses Python truthiness semantics by default (``truthiness`` is
1167
+ **on by default**):
1057
1168
 
1058
1169
  When this flag is active:
1059
1170
 
@@ -1064,8 +1175,6 @@ When this flag is active:
1064
1175
  - **All condition positions** (`if`, `while`, `assert`, `not`, ternary) use Python semantics.
1065
1176
 
1066
1177
  ```py
1067
- from __python__ import truthiness
1068
-
1069
1178
  class Empty:
1070
1179
  def __bool__(self): return False
1071
1180
 
@@ -1079,16 +1188,14 @@ z = [1] and 'ok' # z == 'ok'
1079
1188
 
1080
1189
  The flag is **scoped** — it applies until the end of the enclosing
1081
1190
  function or class body. Use `from __python__ import no_truthiness` to
1082
- disable it in a sub-scope.
1191
+ disable it in a sub-scope where JavaScript truthiness is needed.
1083
1192
 
1084
1193
  ### Callable Objects (`__call__`)
1085
1194
 
1086
1195
  Any class that defines `__call__` can be invoked directly with `obj(args)`,
1087
- just like Python callable objects. This requires `from __python__ import truthiness`:
1196
+ just like Python callable objects:
1088
1197
 
1089
1198
  ```python
1090
- from __python__ import truthiness
1091
-
1092
1199
  class Multiplier:
1093
1200
  def __init__(self, factor):
1094
1201
  self.factor = factor
@@ -1134,6 +1241,108 @@ Mutation methods (`add`, `remove`, `discard`, `clear`, `update`) are not
1134
1241
  present on `frozenset` instances, enforcing immutability at the API level.
1135
1242
  `frozenset` objects can be iterated and copied with `.copy()`.
1136
1243
 
1244
+ ### `bytes` and `bytearray`
1245
+
1246
+ RapydScript provides `bytes` (immutable) and `bytearray` (mutable) builtins
1247
+ that match Python's semantics and are backed by plain JS arrays of integers
1248
+ in the range 0–255.
1249
+
1250
+ #### `b'...'` bytes literals
1251
+
1252
+ RapydScript supports Python `b'...'` bytes literal syntax. The prefix may be
1253
+ `b` or `B` (and `rb`/`br` for raw bytes where backslash sequences are not
1254
+ interpreted). Adjacent bytes literals are automatically concatenated, just
1255
+ like adjacent string literals.
1256
+
1257
+ ```python
1258
+ b'Hello' # bytes([72, 101, 108, 108, 111])
1259
+ b'\x00\xff' # bytes([0, 255]) — hex escape sequences work
1260
+ b'\n\t\r' # bytes([10, 9, 13]) — control-char escapes work
1261
+ b'foo' b'bar' # bytes([102, 111, 111, 98, 97, 114]) — concatenation
1262
+ rb'\n\t' # bytes([92, 110, 92, 116]) — raw: backslashes literal
1263
+ B'ABC' # bytes([65, 66, 67]) — uppercase B also accepted
1264
+ ```
1265
+
1266
+ Each `b'...'` literal is compiled to a `bytes(str, 'latin-1')` call, so the
1267
+ full `bytes` API is available on the result.
1268
+
1269
+ #### Construction
1270
+
1271
+ ```python
1272
+ bytes() # empty bytes
1273
+ bytes(4) # b'\x00\x00\x00\x00' (4 zero bytes)
1274
+ b'\x00\x00\x00\x00' # same — bytes literal syntax
1275
+ bytes([72, 101, 108, 111]) # b'Hello'
1276
+ b'Hell\x6f' # same — mix of ASCII and hex escapes
1277
+ bytes('Hello', 'utf-8') # encode a string
1278
+ bytes('ABC', 'ascii') # ASCII / latin-1 encoding also accepted
1279
+ bytes.fromhex('48656c6c6f') # from hex string → b'Hello'
1280
+
1281
+ bytearray() # empty mutable byte sequence
1282
+ bytearray(3) # bytearray(b'\x00\x00\x00')
1283
+ bytearray([1, 2, 3]) # from list of ints
1284
+ bytearray('Hi', 'utf-8') # from string
1285
+ bytearray(some_bytes) # mutable copy of a bytes object
1286
+ ```
1287
+
1288
+ `Uint8Array` values may also be passed as the source argument.
1289
+
1290
+ #### Common operations (both `bytes` and `bytearray`)
1291
+
1292
+ ```python
1293
+ b = bytes('Hello', 'utf-8')
1294
+
1295
+ len(b) # 5
1296
+ b[0] # 72 (integer)
1297
+ b[-1] # 111
1298
+ b[1:4] # bytes([101, 108, 108]) (slice → new bytes)
1299
+ b[::2] # every other byte
1300
+
1301
+ b + bytes([33]) # concatenate → b'Hello!'
1302
+ b * 2 # repeat → b'HelloHello'
1303
+ 72 in b # True (integer membership)
1304
+ bytes([101, 108]) in b # True (subsequence membership)
1305
+ b == bytes([72, 101, 108, 108, 111]) # True
1306
+
1307
+ b.hex() # '48656c6c6f'
1308
+ b.hex(':', 2) # '48:65:6c:6c:6f' (separator every 2 bytes)
1309
+ b.decode('utf-8') # 'Hello'
1310
+ b.decode('ascii') # works for ASCII-range bytes
1311
+
1312
+ b.find(bytes([108, 108])) # 2
1313
+ b.index(101) # 1
1314
+ b.rfind(108) # 3
1315
+ b.count(108) # 2
1316
+ b.startswith(bytes([72])) # True
1317
+ b.endswith(bytes([111])) # True
1318
+ b.split(bytes([108])) # [b'He', b'', b'o']
1319
+ b.replace(bytes([108]), bytes([76])) # b'HeLLo'
1320
+ b.strip() # strip leading/trailing whitespace bytes
1321
+ b.upper() # b'HELLO'
1322
+ b.lower() # b'hello'
1323
+ bytes(b' ').join([bytes('a', 'ascii'), bytes('b', 'ascii')]) # b'a b'
1324
+
1325
+ repr(b) # "b'Hello'"
1326
+ isinstance(b, bytes) # True
1327
+ isinstance(bytearray([1]), bytes) # True (bytearray is a subclass of bytes)
1328
+ ```
1329
+
1330
+ #### `bytearray`-only mutation methods
1331
+
1332
+ ```python
1333
+ ba = bytearray([1, 2, 3])
1334
+ ba[0] = 99 # item assignment
1335
+ ba[1:3] = bytes([20, 30]) # slice assignment
1336
+ ba.append(4) # add one byte
1337
+ ba.extend([5, 6]) # add multiple bytes
1338
+ ba.insert(0, 0) # insert at index
1339
+ ba.pop() # remove and return last byte (or ba.pop(i))
1340
+ ba.remove(20) # remove first occurrence of value
1341
+ ba.reverse() # reverse in place
1342
+ ba.clear() # remove all bytes
1343
+ ba += bytearray([7, 8]) # in-place concatenation
1344
+ ```
1345
+
1137
1346
  ### `issubclass`
1138
1347
 
1139
1348
  `issubclass(cls, classinfo)` checks whether a class is a subclass of another
@@ -1173,7 +1382,8 @@ semantics:
1173
1382
  | other `float` | derived from the bit pattern |
1174
1383
  | `str` | djb2 algorithm — stable within a process |
1175
1384
  | object with `__hash__` | dispatches to `__hash__()` |
1176
- | class instance | stable identity hash (assigned on first call) |
1385
+ | class instance (no `__hash__`) | stable identity hash (assigned on first call) |
1386
+ | class with `__eq__` but no `__hash__` | `TypeError` (unhashable — Python semantics) |
1177
1387
  | `list` | `TypeError: unhashable type: 'list'` |
1178
1388
  | `set` | `TypeError: unhashable type: 'set'` |
1179
1389
  | `dict` | `TypeError: unhashable type: 'dict'` |
@@ -1193,8 +1403,213 @@ class Point:
1193
1403
  return self.x * 31 + self.y
1194
1404
 
1195
1405
  hash(Point(1, 2)) # 33
1406
+
1407
+ # Python semantics: __eq__ without __hash__ → unhashable
1408
+ class Bar:
1409
+ def __eq__(self, other):
1410
+ return True
1411
+ hash(Bar()) # TypeError: unhashable type: 'Bar'
1196
1412
  ```
1197
1413
 
1414
+ ### `eval` and `exec`
1415
+
1416
+ Both `eval` and `exec` are supported with Python-compatible signatures.
1417
+ String literals passed to them are treated as **RapydScript source code**: the
1418
+ compiler parses and transpiles the string at compile time, so you write
1419
+ RapydScript (not raw JavaScript) inside the quotes — just like Python's
1420
+ `eval`/`exec` take Python source strings.
1421
+
1422
+ #### `eval(expr[, globals[, locals]])`
1423
+
1424
+ * **One argument** — the compiled expression is passed to the native JS `eval`,
1425
+ giving direct scope access to module-level variables:
1426
+
1427
+ ```python
1428
+ result = eval("1 + 2") # 3
1429
+ x = 7
1430
+ sq = eval("x * x") # 49 (x is in scope)
1431
+ ```
1432
+
1433
+ * **Two or three arguments** — uses the `Function` constructor with explicit
1434
+ variable bindings. `locals` override `globals` when both are given:
1435
+
1436
+ ```python
1437
+ eval("x + y", {"x": 10, "y": 5}) # 15
1438
+ eval("x", {"x": 1}, {"x": 99}) # 99 (local overrides global)
1439
+ ```
1440
+
1441
+ #### `exec(code[, globals[, locals]])`
1442
+
1443
+ Executes a RapydScript code string and always returns `None`, like Python's
1444
+ `exec`.
1445
+
1446
+ * **One argument** — the compiled code runs via native `eval`:
1447
+
1448
+ ```python
1449
+ exec("print('hi')") # prints hi
1450
+ exec("_x = 42") # _x is discarded after exec returns
1451
+ ```
1452
+
1453
+ * **Two or three arguments** — uses the `Function` constructor. Mutable
1454
+ objects (arrays, dicts) passed in `globals` are accessible by reference, so
1455
+ mutations are visible in the caller:
1456
+
1457
+ ```python
1458
+ log = []
1459
+ exec("log.append(1 + 2)", {"log": log})
1460
+ print(log[0]) # 3
1461
+
1462
+ def add(a, b): log.append(a + b);
1463
+ exec("fn(10, 7)", {"fn": add, "log": log})
1464
+ print(log[1]) # 17
1465
+ ```
1466
+
1467
+ > **Note:** Because strings are compiled at compile time, only **string
1468
+ > literals** are transformed — dynamic strings assembled at runtime are passed
1469
+ > through unchanged. `exec(code)` cannot modify the caller's local variables,
1470
+ > matching Python 3 semantics.
1471
+
1472
+ ### `vars`, `locals`, and `globals`
1473
+
1474
+ #### `vars(obj)`
1475
+
1476
+ Returns a `dict` snapshot of the object's own instance attributes, mirroring
1477
+ Python's `obj.__dict__`. Internal RapydScript properties (prefixed `ρσ`) are
1478
+ excluded automatically. Mutating the returned dict does **not** affect the
1479
+ original object.
1480
+
1481
+ ```python
1482
+ class Point:
1483
+ def __init__(self, x, y):
1484
+ self.x = x
1485
+ self.y = y
1486
+
1487
+ p = Point(3, 4)
1488
+ d = vars(p)
1489
+ print(d['x'], d['y']) # 3 4
1490
+ print(list(d.keys())) # ['x', 'y']
1491
+ ```
1492
+
1493
+ #### `vars()` and `locals()`
1494
+
1495
+ Both return an empty `dict`. JavaScript has no runtime mechanism for
1496
+ introspecting the local call-frame's variables, so a faithful implementation is
1497
+ not possible. Use them as placeholders in patterns that require a dict, or
1498
+ pass explicit dicts where you need named-value lookup.
1499
+
1500
+ ```python
1501
+ loc = locals() # {}
1502
+ v = vars() # {}
1503
+ ```
1504
+
1505
+ #### `globals()`
1506
+
1507
+ Returns a `dict` snapshot of the JS global object (`globalThis` / `window` /
1508
+ `global`). Module-level RapydScript variables compiled inside an IIFE or
1509
+ module wrapper will **not** appear here; use a shared plain dict for that
1510
+ pattern instead.
1511
+
1512
+ ```python
1513
+ g = globals()
1514
+ # g contains JS runtime globals such as Math, console, etc.
1515
+ print('Math' in g) # True (in a browser or Node context)
1516
+ ```
1517
+
1518
+ ### Complex numbers
1519
+
1520
+ RapydScript supports Python's complex number type via the `complex` builtin and
1521
+ the `j`/`J` imaginary literal suffix.
1522
+
1523
+ ```py
1524
+ # Imaginary literal suffix
1525
+ z = 4j # complex(0, 4)
1526
+ w = 3 + 4j # complex(3, 4) — parsed as 3 + complex(0, 4)
1527
+
1528
+ # Constructor
1529
+ z1 = complex(3, 4) # real=3, imag=4
1530
+ z2 = complex(5) # real=5, imag=0
1531
+ z3 = complex() # 0+0j
1532
+ z4 = complex('2-3j') # string parsing
1533
+
1534
+ # Attributes
1535
+ print(z1.real) # 3
1536
+ print(z1.imag) # 4
1537
+
1538
+ # Methods
1539
+ print(z1.conjugate()) # (3-4j)
1540
+ print(abs(z1)) # 5.0 — dispatches __abs__
1541
+
1542
+ # Arithmetic (requires overload_operators, which is on by default)
1543
+ from __python__ import overload_operators
1544
+ print(z1 + z2) # (8+4j)
1545
+ print(z1 - z2) # (-2+4j)
1546
+ print(z1 * z2) # (15+20j)
1547
+ print(z1 / z2) # (0.6+0.8j)
1548
+
1549
+ # Truthiness, repr, isinstance
1550
+ print(bool(complex(0, 0))) # False
1551
+ print(repr(z1)) # (3+4j)
1552
+ print(isinstance(z1, complex)) # True
1553
+ ```
1554
+
1555
+ The `j`/`J` suffix is handled at the tokenizer level: `4j` is parsed into an
1556
+ `AST_Call(complex, 0, 4)` node, so it composes naturally with all other
1557
+ expressions. Mixed expressions like `3 + 4j` work without `overload_operators`
1558
+ because `ρσ_list_add` dispatches `__radd__` on the right operand.
1559
+
1560
+ ### Attribute-Access Dunders
1561
+
1562
+ RapydScript supports the four Python attribute-interception hooks:
1563
+ `__getattr__`, `__setattr__`, `__delattr__`, and `__getattribute__`.
1564
+ When a class defines any of them, instances are automatically wrapped in a
1565
+ JavaScript `Proxy` that routes attribute access through the hooks — including
1566
+ accesses that occur inside `__init__`.
1567
+
1568
+ | Hook | When called |
1569
+ |---|---|
1570
+ | `__getattr__(self, name)` | Fallback — only called when normal lookup finds nothing |
1571
+ | `__setattr__(self, name, value)` | Every attribute assignment (including `self.x = …` in `__init__`) |
1572
+ | `__delattr__(self, name)` | Every `del obj.attr` |
1573
+ | `__getattribute__(self, name)` | Every attribute read (overrides normal lookup) |
1574
+
1575
+ To bypass the hooks from within the hook itself (avoiding infinite recursion),
1576
+ use the `object.*` bypass functions:
1577
+
1578
+ | Python idiom | Compiled form | Effect |
1579
+ |---|---|---|
1580
+ | `object.__setattr__(self, name, val)` | `ρσ_object_setattr(self, name, val)` | Set attribute directly, bypassing `__setattr__` |
1581
+ | `object.__getattribute__(self, name)` | `ρσ_object_getattr(self, name)` | Read attribute directly, bypassing `__getattribute__` |
1582
+ | `object.__delattr__(self, name)` | `ρσ_object_delattr(self, name)` | Delete attribute directly, bypassing `__delattr__` |
1583
+
1584
+ Subclasses automatically inherit proxy wrapping from their parent class — if
1585
+ `Base` defines `__getattr__`, all `Child(Base)` instances are also Proxy-wrapped.
1586
+
1587
+ ```py
1588
+ class Validated:
1589
+ """Reject negative values at assignment time."""
1590
+ def __setattr__(self, name, value):
1591
+ if jstype(value) is 'number' and value < 0:
1592
+ raise ValueError(name + ' must be non-negative')
1593
+ object.__setattr__(self, name, value)
1594
+
1595
+ v = Validated()
1596
+ v.x = 5 # ok
1597
+ v.x = -1 # ValueError: x must be non-negative
1598
+
1599
+ class AttrProxy:
1600
+ """Log every attribute read."""
1601
+ def __init__(self):
1602
+ object.__setattr__(self, '_log', [])
1603
+
1604
+ def __getattribute__(self, name):
1605
+ self._log.append(name) # self._log goes through __getattribute__ too!
1606
+ return object.__getattribute__(self, name)
1607
+ ```
1608
+
1609
+ > **Proxy support required** — The hooks rely on `Proxy`, which is available
1610
+ > in all modern browsers and Node.js ≥ 6. In environments that lack `Proxy`
1611
+ > the class still works, but the hooks are silently bypassed.
1612
+
1198
1613
  Loops
1199
1614
  -----
1200
1615
  RapydScript's loops work like Python, not JavaScript. You can't, for example
@@ -1377,8 +1792,33 @@ format('hi', '>10') # ' hi' — right-aligned in 10-char field
1377
1792
  format(42) # '42' — no spec: same as str(42)
1378
1793
  ```
1379
1794
 
1380
- Objects with a `__format__` method are dispatched to it, matching Python's
1381
- protocol exactly.
1795
+ Objects with a `__format__` method are dispatched to it in all three contexts
1796
+ `format(obj, spec)`, `str.format('{:spec}', obj)`, and `f'{obj:spec}'` —
1797
+ matching Python's protocol exactly. Every user-defined class automatically
1798
+ gets a default `__format__` that returns `str(self)` for an empty spec and
1799
+ raises `TypeError` for any other spec, just like `object.__format__` in
1800
+ Python:
1801
+
1802
+ ```py
1803
+ class Money:
1804
+ def __init__(self, amount):
1805
+ self.amount = amount
1806
+ def __str__(self):
1807
+ return str(self.amount)
1808
+ def __format__(self, spec):
1809
+ if spec == 'usd':
1810
+ return '$' + str(self.amount)
1811
+ return format(self.amount, spec) # delegate numeric specs
1812
+
1813
+ m = Money(42)
1814
+ format(m, 'usd') # '$42'
1815
+ str.format('{:usd}', m) # '$42'
1816
+ f'{m:usd}' # '$42'
1817
+ f'{m:.2f}' # '42.00'
1818
+ ```
1819
+
1820
+ The `!r`, `!s`, and `!a` conversion flags apply `repr()`/`str()`/`repr()` to the
1821
+ value before formatting, bypassing `__format__` (same as Python).
1382
1822
 
1383
1823
  String predicate methods are also available:
1384
1824
 
@@ -1405,6 +1845,16 @@ Case-folding for locale-insensitive lowercase comparison:
1405
1845
  str.casefold('ÄÖÜ') == str.casefold('äöü') # True (maps to lowercase)
1406
1846
  ```
1407
1847
 
1848
+ Tab expansion:
1849
+
1850
+ ```py
1851
+ str.expandtabs('a\tb', 4) # 'a b' — expand to next 4-space tab stop
1852
+ str.expandtabs('\t\t', 8) # ' ' — two full tab stops
1853
+ str.expandtabs('ab\tc', 4) # 'ab c' — only 2 spaces needed to reach next stop
1854
+ ```
1855
+
1856
+ The optional `tabsize` argument defaults to `8`, matching Python's default. A `tabsize` of `0` removes all tab characters. Newline (`\n`) and carriage-return (`\r`) characters reset the column counter, so each line is expanded independently.
1857
+
1408
1858
  However, if you want to make the python string methods available on string
1409
1859
  objects, there is a convenience method in the standard library to do so. Use
1410
1860
  the following code:
@@ -1612,6 +2062,7 @@ can annotate a variable with a type hint, with or without an initial value:
1612
2062
  x: int = 42
1613
2063
  name: str = "Alice"
1614
2064
  items: list = [1, 2, 3]
2065
+ coords: tuple = (10, 20)
1615
2066
 
1616
2067
  # Annotation only: declares the type without assigning a value
1617
2068
  count: int
@@ -1656,18 +2107,22 @@ Regular Expressions
1656
2107
  ----------------------
1657
2108
 
1658
2109
  RapydScript includes a ```re``` module that mimics the interface of the Python
1659
- re module. However, it uses the JavaScript regular expression functionality
1660
- under the hood, which has several differences from the Python regular
1661
- expression engine. Most importantly:
1662
-
1663
- - it does not support lookbehind and group existence assertions
1664
- - it does not support unicode (on ES 6 runtimes, unicode is supported, but
1665
- with a different syntax). You can test for the presence of unicode support with
1666
- ```re.supports_unicode```.
1667
- - The ``MatchObject``'s ``start()`` and ``end()`` method cannot return correct values
1668
- for subgroups for some kinds of regular expressions, for example, those
1669
- with nested captures. This is because the JavaScript regex API does not expose
1670
- this information, so it has to be guessed via a heuristic.
2110
+ re module. It uses the JavaScript regular expression engine under the hood, so
2111
+ it supports the full feature set available in modern JS runtimes:
2112
+
2113
+ - **Lookbehind assertions** — both positive `(?<=...)` and negative `(?<!...)`
2114
+ are fully supported (ES2018+), including variable-width lookbehind.
2115
+ - **Unicode** the `u` flag is added automatically on ES2015+ runtimes.
2116
+ `re.supports_unicode` reflects whether the runtime supports it.
2117
+ - **`re.fullmatch()`** — matches the entire string against the pattern.
2118
+ - **`re.S` / `re.DOTALL`** make `.` match newlines; `re.S` is now the
2119
+ canonical alias (matching Python), with `re.D` kept for compatibility.
2120
+ - **`re.NOFLAG`** (= 0) the Python 3.11 no-flags sentinel.
2121
+ - **`MatchObject.start()`/`.end()`** return accurate positions for all
2122
+ sub-groups on runtimes that support the ES2022 `d` (hasIndices) flag
2123
+ (Node 18+, Chrome 90+). On older runtimes a heuristic is used.
2124
+ - **Conditional groups** `(?(id)yes|no)` — not supported in JavaScript;
2125
+ an `re.error` is raised if they appear in the pattern.
1671
2126
 
1672
2127
  You can use the JavaScript regex literal syntax, including verbose regex
1673
2128
  literals, as shown below. In verbose mode, whitespace is ignored and # comments
@@ -1682,8 +2137,338 @@ re.match(///
1682
2137
  a # a comment
1683
2138
  b # Another comment
1684
2139
  ///, 'ab')
2140
+
2141
+ # Lookbehind and fullmatch
2142
+ re.sub(r'(?<=\d)px', '', '12px 3em') # '12 3em'
2143
+ re.fullmatch(r'\w+', 'hello') # MatchObject
2144
+ re.fullmatch(r'\w+', 'hello world') # None
2145
+ ```
2146
+
2147
+ JSX Support
2148
+ -----------
2149
+
2150
+ RapydScript supports JSX syntax for building React UI components. JSX elements compile directly to `React.createElement()` calls, so the output is plain JavaScript — no Babel or JSX transform step is needed.
2151
+
2152
+ JSX support is **on by default**. The ``jsx`` flag can be disabled with
2153
+ ``from __python__ import no_jsx`` if needed.
2154
+
2155
+ ### Requirements
2156
+
2157
+ `React` must be in scope at runtime. How you provide it depends on your environment:
2158
+
2159
+ - **Bundler (Vite, webpack, etc.):** `import React from 'react'` at the top of your file (or configure your bundler's global React shim).
2160
+ - **CDN / browser script tag:** load React before your compiled script.
2161
+ - **Bitburner:** React is already available as a global — no import needed.
2162
+ - **RapydScript web REPL:** a minimal React stub is injected automatically so `React.createElement` calls succeed.
2163
+
2164
+ ### Output format
2165
+
2166
+ RapydScript compiles JSX to `React.createElement()` calls. For example:
2167
+
2168
+ ```py
2169
+ def Greeting(props):
2170
+ return <h1>Hello, {props.name}!</h1>
2171
+ ```
2172
+
2173
+ Compiles to:
2174
+
2175
+ ```js
2176
+ function Greeting(props) {
2177
+ return React.createElement("h1", null, "Hello, ", props.name);
2178
+ }
1685
2179
  ```
1686
2180
 
2181
+ Lowercase tags (`div`, `span`, `h1`, …) become string arguments. Capitalised names and dot-notation (`MyComponent`, `Router.Route`) are passed as references — not strings.
2182
+
2183
+ ### Attributes
2184
+
2185
+ String, expression, boolean, and hyphenated attribute names all work:
2186
+
2187
+ ```py
2188
+ def Form(props):
2189
+ return (
2190
+ <form>
2191
+ <input
2192
+ type="text"
2193
+ aria-label="Name"
2194
+ disabled={props.readonly}
2195
+ onChange={props.onChange}
2196
+ required
2197
+ />
2198
+ </form>
2199
+ )
2200
+ ```
2201
+
2202
+ Hyphenated names (e.g. `aria-label`, `data-id`) are automatically quoted as object keys: `{"aria-label": "Name"}`. Boolean attributes with no value compile to `true`.
2203
+
2204
+ ### Nested elements and expressions
2205
+
2206
+ ```py
2207
+ def UserList(users):
2208
+ return (
2209
+ <ul className="user-list">
2210
+ {[<li key={u.id}>{u.name}</li> for u in users]}
2211
+ </ul>
2212
+ )
2213
+ ```
2214
+
2215
+ ### Fragments
2216
+
2217
+ Use `<>...</>` to return multiple elements without a wrapper node. Fragments compile to `React.createElement(React.Fragment, null, ...)`:
2218
+
2219
+ ```py
2220
+ def TwoItems():
2221
+ return (
2222
+ <>
2223
+ <span>First</span>
2224
+ <span>Second</span>
2225
+ </>
2226
+ )
2227
+ ```
2228
+
2229
+ ### Self-closing elements
2230
+
2231
+ ```py
2232
+ def Avatar(props):
2233
+ return <img src={props.url} alt={props.name} />
2234
+ ```
2235
+
2236
+ ### Spread attributes
2237
+
2238
+ ```py
2239
+ def Button(props):
2240
+ return <button {...props}>Click me</button>
2241
+ ```
2242
+
2243
+ Compiles to `React.createElement("button", {...props}, "Click me")`.
2244
+
2245
+ ### Component tags
2246
+
2247
+ Capitalised names and dot-notation are treated as component references (not quoted strings):
2248
+
2249
+ ```py
2250
+ def App():
2251
+ return (
2252
+ <Router.Provider>
2253
+ <MyComponent name="hello" />
2254
+ </Router.Provider>
2255
+ )
2256
+ ```
2257
+
2258
+ Compiles to:
2259
+
2260
+ ```js
2261
+ React.createElement(Router.Provider, null,
2262
+ React.createElement(MyComponent, {name: "hello"})
2263
+ )
2264
+ ```
2265
+
2266
+ ### Compiling JSX files
2267
+
2268
+ Since the output is plain JavaScript, compile to a `.js` file as normal:
2269
+
2270
+ ```sh
2271
+ rapydscript mycomponent.pyj --output mycomponent.js
2272
+ ```
2273
+
2274
+ React Standard Library
2275
+ -----------------------
2276
+
2277
+ RapydScript ships a `react` standard library module that re-exports every standard React hook and utility under their familiar Python-friendly names. Import the pieces you need and the compiler will resolve each name to the corresponding `React.*` property at compile time.
2278
+
2279
+ ### Importing hooks
2280
+
2281
+ ```py
2282
+ from react import useState, useEffect, useCallback, useMemo, useRef
2283
+
2284
+ def Counter():
2285
+ count, setCount = useState(0)
2286
+
2287
+ def increment():
2288
+ setCount(count + 1)
2289
+
2290
+ return <button onClick={increment}>{count}</button>
2291
+ ```
2292
+
2293
+ Compiles to:
2294
+
2295
+ ```js
2296
+ var useState = React.useState;
2297
+ // ...
2298
+ function Counter() {
2299
+ var [count, setCount] = React.useState(0);
2300
+ function increment() {
2301
+ setCount(count + 1);
2302
+ }
2303
+ return React.createElement("button", {onClick: increment}, count);
2304
+ }
2305
+ ```
2306
+
2307
+ Tuple unpacking works naturally because `React.useState` returns a two-element array — `count, setCount = useState(0)` compiles to the ES6 destructuring `var [count, setCount] = React.useState(0)`.
2308
+
2309
+ ### Available exports
2310
+
2311
+ **Hooks (React 16.8+)**
2312
+
2313
+ | Import name | React API |
2314
+ |---|---|
2315
+ | `useState` | `React.useState` |
2316
+ | `useEffect` | `React.useEffect` |
2317
+ | `useContext` | `React.useContext` |
2318
+ | `useReducer` | `React.useReducer` |
2319
+ | `useCallback` | `React.useCallback` |
2320
+ | `useMemo` | `React.useMemo` |
2321
+ | `useRef` | `React.useRef` |
2322
+ | `useImperativeHandle` | `React.useImperativeHandle` |
2323
+ | `useLayoutEffect` | `React.useLayoutEffect` |
2324
+ | `useDebugValue` | `React.useDebugValue` |
2325
+
2326
+ **Hooks (React 18+)**
2327
+
2328
+ | Import name | React API |
2329
+ |---|---|
2330
+ | `useId` | `React.useId` |
2331
+ | `useTransition` | `React.useTransition` |
2332
+ | `useDeferredValue` | `React.useDeferredValue` |
2333
+ | `useSyncExternalStore` | `React.useSyncExternalStore` |
2334
+ | `useInsertionEffect` | `React.useInsertionEffect` |
2335
+
2336
+ **Core classes and elements**
2337
+
2338
+ | Import name | React API |
2339
+ |---|---|
2340
+ | `Component` | `React.Component` |
2341
+ | `PureComponent` | `React.PureComponent` |
2342
+ | `Fragment` | `React.Fragment` |
2343
+ | `StrictMode` | `React.StrictMode` |
2344
+ | `Suspense` | `React.Suspense` |
2345
+ | `Profiler` | `React.Profiler` |
2346
+
2347
+ **Utilities**
2348
+
2349
+ | Import name | React API |
2350
+ |---|---|
2351
+ | `createElement` | `React.createElement` |
2352
+ | `cloneElement` | `React.cloneElement` |
2353
+ | `createContext` | `React.createContext` |
2354
+ | `createRef` | `React.createRef` |
2355
+ | `forwardRef` | `React.forwardRef` |
2356
+ | `isValidElement` | `React.isValidElement` |
2357
+ | `memo` | `React.memo` |
2358
+ | `lazy` | `React.lazy` |
2359
+
2360
+ ### Common patterns
2361
+
2362
+ **useEffect with cleanup**
2363
+
2364
+ ```py
2365
+ from react import useState, useEffect
2366
+
2367
+ def Timer():
2368
+ count, setCount = useState(0)
2369
+ def setup():
2370
+ interval = setInterval(def(): setCount(count + 1);, 1000)
2371
+ def cleanup():
2372
+ clearInterval(interval)
2373
+ return cleanup
2374
+ useEffect(setup, [count])
2375
+ return count
2376
+ ```
2377
+
2378
+ **useReducer**
2379
+
2380
+ ```py
2381
+ from react import useReducer
2382
+
2383
+ def reducer(state, action):
2384
+ if action.type == 'increment':
2385
+ return state + 1
2386
+ if action.type == 'decrement':
2387
+ return state - 1
2388
+ return state
2389
+
2390
+ def Counter():
2391
+ state, dispatch = useReducer(reducer, 0)
2392
+ def inc():
2393
+ dispatch({'type': 'increment'})
2394
+ return state
2395
+ ```
2396
+
2397
+ **useContext**
2398
+
2399
+ ```py
2400
+ from react import createContext, useContext
2401
+
2402
+ ThemeContext = createContext('light')
2403
+
2404
+ def ThemedButton():
2405
+ theme = useContext(ThemeContext)
2406
+ return theme
2407
+ ```
2408
+
2409
+ **useRef**
2410
+
2411
+ ```py
2412
+ from react import useRef
2413
+
2414
+ def FocusInput():
2415
+ inputRef = useRef(None)
2416
+ def handleClick():
2417
+ inputRef.current.focus()
2418
+ return <input ref={inputRef} />
2419
+ ```
2420
+
2421
+ **memo**
2422
+
2423
+ ```py
2424
+ from react import memo
2425
+
2426
+ def Row(props):
2427
+ return <li>{props.label}</li>
2428
+
2429
+ MemoRow = memo(Row)
2430
+ ```
2431
+
2432
+ **forwardRef**
2433
+
2434
+ ```py
2435
+ from react import forwardRef
2436
+
2437
+ def FancyInput(props, ref):
2438
+ return <input ref={ref} placeholder={props.placeholder} />
2439
+
2440
+ FancyInputWithRef = forwardRef(FancyInput)
2441
+ ```
2442
+
2443
+ **Class component**
2444
+
2445
+ You can extend `React.Component` directly without importing it, or import `Component` from the `react` module:
2446
+
2447
+ ```py
2448
+ from react import Component
2449
+
2450
+ class Greeter(Component):
2451
+ def render(self):
2452
+ return <h1>Hello, {self.props.name}!</h1>
2453
+ ```
2454
+
2455
+ **useTransition (React 18)**
2456
+
2457
+ ```py
2458
+ from react import useState, useTransition
2459
+
2460
+ def SearchInput():
2461
+ isPending, startTransition = useTransition()
2462
+ query, setQuery = useState('')
2463
+ def handleChange(e):
2464
+ startTransition(def(): setQuery(e.target.value);)
2465
+ return isPending
2466
+ ```
2467
+
2468
+ ### Requirements
2469
+
2470
+ The `react` module does not bundle React itself — it provides compile-time name bindings only. `React` must be available as a global variable at runtime, exactly as described in the [JSX Requirements](#requirements) section above.
2471
+
1687
2472
  Creating DOM trees easily
1688
2473
  ---------------------------------
1689
2474
 
@@ -1914,6 +2699,117 @@ print(Counter.get_count()) # 2
1914
2699
 
1915
2700
  The `@classmethod` decorator compiles to a method placed directly on the class (not its prototype), with `cls` mapped to `this`. A prototype delegation shim is also generated so instance calls work correctly.
1916
2701
 
2702
+ ### `__new__` Constructor Hook
2703
+
2704
+ RapydScript supports Python's `__new__` method, which runs *before* `__init__` and controls instance creation. Use it to implement patterns like singletons or alternative constructors:
2705
+
2706
+ ```py
2707
+ class Singleton:
2708
+ _instance = None
2709
+ def __new__(cls):
2710
+ if cls._instance is None:
2711
+ cls._instance = super().__new__(cls)
2712
+ return cls._instance
2713
+ def __init__(self):
2714
+ pass
2715
+
2716
+ a = Singleton()
2717
+ b = Singleton()
2718
+ assert a is b # same instance
2719
+ ```
2720
+
2721
+ `super().__new__(cls)` creates a bare instance of `cls` (equivalent to `Object.create(cls.prototype)` in JavaScript). If `__new__` returns an instance of the class, `__init__` is called on it automatically. If it returns something else, `__init__` is skipped.
2722
+
2723
+ Class variables accessed via `cls` inside `__new__` are correctly rewritten to `cls.prototype.varname`, matching Python's semantics.
2724
+
2725
+ ### `__class_getitem__`
2726
+
2727
+ RapydScript supports Python's `__class_getitem__` hook, which enables subscript syntax on a class itself (`MyClass[item]`). Define `__class_getitem__(cls, item)` in a class body to intercept `ClassName[x]`:
2728
+
2729
+ ```py
2730
+ class Box:
2731
+ def __class_getitem__(cls, item):
2732
+ return cls.__name__ + '[' + str(item) + ']'
2733
+
2734
+ print(Box[int]) # Box[<class 'int'>]
2735
+ print(Box['str']) # Box[str]
2736
+ ```
2737
+
2738
+ `__class_getitem__` is an implicit `@classmethod`: the compiler strips `cls` from the JS parameter list and maps it to `this`, so calling `Box[item]` compiles to `Box.__class_getitem__(item)` with `this = Box`.
2739
+
2740
+ Subclasses inherit `__class_getitem__` from their parent and receive the subclass as `cls`:
2741
+
2742
+ ```py
2743
+ class Base:
2744
+ def __class_getitem__(cls, item):
2745
+ return cls.__name__ + '<' + str(item) + '>'
2746
+
2747
+ class Child(Base):
2748
+ pass
2749
+
2750
+ print(Base[42]) # Base<42>
2751
+ print(Child[42]) # Child<42>
2752
+ ```
2753
+
2754
+ Class variables declared in the class body are accessible via `cls.varname` inside `__class_getitem__`, just as with `@classmethod`.
2755
+
2756
+ ### `__init_subclass__`
2757
+
2758
+ `__init_subclass__` is a hook that is called automatically on a base class whenever a subclass is created. It is an implicit `@classmethod`: the compiler strips `cls` from the JS signature and maps it to `this`, so `cls` receives the newly-created subclass.
2759
+
2760
+ ```py
2761
+ class PluginBase:
2762
+ _plugins = []
2763
+
2764
+ def __init_subclass__(cls, **kwargs):
2765
+ PluginBase._plugins.append(cls)
2766
+
2767
+ class AudioPlugin(PluginBase):
2768
+ pass
2769
+
2770
+ class VideoPlugin(PluginBase):
2771
+ pass
2772
+
2773
+ print(len(PluginBase._plugins)) # 2
2774
+ print(PluginBase._plugins[0].__name__) # AudioPlugin
2775
+ ```
2776
+
2777
+ Keyword arguments written in the class header are forwarded to `__init_subclass__`:
2778
+
2779
+ ```py
2780
+ class Base:
2781
+ def __init_subclass__(cls, required=False, **kwargs):
2782
+ cls._required = required
2783
+
2784
+ class Strict(Base, required=True):
2785
+ pass
2786
+
2787
+ class Loose(Base):
2788
+ pass
2789
+
2790
+ print(Strict._required) # True
2791
+ print(Loose._required) # False
2792
+ ```
2793
+
2794
+ Use `super().__init_subclass__(**kwargs)` to propagate the hook up the hierarchy:
2795
+
2796
+ ```py
2797
+ class GrandParent:
2798
+ def __init_subclass__(cls, **kwargs):
2799
+ cls._from_grandparent = True
2800
+
2801
+ class Parent(GrandParent):
2802
+ def __init_subclass__(cls, **kwargs):
2803
+ super().__init_subclass__(**kwargs) # propagates to GrandParent
2804
+
2805
+ class Child(Parent):
2806
+ pass
2807
+
2808
+ print(Child._from_grandparent) # True
2809
+ ```
2810
+
2811
+ The hook is called after the subclass is fully set up (including `__name__`, `__qualname__`, and `__module__`), so `cls.__name__` is always the correct subclass name inside the hook.
2812
+
1917
2813
  ### Nested Classes
1918
2814
 
1919
2815
  A class may be defined inside another class. The nested class becomes an attribute of the outer class (accessible as `Outer.Inner`) and is also reachable via instances (`self.Inner` inside methods). This mirrors Python semantics exactly.
@@ -2014,67 +2910,51 @@ You could also use `external` decorator to bypass improperly imported RapydScrip
2014
2910
 
2015
2911
  ### Method Binding
2016
2912
 
2017
- By default, RapydScript does not bind methods to the classes they're declared under. This behavior is unlike Python, but very much like the rest of JavaScript. For example, consider this code:
2913
+ RapydScript automatically binds methods to their objects by default (the
2914
+ ``bound_methods`` flag is **on by default**). This means method references
2915
+ like ``getattr(obj, 'method')`` work correctly when called later.
2018
2916
 
2019
- ```py
2020
- class Boy:
2021
- def __init__(self, name):
2022
- self.name = name
2023
-
2024
- def greet(self):
2025
- if self:
2026
- print('My name is' + self.name)
2027
-
2028
- tod = Boy('Tod')
2029
- tod.greet() # Hello, my name is Tod
2030
- getattr(tod, 'greet')() # prints nothing
2031
- ```
2917
+ If you need to disable auto-binding in a scope, use
2918
+ ``from __python__ import no_bound_methods``.
2032
2919
 
2033
- In some cases, however, you may wish for the functions in the class to be
2034
- automatically bound when the objects of that class are instantiated. In order
2035
- to do that, use a *scoped flag*, which is a simple instruction to the compiler
2036
- telling it to auto-bind methods, as shown below:
2920
+ For example:
2037
2921
 
2038
2922
  ```py
2923
+ class C:
2924
+ def __init__(self):
2925
+ self.a = 3
2039
2926
 
2040
- class AutoBound:
2041
- from __python__ import bound_methods
2042
-
2043
- def __init__(self):
2044
- self.a = 3
2045
-
2046
- def val(self):
2047
- return self.a
2927
+ def val(self):
2928
+ return self.a
2048
2929
 
2049
- getattr(AutoBound(), 'val')() == 3
2930
+ getattr(C(), 'val')() == 3 # works because bound_methods is on by default
2050
2931
  ```
2051
2932
 
2052
- If you want all classes in a module to be auto-bound simply put the scoped flag
2053
- at the top of the module. You can even choose to have only a few methods of the
2054
- class auto-bound, like this:
2933
+ You can mix bound and unbound methods within a class using ``no_bound_methods``
2934
+ and ``bound_methods`` to toggle at any point:
2055
2935
 
2056
2936
  ```py
2057
2937
  class C:
2058
2938
 
2059
- def unbound1(self):
2060
- pass # this method will not be auto-bound
2061
-
2062
- from __python__ import bound_methods
2063
- # Methods below this line will be auto-bound
2064
-
2065
- def bound(self):
2066
- pass # This method will be auto-bound
2939
+ def bound1(self):
2940
+ pass # auto-bound (default)
2067
2941
 
2068
2942
  from __python__ import no_bound_methods
2069
2943
  # Methods below this line will not be auto-bound
2070
2944
 
2071
- def unbound2(self):
2072
- pass # this method will be unbound
2945
+ def unbound(self):
2946
+ pass # not auto-bound
2947
+
2948
+ from __python__ import bound_methods
2949
+ # Methods below this line will be auto-bound again
2950
+
2951
+ def bound2(self):
2952
+ pass # auto-bound
2073
2953
  ```
2074
2954
 
2075
2955
  Scoped flags apply only to the scope they are defined in, so if you define them
2076
2956
  inside a class declaration, they only apply to that class. If you define it at
2077
- the module level, it will only apply to all classes in the module that occur
2957
+ the module level, it will apply to all classes in the module that occur
2078
2958
  below that line, and so on.
2079
2959
 
2080
2960
  Iterators
@@ -2350,6 +3230,45 @@ except SomeInternalError:
2350
3230
 
2351
3231
  Basically, `try/except/finally` in RapydScript works very similar to the way it does in Python 3.
2352
3232
 
3233
+ ### Exception Groups (`except*`, Python 3.11+)
3234
+
3235
+ RapydScript supports exception groups via the `ExceptionGroup` class and `except*` syntax. An `ExceptionGroup` bundles multiple exceptions under a single message:
3236
+
3237
+ ```py
3238
+ eg = ExceptionGroup("network errors", [
3239
+ TimeoutError("host A timed out"),
3240
+ TimeoutError("host B timed out"),
3241
+ ValueError("bad address"),
3242
+ ])
3243
+ raise eg
3244
+ ```
3245
+
3246
+ Use `except*` to handle exceptions by type — each handler receives a sub-group containing only the matching exceptions:
3247
+
3248
+ ```py
3249
+ try:
3250
+ fetch_all()
3251
+ except* TimeoutError as group:
3252
+ for e in group.exceptions:
3253
+ print("timeout:", e)
3254
+ except* ValueError as group:
3255
+ print("bad input:", group.exceptions[0])
3256
+ ```
3257
+
3258
+ - Each `except*` clause sees the exceptions not already matched by earlier clauses.
3259
+ - Any unmatched exceptions are automatically re-raised as a new `ExceptionGroup`.
3260
+ - A bare `except*:` (no type) catches all remaining exceptions.
3261
+ - You cannot mix `except` and `except*` in the same `try` block.
3262
+ - Plain (non-group) exceptions can also be caught with `except*`; the variable is bound to the exception itself rather than a sub-group.
3263
+
3264
+ `ExceptionGroup` also provides `subgroup(condition)` and `split(condition)` for programmatic filtering, where `condition` is either an exception class or a predicate function:
3265
+
3266
+ ```py
3267
+ eg = ExceptionGroup("mixed", [ValueError("v"), TypeError("t")])
3268
+ ve_group = eg.subgroup(ValueError) # ExceptionGroup of just ValueErrors
3269
+ matched, rest = eg.split(ValueError) # (ve_group, te_group)
3270
+ ```
3271
+
2353
3272
  Scope Control
2354
3273
  -------------
2355
3274
 
@@ -2416,7 +3335,16 @@ One of Python's main strengths is the number of libraries available to the devel
2416
3335
  gettext # Support for internationalization of your RapydScript app
2417
3336
  operator # a subset of Python's operator module
2418
3337
  functools # reduce, partial, wraps, lru_cache, cache, total_ordering, cmp_to_key
3338
+ enum # Enum base class — class Color(Enum): RED=1 with .name/.value, iteration
3339
+ dataclasses # @dataclass decorator — auto-generates __init__, __repr__, __eq__; field(),
3340
+ # fields(), asdict(), astuple(), replace(), is_dataclass(), frozen=True, order=True
3341
+ abc # ABC base class, @abstractmethod, Protocol, @runtime_checkable;
3342
+ # abstract enforcement at instantiation; ABC.register() virtual subclasses
2419
3343
  collections # namedtuple, deque, Counter, OrderedDict, defaultdict
3344
+ copy # copy (shallow), deepcopy; honours __copy__ / __deepcopy__ hooks
3345
+ typing # TYPE_CHECKING, Any, Union, Optional, List, Dict, Set, Tuple, TypeVar,
3346
+ # Generic, Protocol, Callable, Literal, Final, TypedDict, NamedTuple,
3347
+ # ByteString, AnyStr (str | bytes), cast, …
2420
3348
  itertools # count, cycle, repeat, accumulate, chain, compress, dropwhile, filterfalse,
2421
3349
  # groupby, islice, pairwise, starmap, takewhile, zip_longest,
2422
3350
  # product, permutations, combinations, combinations_with_replacement
@@ -2624,18 +3552,21 @@ As a result, there are some things in RapydScript that might come as surprises
2624
3552
  to an experienced Python developer. The most important such gotchas are listed
2625
3553
  below:
2626
3554
 
2627
- - Truthiness in JavaScript is very different from Python. Empty lists and dicts
2628
- are ``False`` in Python but ``True`` in JavaScript. You can opt in to full
2629
- Python truthiness semantics (where empty containers are falsy and ``__bool__``
2630
- is dispatched) with ``from __python__ import truthiness``. Without that flag,
2631
- test the length explicitly instead of the container directly.
2632
-
2633
- - Operators in JavaScript are very different from Python. ``1 + '1'`` would be
2634
- an error in Python, but results in ``'11'`` in JavaScript. Similarly, ``[1] +
2635
- [1]`` is a new list in Python, but a string in JavaScript. Keep that in mind
2636
- as you write code. By default, RapydScript does not implement operator
2637
- overloading for performance reasons. You can opt in via the
2638
- ``overload_operators`` scoped flag (see below).
3555
+ - RapydScript uses Python truthiness semantics by default: empty lists and dicts
3556
+ are falsy and ``__bool__`` is dispatched. This is controlled by the
3557
+ ``truthiness`` flag, which is on by default. Use
3558
+ ``from __python__ import no_truthiness`` to fall back to JavaScript truthiness
3559
+ in a scope.
3560
+
3561
+ - Operator overloading is enabled by default via the ``overload_operators``
3562
+ flag, so ``[1] + [1]`` produces a new list and ``'ha' * 3`` produces
3563
+ ``'hahaha'``. Type-checking is controlled by the separate ``strict_arithmetic``
3564
+ flag (also on by default): mixing incompatible types raises ``TypeError``
3565
+ (e.g. ``1 + 'x'`` ``TypeError: unsupported operand type(s) for +: 'int'
3566
+ and 'str'``). Use ``from __python__ import no_strict_arithmetic`` to keep
3567
+ dunder dispatch but revert to JavaScript's silent coercion for unrecognised
3568
+ type combinations. Use ``from __python__ import no_overload_operators`` to
3569
+ disable operator overloading entirely.
2639
3570
 
2640
3571
  - There are many more keywords than in Python. Because RapydScript compiles
2641
3572
  down to JavaScript, the set of keywords is all the keywords of Python + all
@@ -2656,9 +3587,40 @@ below:
2656
3587
  yourself. Similarly, the compiler will try to convert SomeClass.method() into
2657
3588
  SomeClass.prototype.method() for you, but again, this is not 100% reliable.
2658
3589
 
2659
- - The {"a":b} syntax is used to create JavaScript hashes. These do not behave
2660
- like python dictionaries. To create python like dictionary objects, you
2661
- should use a scoped flag. See the section on dictionaries above for details.
3590
+ - The ``{"a":b}`` syntax creates Python ``dict`` objects by default (the
3591
+ ``dict_literals`` flag is on by default). Use
3592
+ ``from __python__ import no_dict_literals`` to get plain JavaScript objects
3593
+ in a scope. See the section on dictionaries above for details.
3594
+
3595
+
3596
+ Python Flags
3597
+ ------------
3598
+
3599
+ Python flags are scoped compiler directives that control Python semantics.
3600
+ All flags are **on by default**. They can be turned off in a scope with
3601
+ the ``no_`` prefix. In source code they are written as:
3602
+
3603
+ ```py
3604
+ from __python__ import flag_name
3605
+ ```
3606
+
3607
+ At the top level they take effect for the rest of the file; inside a function
3608
+ or class body they apply only to that scope. Prefix a flag with `no_` to turn
3609
+ it off in a scope (e.g. `from __python__ import no_truthiness`).
3610
+
3611
+ All flags are **on by default**. To revert to legacy RapydScript behavior
3612
+ with no flags enabled, pass ``--legacy-rapydscript`` on the command line.
3613
+
3614
+ | Flag | Description |
3615
+ |---|---|
3616
+ | `dict_literals` | `{k: v}` literals create Python `dict` objects instead of plain JS objects. On by default. |
3617
+ | `overload_getitem` | `obj[key]` dispatches to `__getitem__` / `__setitem__` / `__delitem__` on objects that define them. On by default. |
3618
+ | `overload_operators` | Arithmetic and bitwise operators (`+`, `-`, `*`, `/`, `//`, `%`, `**`, `&`, `\|`, `^`, `<<`, `>>`) dispatch to dunder methods (`__add__`, `__sub__`, etc.) and their reflected variants. Unary `-`/`+`/`~` dispatch to `__neg__`/`__pos__`/`__invert__`. On by default. |
3619
+ | `strict_arithmetic` | When `overload_operators` is active, incompatible operand types (e.g. `int + str`) raise `TypeError` instead of silently coercing as JavaScript would. On by default; disable with `from __python__ import no_strict_arithmetic` to revert to JavaScript coercion behaviour. Internal RapydScript library code is unaffected. |
3620
+ | `truthiness` | Boolean tests and `bool()` dispatch to `__bool__` and treat empty containers as falsy, matching Python semantics. On by default. |
3621
+ | `bound_methods` | Method references (`obj.method`) are automatically bound to their object, so they can be passed as callbacks without losing `self`. On by default. |
3622
+ | `hash_literals` | `{k: v}` creates a Python `dict` (alias for `dict_literals`; kept for backward compatibility). On by default. |
3623
+ | `jsx` | JSX syntax (`<Tag attr={expr}>children</Tag>`) is enabled. On by default. |
2662
3624
 
2663
3625
 
2664
3626
  Monaco Language Service
@@ -2722,9 +3684,11 @@ when the editor is torn down.
2722
3684
  | `compiler` | object | — | **Required.** The `window.RapydScript` compiler bundle. |
2723
3685
  | `parseDelay` | number | `300` | Debounce delay (ms) before re-checking after an edit. |
2724
3686
  | `virtualFiles` | `{name: source}` | `{}` | Virtual modules available to `import` statements. |
3687
+ | `stdlibFiles` | `{name: source}` | `{}` | Like `virtualFiles` but treated as stdlib — always available and never produce bad-import warnings. |
2725
3688
  | `dtsFiles` | `[{name, content}]` | `[]` | TypeScript `.d.ts` files loaded at startup. |
2726
3689
  | `loadDts` | `(name) => Promise<string>` | — | Async callback for lazy-loading `.d.ts` content on demand. |
2727
3690
  | `extraBuiltins` | `{name: true}` | `{}` | Extra global names that suppress undefined-symbol warnings. |
3691
+ | `pythonFlags` | string | — | Comma-separated Python flags to enable globally (e.g. `"dict_literals,overload_getitem"`). See [Python Flags](#python-flags) above. |
2728
3692
 
2729
3693
  ### Runtime API
2730
3694
 
@@ -2744,6 +3708,9 @@ service.loadDts('lib.dom').then(function () { console.log('DOM types loaded'); }
2744
3708
  // Suppress undefined-symbol warnings for additional global names
2745
3709
  service.addGlobals(['myFrameworkGlobal', '$']);
2746
3710
 
3711
+ // Get the most recently built scope map for a Monaco model (null if not yet analysed)
3712
+ var scopeMap = service.getScopeMap(editorModel);
3713
+
2747
3714
  // Tear down all Monaco providers and event listeners
2748
3715
  service.dispose();
2749
3716
  ```
@@ -2867,6 +3834,13 @@ original `.py` file with working breakpoints and correct error stack frames.
2867
3834
  | `keep_docstrings` | bool | `false` | Keep docstrings in the output. |
2868
3835
  | `js_version` | number | `6` | Target ECMAScript version (5 or 6). |
2869
3836
  | `private_scope` | bool | `false` | Wrap the output in an IIFE. |
3837
+ | `python_flags` | string | — | Comma-separated Python flags to enable for this compilation (e.g. `"dict_literals,overload_operators"`). See [Python Flags](#python-flags) above. Flags set here override any inherited from a previous `compile()` call on a streaming compiler. |
3838
+ | `virtual_files` | `{name: source}` | — | Map of module-name → RapydScript source for modules importable via `import`. Only used when the underlying streaming compiler was created with a virtual-file context (as `web_repl()` does). |
3839
+ | `discard_asserts` | bool | `false` | Strip all `assert` statements from the output. |
3840
+ | `omit_function_metadata` | bool | `false` | Omit per-function metadata (e.g. argument names) from the output for smaller bundles. |
3841
+ | `write_name` | bool | `false` | Emit a `var __name__ = "…"` assignment at the top of the output. |
3842
+ | `tree_shake` | bool | `false` | Remove unused imported names from the output (requires stdlib imports). |
3843
+ | `filename` | string | `'<input>'` | Source filename embedded in the source map and used in error messages. |
2870
3844
 
2871
3845
  **How it works**
2872
3846
 
@@ -2897,6 +3871,242 @@ This runs all seven language-service test suites (diagnostics, scope analysis,
2897
3871
  completions, signature help, hover, DTS registry, and built-in stubs).
2898
3872
 
2899
3873
 
3874
+ Python Feature Coverage
3875
+ -----------------------
3876
+
3877
+ ### Fully Supported
3878
+
3879
+ | Feature | Notes |
3880
+ |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---|
3881
+ | `super()` — 0-arg and 2-arg forms | `super().method()` and `super(Cls, self).method()` both work |
3882
+ | `except TypeA, TypeB as e:` | RapydScript comma-separated form; catches multiple exception types |
3883
+ | `except (TypeA, TypeError) as e:` | Tuple form also supported |
3884
+ | `except*` / `ExceptionGroup` (Python 3.11+) | Full support: `ExceptionGroup` class with `subgroup()`/`split()`; `except*` dispatches to typed handlers, re-raises unmatched; bare `except*:` catches all remaining |
3885
+ | `try / else` | `else` block runs only when no exception was raised |
3886
+ | `for / else` | `else` block runs when loop completes without `break`; nested break isolation works |
3887
+ | `while / else` | `else` block runs when loop condition becomes `False` without a `break`; nested `break` isolation correct |
3888
+ | `with A() as a, B() as b:` | Multiple context managers in one statement; exits in LIFO order (Python-correct) |
3889
+ | `callable(fn)` | Works for plain functions and objects with `__call__` |
3890
+ | `round(x, ndigits=0)` | Full Python semantics including negative `ndigits` |
3891
+ | `enumerate(iterable, start=0)` | `start` parameter supported |
3892
+ | `str.isspace()`, `str.islower()`, `str.isupper()` | Working string predicates |
3893
+ | `str.isalpha()` | Regex-based; empty string returns `False` |
3894
+ | `str.isdigit()` | Regex-based (`\d+`) |
3895
+ | `str.isalnum()` | Regex-based |
3896
+ | `str.isidentifier()` | Checks `^[a-zA-Z_][a-zA-Z0-9_]*$` |
3897
+ | `str.casefold()` | Maps to `.toLowerCase()` |
3898
+ | `str.removeprefix(prefix)` | Returns unchanged string if prefix not found |
3899
+ | `str.removesuffix(suffix)` | Returns unchanged string if suffix not found |
3900
+ | `str.expandtabs(tabsize=8)` | Replaces `\t` with spaces to the next tab stop; `\n`/`\r` reset the column counter; `tabsize=0` removes all tabs; available as an instance method on any string (via the default `pythonize_strings` patch) |
3901
+ | `str * n` string repetition | Works (via `overload_operators`, on by default) |
3902
+ | `list * n` / `n * list` | Works (via `overload_operators`); returns a proper RapydScript list |
3903
+ | `list + list` concatenation | `[1,2] + [3,4]` returns `[1, 2, 3, 4]`; `+=` extends in-place. No flag required. |
3904
+ | `match / case` | Structural pattern matching (Python 3.10) fully supported |
3905
+ | Variable type annotations `x: int = 1` | Parsed and ignored (no runtime enforcement); annotated assignments work normally |
3906
+ | Ellipsis literal `...` as expression | Parsed as a valid expression; evaluates to JS `undefined` at runtime |
3907
+ | Generator `.throw()` | Works via JS generator protocol |
3908
+ | Generator `.send()` | Works via `g.next(value)` |
3909
+ | `yield from` | Works; return value of sub-generator is not accessible |
3910
+ | `+=`, `-=`, `*=`, `/=`, `//=`, `**=`, `%=`, `&=`, `\|=`, `^=`, `<<=`, `>>=` | All augmented assignments work |
3911
+ | `raise X from Y` exception chaining | Sets `__cause__` on the thrown exception; `from None` also supported |
3912
+ | Starred assignment `a, *b, c = ...` | Works |
3913
+ | `[*a, 1, *b]` list spread | Works; any iterable; translates to `[...a, 1, ...b]` |
3914
+ | `{*a, 1, *b}` set spread | Works; translates to `ρσ_set([...a, 1, ...b])` |
3915
+ | `**expr` in function calls | Works with any expression (variable, attr access, call, dict literal), not just plain names |
3916
+ | `@classmethod`, `@staticmethod`, `@property` / `@prop.setter` | All work |
3917
+ | `{**dict1, **dict2}` dict spread | Works as merge replacement for the missing `\|` operator |
3918
+ | `dict.fromkeys()` | Works (via `dict_literals`, on by default) |
3919
+ | Chained comparisons `a < b < c` and `a < b > c` | Same-direction and mixed-direction chains both work; middle operand evaluated once; with `overload_operators` each comparison dispatches via `ρσ_op_lt` etc. |
3920
+ | `for`, `while`, `try/except/finally`, `with`, `match/case` | All control-flow constructs work |
3921
+ | Classes, inheritance, decorators, `__dunder__` methods | Fully supported |
3922
+ | Nested class definitions | Accessible as `Outer.Inner` and via instance (`self.Inner`); arbitrary nesting depth; nested class may inherit from outer-scope classes |
3923
+ | List / dict / set comprehensions, generator expressions | Fully supported |
3924
+ | f-strings, `str.format()`, `format()` builtin, all common `str.*` methods | Fully supported |
3925
+ | `abs()`, `divmod()`, `any()`, `all()`, `sum()`, `min()`, `max()` | All work |
3926
+ | `sorted()`, `reversed()`, `zip()`, `map()`, `filter()` | All work |
3927
+ | `zip(strict=True)` | Raises `ValueError` when iterables have different lengths; equal-length iterables work normally |
3928
+ | `set` with full union/intersection/difference API | Fully supported |
3929
+ | `isinstance()`, `hasattr()`, `getattr()`, `setattr()`, `dir()` | All work |
3930
+ | `bin()`, `hex()`, `oct()`, `chr()`, `ord()` | All work |
3931
+ | `int(x, base)`, `float(x)` with ValueError on bad input | Works |
3932
+ | `lambda` keyword | Full support: args, defaults, `*args`, ternary body, closures, nesting |
3933
+ | Arithmetic operator overloading — `__add__`, `__sub__`, `__mul__`, `__truediv__`, `__floordiv__`, `__mod__`, `__pow__`, `__neg__`, `__pos__`, `__abs__`, `__invert__`, `__lshift__`, `__rshift__`, `__and__`, `__or__`, `__xor__`, `__radd__`, `__iadd__` etc. | Dispatched via `overload_operators` (on by default) |
3934
+ | Ordered-comparison operator overloading — `__lt__`, `__gt__`, `__le__`, `__ge__` | `<`, `>`, `<=`, `>=` dispatch to dunder methods (forward then reflected); lists compared lexicographically (like Python); incompatible types raise `TypeError`; chained comparisons (`a < b < c`) fully supported. Active via `overload_operators` (on by default). |
3935
+ | Nested comprehensions (multi-`for` clause) | `[x for row in matrix for x in row if cond]`; works for list, set, and dict comprehensions |
3936
+ | Positional-only parameters `def f(a, b, /):` | Full support — parser enforces placement; runtime passes positional args correctly |
3937
+ | Keyword-only parameters `def f(a, *, b):` | Full support — bare `*` separator enforced; `b` must be passed as keyword |
3938
+ | Walrus operator `:=` | Fully supported: hoisted in `if`/`while` conditions at any scope; comprehension filter assigns to enclosing scope (Python-correct). |
3939
+ | `__call__` dunder dispatch | `obj()` dispatches to `obj.__call__(args)` for callable objects; `callable(obj)` also returns `True`; both forms work. Active via `truthiness` (on by default). |
3940
+ | **Truthiness / `__bool__`** | Full Python truthiness via `truthiness` (on by default): empty `[]`, `{}`, `set()`, `''` are falsy; `__bool__` is dispatched; `and`/`or` return operand values; `not`, `if`, `while`, `assert`, ternary all use `ρσ_bool()`. |
3941
+ | `frozenset(iterable)` | Immutable set: construction from list/set/iterable; `in`, `len()`, iteration, `copy()`, `union()`, `intersection()`, `difference()`, `symmetric_difference()`, `issubset()`, `issuperset()`, `isdisjoint()` — all return `frozenset`. `isinstance(x, frozenset)` works. Compares equal to a `set` with the same elements via `__eq__`. No mutation methods (`add`, `remove`, etc.). |
3942
+ | `issubclass(cls, classinfo)` | Checks prototype chain; `classinfo` may be a class or tuple of classes; every class is a subclass of itself; raises `TypeError` for non-class arguments. |
3943
+ | `hash(obj)` and `__hash__` dunder | Numbers hash by value (int identity, float → int form if whole); strings use djb2; `None` → 0; booleans → 0/1; `def __hash__(self)` in a class is dispatched by `hash()`; class instances without `__hash__` get a stable identity hash; defining `__eq__` without `__hash__` makes the class unhashable (Python semantics — `hash()` raises `TypeError`); `list`, `set`, `dict` raise `TypeError`. |
3944
+ | `__getattr__` / `__setattr__` / `__delattr__` / `__getattribute__` dunders | Full attribute-access interception via JS `Proxy`. Classes defining any of these automatically wrap instances. `__getattr__` is called only for missing attributes; `__getattribute__` overrides all lookups; `__setattr__` intercepts every assignment (including those in `__init__`); `__delattr__` intercepts `del obj.attr`. Use `object.__setattr__(self, name, value)` / `object.__getattribute__(self, name)` / `object.__delattr__(self, name)` (compiled to `ρσ_object_setattr` / `ρσ_object_getattr` / `ρσ_object_delattr`) to bypass the hooks and avoid infinite recursion. Subclasses automatically inherit proxy wrapping. Requires a JS environment that supports `Proxy`; gracefully degrades to plain attribute access in environments without `Proxy`. |
3945
+ | `__class_getitem__` dunder | `Class[item]` dispatches at compile time to `Class.__class_getitem__(item)`. Behaves as an implicit `@classmethod`: `cls` is bound to the calling class. Subclasses inherit `__class_getitem__` and receive the subclass as `cls`. Multi-argument subscripts (`Class[A, B]`) are passed as a JS array. |
3946
+ | `__init_subclass__` hook | Called automatically on the parent class whenever a subclass is created (e.g. `class Child(Base):`). Implicit `@classmethod`: `cls` receives the new subclass. Keyword arguments in the class header (`class Child(Base, tag='x'):`) are forwarded to `__init_subclass__` as keyword arguments. `super().__init_subclass__(**kwargs)` propagates up the hierarchy. No explicit call needed — the compiler emits it after inheritance setup and identity properties are assigned. |
3947
+ | `next(iterator[, default])` | Advances a JS-protocol iterator (`{done, value}`); returns `default` when exhausted if provided, otherwise raises `StopIteration`. Works with `iter()`, `range()`, `enumerate()`, generators, and any object with a `.next()` or `__next__()` method. |
3948
+ | `StopIteration` exception | Defined as a builtin exception class; raised by `next()` when an iterator is exhausted and no default is given. |
3949
+ | `iter(callable, sentinel)` | Two-argument form calls `callable` (no args) repeatedly until the return value equals `sentinel` (strict `===`). Returns a lazy iterator compatible with `for` loops, `next()`, `list()`, and all iterator consumers. Works with plain functions and callable objects (`__call__`). |
3950
+ | `dict \| dict` and `dict \|= dict` (Python 3.9+) | Dict merge via `\|` creates a new merged dict (right-side values win); `\|=` updates in-place. Active via `overload_operators` + `dict_literals` (both on by default). |
3951
+ | `__format__` dunder | `format()`, `str.format()`, and f-strings all dispatch to `__format__`; default `__format__` auto-generated for classes (returns `__str__()` for empty spec, raises `TypeError` for non-empty spec); `!r`/`!s`/`!a` transformers bypass `__format__` correctly |
3952
+ | `slice(start, stop[, step])` | Full Python `slice` class: 1-, 2-, and 3-argument forms; `.start`, `.stop`, `.step` attributes; `.indices(length)` → `(start, stop, step)`; `str()` / `repr()`; `isinstance(s, slice)`; equality `==`; use as subscript `lst[s]` (read, write, `del`) all work. |
3953
+ | `__import__(name[, globals, locals, fromlist, level])` | Runtime lookup in the compiled module registry (`ρσ_modules`). Without `fromlist` (or empty `fromlist`) returns the top-level package, matching Python's semantics. `ImportError` / `ModuleNotFoundError` raised for unknown modules. **Constraint**: the module must have been statically imported elsewhere in the source so it is present in `ρσ_modules`. |
3954
+ | `ImportError`, `ModuleNotFoundError` | Both defined as runtime exception classes; `ModuleNotFoundError` is a subclass of `ImportError` (same as Python 3.6+). |
3955
+ | `bytes(source[, encoding[, errors]])` and `bytearray(source[, encoding[, errors]])` | Full Python semantics: construction from integer (n zero bytes), list/iterable of ints (0–255), string + encoding (`utf-8`, `latin-1`, `ascii`), `Uint8Array`, or another `bytes`/`bytearray`. Key methods: `hex([sep[, bytes_per_sep]])`, `decode(encoding)`, `fromhex(s)` (static), `count`, `find`, `rfind`, `index`, `rindex`, `startswith`, `endswith`, `join`, `split`, `replace`, `strip`, `lstrip`, `rstrip`, `upper`, `lower`, `copy`. `bytearray` adds: `append`, `extend`, `insert`, `pop`, `remove`, `reverse`, `clear`, `__setitem__` (single and slice). Slicing returns a new `bytes`/`bytearray`. `+` concatenates; `*` repeats; `==` compares element-wise; `in` tests integer or subsequence membership; `isinstance(x, bytes)` / `isinstance(x, bytearray)` work; `bytearray` is a subclass of `bytes`. `repr()` returns `b'...'` notation. `Uint8Array` values may be passed anywhere a `bytes`-like object is accepted. |
3956
+ | `object()` | Featureless base-class instance: `object()` returns a unique instance; `isinstance(x, object)` works; `class Foo(object):` explicit base works; `repr()` → `'<object object at 0x…>'`; `hash()` returns a stable identity hash; each call returns a distinct object suitable as a sentinel value. Note: unlike CPython, JS objects are open, so arbitrary attributes can be set on `object()` instances. |
3957
+ | `float.is_integer()` | Returns `True` if the float has no fractional part (i.e. is a whole number), `False` otherwise. `float('inf').is_integer()` and `float('nan').is_integer()` both return `False`, matching Python semantics. Added to `Number.prototype` in the baselib so it works on any numeric literal or variable. |
3958
+ | `int.bit_length()` | Returns the number of bits needed to represent the integer in binary, excluding the sign and leading zeros. `(0).bit_length()` → `0`; `(255).bit_length()` → `8`; `(256).bit_length()` → `9`; sign is ignored (`(-5).bit_length()` → `3`). Added to `Number.prototype` in the baselib. |
3959
+ | Arithmetic type coercion — `TypeError` on incompatible operands | `1 + '1'` raises `TypeError: unsupported operand type(s) for +: 'int' and 'str'`; all arithmetic operators (`+`, `-`, `*`, `/`, `//`, `%`, `**`) enforce compatible types in their `ρσ_op_*` helpers. `bool` is treated as numeric (like Python's `int` subclass). Activated by `overload_operators` (on by default). String `+` string and numeric `+` numeric are allowed; mixed types raise `TypeError` with a Python-style message. |
3960
+ | `complex(real=0, imag=0)` and complex literals `3+4j` | Full complex number type via `ρσ_complex` class. `complex(real, imag)`, `complex(string)` (parses `'3+4j'`), and `j`/`J` imaginary literal suffix (e.g. `4j`, `3.5J`). Attributes: `.real`, `.imag`. Methods: `conjugate()`, `__abs__()`, `__bool__()`, `__repr__()`, `__str__()`. Arithmetic: `+`, `-`, `*`, `/`, `**` via dunder methods (or operator overloading with `overload_operators`). `abs(z)` dispatches `__abs__`. `isinstance(z, complex)` works. String representation matches Python: `(3+4j)`, `4j`, `(3-0j)`. |
3961
+ | `eval(expr[, globals[, locals]])` | String literals are compiled as **RapydScript source** at compile time (the compiler parses and transpiles the string, just like Python's `eval` takes Python source). `eval(expr)` maps to native JS direct `eval` for scope access. `eval(expr, globals)` / `eval(expr, globals, locals)` use `Function` constructor with explicit bindings; `locals` override `globals`. Runtime `ρσ_` helpers referenced in the compiled string are automatically injected into the Function scope. Only string *literals* are transformed at compile time; dynamic strings are passed through unchanged. |
3962
+ | `exec(code[, globals[, locals]])` | String literals are compiled as **RapydScript source** at compile time. Executes the compiled code string; always returns `None`. Without `globals`/`locals` uses native `eval` (scope access). With `globals`/`locals` uses `Function` constructor — mutable objects (lists, dicts) passed in `globals` are accessible by reference, so side-effects are visible after the call. `ρσ_dict` instances (created when `dict_literals` flag is active) are correctly unwrapped via their `jsmap` backing store. |
3963
+ | `vars(obj)` | Returns a Python `dict` snapshot of the object's own instance attributes (own enumerable JS properties, filtering internal `ρσ`-prefixed keys) — equivalent to Python's `obj.__dict__`. Mutating the returned dict does not affect the original object. Returns a proper `ρσ_dict` instance when `dict_literals` is active (default), so `.keys()`, `.values()`, `.items()`, and `[]` access work as expected. |
3964
+ | `vars()` | Zero-argument form is rewritten by the compiler to `vars(this)`, so calling `vars()` inside a method returns a snapshot of the current instance's attributes — equivalent to Python's no-arg `vars()` inside a method. |
3965
+ | `globals()` | Returns a `dict` snapshot of the JS global object's own enumerable keys (`globalThis` / `window` / `global`). Note that module-level RapydScript variables compiled inside an IIFE/module wrapper will not appear here. |
3966
+
3967
+ ---
3968
+
3969
+ ### Python Compatibility Flags (Default-On)
3970
+
3971
+ All flags below are enabled by default. They can be turned off per-file, per-scope, or globally via the CLI.
3972
+
3973
+ #### Opt-out: per-file or per-scope
3974
+
3975
+ Place at the top of a file to affect the whole file, or inside a function to affect only that scope:
3976
+
3977
+ ```python
3978
+ from __python__ import no_truthiness # single flag
3979
+ from __python__ import no_dict_literals, no_overload_operators # multiple flags
3980
+ ```
3981
+
3982
+ To re-enable a flag in a nested scope after an outer scope turned it off:
3983
+
3984
+ ```python
3985
+ from __python__ import truthiness
3986
+ ```
3987
+
3988
+ #### Opt-out: CLI (all files)
3989
+
3990
+ ```sh
3991
+ rapydscript compile --python-flags=no_dict_literals,no_truthiness input.pyj
3992
+ ```
3993
+
3994
+ Flags in `--python-flags` are comma-separated. Prefix a flag with `no_` to disable it; omit the prefix to force-enable it (useful when combining with `--legacy-rapydscript`).
3995
+
3996
+ #### Disable all flags (legacy mode)
3997
+
3998
+ ```sh
3999
+ rapydscript compile --legacy-rapydscript input.pyj
4000
+ ```
4001
+
4002
+ This restores the original RapydScript behavior: plain JS objects for `{}`, no operator overloading, JS truthiness, unbound methods, and no `String.prototype` patching.
4003
+
4004
+ #### Flag reference
4005
+
4006
+ | Flag | What it enables | Effect when disabled |
4007
+ |---|---|---|
4008
+ | `dict_literals` | `{}` creates a Python `ρσ_dict` with `.keys()`, `.values()`, `.items()`, `.get()`, `.pop()`, `.update()`, `fromkeys()`, and `KeyError` on missing key access. | `{}` becomes a plain JS object; no Python dict methods; missing key access returns `undefined`. |
4009
+ | `overload_getitem` | `obj[key]` dispatches to `obj.__getitem__(key)` when defined; `obj[a:b:c]` passes a `slice` object; dict `[]` access raises `KeyError` on missing key. | `[]` compiles to plain JS property access; no `__getitem__` dispatch; no slice dispatch. |
4010
+ | `bound_methods` | Class methods are automatically bound to `self`, so they retain their `self` binding when stored in a variable or passed as a callback. | Detached method references lose `self` (JS default behavior). |
4011
+ | `hash_literals` | When `dict_literals` is off, `{}` creates `Object.create(null)` rather than `{}`, preventing prototype-chain pollution from keys like `toString`. Has no visible effect while `dict_literals` is on. | `{}` becomes a plain `{}` (inherits from `Object.prototype`). Only relevant when `dict_literals` is also disabled. |
4012
+ | `overload_operators` | Arithmetic and bitwise operators (`+`, `-`, `*`, `/`, `//`, `%`, `**`, `&`, `\|`, `^`, `~`, `<<`, `>>`) and ordered-comparison operators (`<`, `>`, `<=`, `>=`) dispatch to dunder methods (`__add__`, `__lt__`, etc.) when defined on the left operand. Also enables `str * n` string repetition, `list * n` / `n * list` list repetition, `dict \| dict` / `dict \|= dict` merge, and Python-style lexicographic list comparison. | All operators compile directly to JS; no dunder dispatch. `str * n` produces `NaN`; list repetition, dict merge, and Python-style list ordering are unavailable. |
4013
+ | `truthiness` | Python truthiness semantics: `[]`, `{}`, `set()`, `''`, `0`, `None` are falsy; objects with `__bool__` are dispatched; `and`/`or` return the deciding operand value (not `True`/`False`); `not`, `if`, `while`, `assert`, and ternary all route through `ρσ_bool()`. Also enables `__call__` dispatch: `obj(args)` invokes `obj.__call__(args)` for callable objects. | Truthiness is JS-native (all objects truthy); `__bool__` is never called; `and`/`or` return booleans; `__call__` is not dispatched. |
4014
+ | `jsx` | JSX syntax (`<Tag attr={expr}>children</Tag>` and `<>...</>` fragments) is recognised as expression syntax and compiled to `React.createElement` calls (or equivalent). | `<` is always a less-than operator; angle-bracket tokens are never parsed as JSX. |
4015
+ | `pythonize_strings` *(output-level option, not a `from __python__` flag)* | `String.prototype` is patched at startup with Python string methods (`strip`, `lstrip`, `rstrip`, `join`, `format`, `capitalize`, `lower`, `upper`, `find`, `rfind`, `index`, `rindex`, `count`, `startswith`, `endswith`, `center`, `ljust`, `rjust`, `zfill`, `partition`, `rpartition`, `splitlines`, `expandtabs`, `swapcase`, `title`, `isspace`, `islower`, `isupper`). Equivalent to calling `from pythonize import strings; strings()` manually. Note: `split()` and `replace()` are intentionally kept as their JS versions. | Python string methods are not available on string instances; call `str.strip(s)` etc., or import and call `strings()` from `pythonize` manually. Disable globally with `--legacy-rapydscript`. |
4016
+
4017
+ ---
4018
+
4019
+ ### Not Supported
4020
+
4021
+ | Feature | Notes |
4022
+ |---------------------------------------|-----------------------------------------------------------------------------------------|
4023
+ | `__slots__` enforcement | Accepted, but does not restrict attribute assignment |
4024
+ | `locals()` | Returns an empty `dict`. JS has no runtime mechanism for introspecting local variables. |
4025
+ | `input(prompt)` | There is no simple cli input in browser; use `prompt()` |
4026
+ | `compile()` | Python compile/code objects have no JS equivalent |
4027
+ | `memoryview(obj)` | There is no buffer protocol in browser context |
4028
+ | `open(path)` | There is no filesystem access in browser context |
4029
+ | `from module import *` (star imports) | Intentionally unsupported (by design, to prevent namespace pollution) |
4030
+ | `__del__` destructor / finalizer | JS has no guaranteed finalizer |
4031
+
4032
+ ---
4033
+
4034
+ ### Standard Library Modules
4035
+
4036
+ Modules with a `src/lib/` implementation available are marked ✅. All others are absent.
4037
+
4038
+ | Module | Status | Notes |
4039
+ |---------------|-------------|-----------------------------------------------------------------------------------------------|
4040
+ | `math` | ✅ | Full implementation in `src/lib/math.pyj` |
4041
+ | `random` | ✅ | RC4-seeded PRNG in `src/lib/random.pyj` |
4042
+ | `re` | ✅ | Regex wrapper in `src/lib/re.pyj`; uses the JS engine — full PCRE-level support on modern runtimes: positive/negative lookbehind (ES2018+, including variable-width), unicode via automatic `u` flag (ES2015+), `re.fullmatch()`, `re.S`/`re.NOFLAG` aliases. `MatchObject.start()`/`.end()` return exact positions on runtimes with the ES2022 `d` flag (Node 18+); heuristic fallback on older runtimes. Conditional groups `(?(id)yes\|no)` are not supported (JS limitation) and raise `re.error`. |
4043
+ | `encodings` | ✅ | Base64 and encoding helpers; partial `base64` coverage |
4044
+ | `collections` | ✅ | `defaultdict`, `Counter`, `OrderedDict`, `deque` |
4045
+ | `functools` | ✅ | `reduce`, `partial`, `wraps`, `lru_cache` |
4046
+ | `itertools` | ✅ | Common iteration tools |
4047
+ | `numpy` | ✅ | Full numpy-like library in `src/lib/numpy.pyj`; `numpy.random` and `numpy.linalg` sub-modules |
4048
+ | `copy` | ✅ | `copy()` shallow copy and `deepcopy()` (circular-ref-safe via memo Map); `__copy__` / `__deepcopy__(memo)` hooks honoured; handles list, set, frozenset, dict, class instances, and plain JS objects |
4049
+ | `typing` | ✅ | `TYPE_CHECKING`, `Any`, `Union`, `Optional`, `ClassVar`, `Final`, `Literal`, `NoReturn`, `List`, `Dict`, `Set`, `FrozenSet`, `Tuple`, `Type`, `Callable`, `Iterator`, `Iterable`, `Generator`, `Sequence`, `MutableSequence`, `Mapping`, `MutableMapping`, `Awaitable`, `Coroutine`, `AsyncGenerator`, `AsyncIterator`, `AsyncIterable`, `IO`, `TextIO`, `BinaryIO`, `Pattern`, `Match`, `TypeVar`, `Generic`, `Protocol`, `cast`, `overload`, `no_type_check`, `no_type_check_decorator`, `runtime_checkable`, `get_type_hints`, `TypedDict`, `NamedTuple`, `AnyStr`, `Text` — all available in `src/lib/typing.pyj` |
4050
+ | `dataclasses` | ✅ | `@dataclass`, `field()`, `asdict()`, `astuple()`, `replace()`, `fields()`, `is_dataclass()`, `MISSING` in `src/lib/dataclasses.pyj`; `frozen=True`, `order=True`, inheritance supported; note: `field()` first positional arg is the default value (JS reserved word `default` cannot be used as a kwarg) |
4051
+ | `enum` | ✅ | `Enum` base class in `src/lib/enum.pyj`; `.name`, `.value`, iteration, `isinstance` checks; `IntEnum`/`Flag` not available |
4052
+ | `abc` | ✅ | `ABC`, `@abstractmethod`, `Protocol`, `@runtime_checkable`, `ABCMeta` (informational), `get_cache_token()` in `src/lib/abc.pyj`; abstract method enforcement via `__init__` guard; `ABC.register()` for virtual subclasses with isinstance support; `Symbol.hasInstance` enables structural isinstance for `@runtime_checkable` protocols; `ABCMeta` metaclass not usable (no metaclass support), use `ABC` base class instead |
4053
+ | `contextlib` | ❌ | `contextmanager`, `suppress`, `ExitStack`, `asynccontextmanager` not available |
4054
+ | `string` | ❌ | Character constants, `Template`, `Formatter` not available |
4055
+ | `json` | ❌ | No Python wrapper; JS `JSON.parse` / `JSON.stringify` work directly via verbatim JS |
4056
+ | `datetime` | ❌ | `date`, `time`, `datetime`, `timedelta` not available |
4057
+ | `inspect` | ❌ | `signature`, `getmembers`, `isfunction` etc. not available |
4058
+ | `asyncio` | ❌ | Event loop, `gather`, `sleep`, `Queue`, `Task` wrappers not available; use `async`/`await` |
4059
+ | `io` | ❌ | `StringIO`, `BytesIO` not available |
4060
+ | `struct` | ❌ | Binary packing/unpacking not available |
4061
+ | `hashlib` | ❌ | MD5, SHA-256 etc. not available; use Web Crypto API via verbatim JS |
4062
+ | `hmac` | ❌ | Keyed hashing not available |
4063
+ | `base64` | ❌ (partial) | Partial coverage via `encodings` module; no full `base64` module |
4064
+ | `urllib` | ❌ | URL parsing/encoding (`urllib.parse`) not available; use JS `URL` API |
4065
+ | `html` | ❌ | `escape`, `unescape` not available; use JS DOM APIs |
4066
+ | `csv` | ❌ | CSV parsing not available |
4067
+ | `textwrap` | ❌ | `wrap`, `fill`, `dedent`, `indent` not available |
4068
+ | `pprint` | ❌ | Pretty-printing not available |
4069
+ | `logging` | ❌ | Logging framework not available; use `console.*` directly |
4070
+ | `unittest` | ❌ | Not available; RapydScript uses a custom test runner (`node bin/rapydscript test`) |
4071
+
4072
+ ---
4073
+
4074
+ ### Semantic Differences
4075
+
4076
+ Features that exist in RapydScript but behave differently from standard Python:
4077
+
4078
+ | Feature | Python Behavior | RapydScript Behavior |
4079
+ |---|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
4080
+ | `is` / `is not` | Object identity | Strict equality `===` / `!==`, which is not object/pointer comparison for JS primitives |
4081
+ | `//` floor division on floats | `math.floor(a/b)` always | uses `Math.floor` - same result for well-behaved floats |
4082
+ | `%` on negative numbers | Python modulo (always non-negative) | JS remainder (can be negative) |
4083
+ | `global` / `nonlocal` scoping | Full cross-scope declaration | `global` works for module-level; if a variable exists in both an intermediate outer scope **and** the module-level scope, the outer scope takes precedence (differs from Python where `global` always forces module-level) |
4084
+ | `Exception.message` | Not standard; use `.args[0]` | `.message` is the standard attribute (JS `Error` style) |
4085
+ | Function call argument count | Too few args → `TypeError`; too many → `TypeError` | Too few args → extra params are `undefined`; too many → extras silently discarded. No `TypeError` is raised in either case. |
4086
+ | Positional-only param enforcement | Passing by keyword raises `TypeError` | Passing by keyword is silently ignored — the named arg is discarded and the parameter gets `undefined` (no error raised) |
4087
+ | Keyword-only param enforcement | Passing positionally raises `TypeError` | Passing positionally raises no error — the extra positional arg is silently discarded and the default value is used |
4088
+ | Default `{}` dict — numeric keys | Integer keys are stored as integers | Numeric keys are auto-coerced to strings by the JS engine: `d[1]` and `d['1']` refer to the same slot |
4089
+ | Default `{}` dict — attribute access | `d.foo` raises `AttributeError` | `d.foo` and `d['foo']` access the same slot; keys are also properties |
4090
+ | String encoding | Unicode strings (full code-point aware) | UTF-16 — non-BMP characters (e.g. emoji) are stored as surrogate pairs. Use `str.uchrs()`, `str.uslice()`, `str.ulen()` for code-point-aware operations. |
4091
+ | Multiple inheritance MRO | C3 linearization (MRO) always deterministic | Built on JS prototype chain; may differ from Python's C3 MRO in complex or diamond-inheritance hierarchies |
4092
+ | Generators — output format | Native Python generator objects | Down-compiled to ES5 state-machine switch statements by default; pass `--js-version 6` for native ES6 generators (smaller and faster) |
4093
+ | `dict` key ordering | Insertion order guaranteed (3.7+) | Depends on JS engine (V8 preserves insertion order in practice) |
4094
+ | Reserved keywords | Python keywords only | All JavaScript reserved words (`default`, `switch`, `delete`, `void`, `typeof`, etc.) are also reserved in RapydScript, since it compiles to JS |
4095
+ | `parenthesized with (A() as a, B() as b):` | Multiple context managers in parenthesized form (3.10+) | Not meaningful in a browser/event-driven context; multi-context `with` without parens works |
4096
+
4097
+ ---
4098
+
4099
+ ### Test File
4100
+
4101
+ `test/python_features.pyj` contains runnable assertions for all features surveyed.
4102
+ Features that are not supported have their test code commented out with a `# SKIP:` label
4103
+ and an explanation. Run with:
4104
+
4105
+ ```sh
4106
+ node bin/rapydscript test python_features
4107
+ ```
4108
+
4109
+
2900
4110
  Reasons for the fork
2901
4111
  ----------------------
2902
4112
 
@@ -2910,4 +4120,4 @@ ever resumes, they are welcome to use the code from this fork. All the
2910
4120
  new code is under the same license, to make that possible.
2911
4121
 
2912
4122
  See the [Changelog](https://github.com/ficocelliguy/rapydscript-ns/blob/master/CHANGELOG.md)
2913
- for a list of changes to rapydscript-ns, including this fork at version 8.0
4123
+ for a list of changes to rapydscript-ns, including this fork at version 8.0