rapydscript-ns 0.8.2 → 0.8.3

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 (50) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/PYTHON_DIFFERENCES_REPORT.md +291 -0
  3. package/PYTHON_FEATURE_COVERAGE.md +96 -5
  4. package/README.md +161 -46
  5. package/TODO.md +2 -283
  6. package/language-service/index.js +4474 -0
  7. package/language-service/language-service.d.ts +40 -0
  8. package/package.json +9 -7
  9. package/src/baselib-builtins.pyj +77 -1
  10. package/src/baselib-containers.pyj +8 -4
  11. package/src/baselib-internal.pyj +30 -1
  12. package/src/baselib-str.pyj +8 -1
  13. package/src/lib/collections.pyj +1 -1
  14. package/src/lib/numpy.pyj +10 -10
  15. package/src/monaco-language-service/analyzer.js +131 -9
  16. package/src/monaco-language-service/builtins.js +12 -2
  17. package/src/monaco-language-service/completions.js +170 -1
  18. package/src/monaco-language-service/diagnostics.js +1 -1
  19. package/src/monaco-language-service/index.js +17 -0
  20. package/src/monaco-language-service/scope.js +3 -0
  21. package/src/output/classes.pyj +20 -3
  22. package/src/output/codegen.pyj +1 -1
  23. package/src/output/functions.pyj +4 -16
  24. package/src/output/loops.pyj +0 -9
  25. package/src/output/modules.pyj +1 -4
  26. package/src/output/operators.pyj +14 -0
  27. package/src/output/stream.pyj +0 -13
  28. package/src/parse.pyj +17 -1
  29. package/test/baselib.pyj +4 -4
  30. package/test/classes.pyj +56 -17
  31. package/test/collections.pyj +5 -5
  32. package/test/python_compat.pyj +326 -0
  33. package/test/python_features.pyj +110 -23
  34. package/test/slice.pyj +105 -0
  35. package/test/str.pyj +25 -0
  36. package/test/unit/fixtures/fibonacci_expected.js +1 -1
  37. package/test/unit/index.js +119 -7
  38. package/test/unit/language-service-builtins.js +70 -0
  39. package/test/unit/language-service-bundle.js +5 -5
  40. package/test/unit/language-service-completions.js +180 -0
  41. package/test/unit/language-service-index.js +350 -0
  42. package/test/unit/language-service-scope.js +255 -0
  43. package/test/unit/language-service.js +35 -0
  44. package/test/unit/run-language-service.js +1 -0
  45. package/test/unit/web-repl.js +134 -0
  46. package/tools/build-language-service.js +2 -2
  47. package/tools/compiler.js +0 -24
  48. package/tools/export.js +3 -37
  49. package/web-repl/rapydscript.js +6 -40
  50. package/web-repl/language-service.js +0 -4187
@@ -0,0 +1,326 @@
1
+ # globals: assrt
2
+ # vim:fileencoding=utf-8
3
+ #
4
+ # python_compat.pyj
5
+ # Tests for places where RapydScript intentionally differs from Python.
6
+ # Each section documents the Python behavior, the RapydScript behavior, and
7
+ # asserts that RapydScript does what the README says it does.
8
+
9
+ ae = assrt.equal
10
+ ade = assrt.deepEqual
11
+ ok = assrt.ok
12
+
13
+ # ── 1. Function argument leniency — too few arguments ────────────────────────
14
+ # Python: raises TypeError("f() missing 1 required positional argument: 'b'")
15
+ # RapydScript: extra parameters are undefined instead of raising TypeError.
16
+
17
+ def _f_two_args(a, b):
18
+ return b
19
+
20
+ ae(_f_two_args(1), undefined) # b is undefined
21
+ ae(_f_two_args(1, 2), 2) # normal call still works
22
+
23
+ def _f_default(a, b=99):
24
+ return b
25
+
26
+ ae(_f_default(1), 99) # default value used when b not passed
27
+ ae(_f_default(1, 7), 7)
28
+
29
+ # ── 2. Function argument leniency — too many arguments ───────────────────────
30
+ # Python: raises TypeError("f() takes 1 positional argument but 3 were given")
31
+ # RapydScript: extra positional arguments are silently discarded.
32
+
33
+ def _f_one_arg(a):
34
+ return a
35
+
36
+ ae(_f_one_arg(42, 99, 'extra'), 42) # extras silently ignored
37
+
38
+ # ── 3. Positional-only parameter enforcement — lenient ───────────────────────
39
+ # Python: greet(name="Alice") raises TypeError when name is positional-only.
40
+ # RapydScript: the named keyword arg is silently ignored; the parameter gets
41
+ # its positional value or undefined if not provided positionally.
42
+
43
+ def _po_only(a, b, /):
44
+ return [a, b]
45
+
46
+ ade(_po_only(1, 2), [1, 2]) # normal positional call works
47
+
48
+ # Passing 'b' by keyword: keyword is silently ignored, b gets undefined.
49
+ result3 = _po_only(1, b=2)
50
+ ae(result3[0], 1)
51
+ ae(result3[1], undefined) # named value was silently ignored
52
+
53
+ # ── 4. Keyword-only parameter enforcement — lenient ──────────────────────────
54
+ # Python: f(1, 2) raises TypeError when b is keyword-only (after bare *).
55
+ # RapydScript: positional value is accepted without error.
56
+
57
+ def _kw_only(a, *, b=10):
58
+ return [a, b]
59
+
60
+ ade(_kw_only(1, b=99), [1, 99]) # normal keyword call works
61
+ ade(_kw_only(1), [1, 10]) # default value used
62
+
63
+ # Passing keyword-only param positionally: no TypeError in RapydScript.
64
+ # The extra positional arg is silently discarded; b gets its default value.
65
+ # (Python would raise TypeError here.)
66
+ _kw_result = None
67
+ _kw_error = None
68
+ try:
69
+ _kw_result = _kw_only(1, 99)
70
+ except TypeError as e:
71
+ _kw_error = e
72
+ ok(_kw_error is None) # no TypeError raised
73
+ ae(_kw_result[0], 1)
74
+ ae(_kw_result[1], 10) # default used; positional 99 was discarded
75
+
76
+ # ── 5. `is` operator — maps to JavaScript === ────────────────────────────────
77
+ # Python: `is` checks object identity (id(a) == id(b)).
78
+ # RapydScript: `is` compiles to ===, which is strict value equality for primitives.
79
+ # Most cases behave identically, but NaN is a notable divergence.
80
+
81
+ ae(1 is 1, True)
82
+ ae('hello' is 'hello', True)
83
+ ae(None is None, True)
84
+
85
+ # NaN: RapydScript special-cases `x is NaN` → compiles to isNaN(x).
86
+ # This is a deliberate deviation from `===` to make NaN checks useful.
87
+ # (Plain `NaN === NaN` is always false in JS, so `is NaN` would be useless.)
88
+ ok(42 / 'x' is NaN) # compiles to: isNaN(42 / 'x') → true
89
+ ok(42 is not NaN) # compiles to: !isNaN(42) → true
90
+
91
+ # ── 6. Arithmetic coercion: number + string ───────────────────────────────────
92
+ # Python: 1 + '1' raises TypeError.
93
+ # RapydScript: delegates to JavaScript which coerces 1 to '1', giving '11'.
94
+
95
+ ae(1 + '1', '11')
96
+ ae('prefix_' + 42, 'prefix_42')
97
+
98
+ # ── 7. List + list concatenation ─────────────────────────────────────────────
99
+ # Python: [1] + [2] returns [1, 2].
100
+ # RapydScript: list + list returns a new concatenated list (Python semantics).
101
+ # The + operator compiles to ρσ_list_add which uses Array.concat when both sides are arrays.
102
+
103
+ ade([1] + [2], [1, 2])
104
+ ade([1, 2] + [3, 4], [1, 2, 3, 4])
105
+
106
+ # Variables holding lists also work:
107
+ _a7 = [1, 2]
108
+ _b7 = [3, 4]
109
+ ade(_a7 + _b7, [1, 2, 3, 4])
110
+
111
+ # += extends the list in-place (Python semantics — same object is mutated):
112
+ _a8 = [1, 2]
113
+ _b8 = _a8
114
+ _a8 += [3, 4]
115
+ ade(_a8, [1, 2, 3, 4])
116
+ ade(_b8, [1, 2, 3, 4]) # _b8 still points to same object, now extended
117
+
118
+ # ── 8. Ordering operators on lists — JS string coercion ──────────────────────
119
+ # Python: [10] < [9] compares element-wise → False (10 > 9).
120
+ # RapydScript: < coerces to string → '10' < '9' is True (char comparison).
121
+ # This is a common gotcha: single-element lists with multi-digit numbers.
122
+
123
+ # RapydScript lists have a Python-style __str__, so [10].toString() = '[10]'.
124
+ # String compare of '[10]' vs '[9]': '[' = '[', then '1' < '9' → [10] < [9] is True.
125
+ ok([10] < [9]) # True in RapydScript (string: '[10]' < '[9]')
126
+ ae([9] < [10], False) # False (string: '[9]' > '[10]')
127
+
128
+ # Compare: Python uses numeric element-wise ordering, so [10] > [9] (10 > 9).
129
+ # RapydScript gives the opposite result for these specific values.
130
+
131
+ # For correct numeric comparisons, compare elements explicitly:
132
+ ok(10 > 9) # direct numeric comparison always works
133
+
134
+ # ── 9. List sort() — Python numeric sort (jssort() for JS lexicographic) ──────
135
+ # list.sort() now does Python-style numeric ordering by default (in-place).
136
+ # list.jssort() gives the native JS sort (lexicographic, for JS interop).
137
+
138
+ lst_py = [10, 9, 2]
139
+ lst_py.sort() # Python-style numeric sort
140
+ ade(lst_py, [2, 9, 10])
141
+
142
+ # sort with key and reverse also work:
143
+ lst_rev = [3, 1, 2]
144
+ lst_rev.sort(reverse=True)
145
+ ade(lst_rev, [3, 2, 1])
146
+
147
+ # jssort() is the native JS lexicographic sort (for JS interop):
148
+ lst_js = [10, 9, 2]
149
+ lst_js.jssort() # JS sort: compares as strings '10', '9', '2'
150
+ ade(lst_js, [10, 2, 9]) # lexicographic: '1' < '2' < '9'
151
+
152
+ # ── 10. List pop() — Python bounds-checked pop (jspop() for JS native) ────────
153
+ # list.pop(i) now behaves like Python: raises IndexError for out-of-bounds index.
154
+ # list.jspop() is the native JS pop — no bounds check, always pops the last element.
155
+
156
+ lst10 = [1, 2, 3]
157
+ ae(lst10.pop(), 3) # Python pop: removes and returns last element
158
+ ade(lst10, [1, 2])
159
+
160
+ lst10b = [1, 2, 3]
161
+ ae(lst10b.pop(0), 1) # Python pop with index: removes and returns element at 0
162
+ ade(lst10b, [2, 3])
163
+
164
+ # pop raises IndexError for out-of-bounds:
165
+ _caught10 = False
166
+ try:
167
+ [1].pop(99)
168
+ except IndexError:
169
+ _caught10 = True
170
+ ok(_caught10)
171
+
172
+ # jspop() is the native JS pop — ignores arguments, always pops the last element:
173
+ lst10c = [1, 2, 3]
174
+ ae(lst10c.jspop(), 3) # JS pop: always removes and returns the last element
175
+ ade(lst10c, [1, 2])
176
+
177
+ # ── 11. Default dict (JS object) — missing key returns undefined ──────────────
178
+ # Python: d['missing'] raises KeyError.
179
+ # RapydScript (no dict_literals): missing key returns undefined.
180
+
181
+ d11 = {'a': 1, 'b': 2}
182
+ ae(d11['a'], 1)
183
+ ae(d11['missing'], undefined) # no KeyError
184
+
185
+ # With dict_literals, Python behavior is enabled:
186
+ def _test_keyerror():
187
+ from __python__ import dict_literals, overload_getitem
188
+ d = {'a': 1}
189
+ caught = False
190
+ try:
191
+ x = d['missing']
192
+ except KeyError:
193
+ caught = True
194
+ ok(caught)
195
+ _test_keyerror()
196
+
197
+ # ── 12. Default dict — numeric keys auto-converted to strings ─────────────────
198
+ # Python: {1: 'one'} keeps integer key 1.
199
+ # RapydScript JS object: numeric keys become strings. d[1] and d['1'] are the same.
200
+
201
+ d12 = {}
202
+ d12[1] = 'one'
203
+ ae(d12[1], 'one')
204
+ ae(d12['1'], 'one') # same slot — number keys coerced to string
205
+
206
+ # ── 13. Default dict — keys accessible as attributes ─────────────────────────
207
+ # Python dicts: d.foo is always an AttributeError (use d['foo'] or d.get('foo')).
208
+ # RapydScript JS object: d.foo and d['foo'] access the same slot.
209
+
210
+ d13 = {'hello': 42, 'world': 99}
211
+ ae(d13.hello, 42)
212
+ ae(d13['hello'], 42)
213
+ d13.newkey = 'set via attr'
214
+ ae(d13['newkey'], 'set via attr')
215
+
216
+ # ── 14. String methods not on string instances by default ─────────────────────
217
+ # Python: 'hello'.strip() works directly on the string.
218
+ # RapydScript (default): Python string methods live on the `str` object,
219
+ # not on string instances. Native JS methods (upper, toLowerCase, etc.) do work.
220
+
221
+ s14 = ' hello '
222
+ ae(str.strip(s14), 'hello') # Python method via str object
223
+ ae(s14.trim(), 'hello') # JS native trim() works on instances
224
+
225
+ s14b = 'hello world'
226
+ ae(str.upper(s14b), 'HELLO WORLD') # Python via str
227
+ ae(s14b.toUpperCase(), 'HELLO WORLD') # JS native
228
+
229
+ # After strings(), Python methods work on instances:
230
+ from pythonize import strings
231
+ strings()
232
+
233
+ ae(' hello '.strip(), 'hello')
234
+ ae('hello world'.upper(), 'HELLO WORLD')
235
+
236
+ # ── 15. strings() leaves split() and replace() as JS versions ─────────────────
237
+ # (strings() is active from section 14 above)
238
+ # Python str.split() with no args: splits on any whitespace, strips leading/trailing.
239
+ # JS String.prototype.split() with no args: returns the whole string in a 1-element array.
240
+
241
+ # Python semantics via str module:
242
+ ade(str.split('a b'), ['a', 'b']) # Python split on any whitespace
243
+ ade(str.split('a b c'), ['a', 'b', 'c'])
244
+
245
+ # After strings(), .split() is STILL the JS version (by design):
246
+ ade('a b'.split(), ['a b']) # JS: no-arg split = one-element array
247
+ ade('a b'.split(' '), ['a', 'b']) # JS split with separator still works
248
+
249
+ # Python str.replace() vs JS String.prototype.replace():
250
+ # Python replaces ALL occurrences by default.
251
+ # JS .replace() with a string pattern only replaces the FIRST occurrence.
252
+ ae(str.replace('aaa', 'a', 'b'), 'bbb') # Python: replaces all
253
+ ae('aaa'.replace('a', 'b'), 'baa') # JS: replaces only first
254
+
255
+ # ── 16. Method binding — not automatic ──────────────────────────────────────
256
+ # Python: bound methods carry a reference to self.
257
+ # RapydScript (default): methods are unbound; extracting a method loses self.
258
+
259
+ class _Bindable:
260
+ def __init__(self, value):
261
+ self.value = value
262
+ def get_value(self):
263
+ return self.value if self else None # guard: self may be falsy if unbound
264
+
265
+ obj16 = _Bindable(55)
266
+ ae(obj16.get_value(), 55) # direct call works
267
+
268
+ unbound = obj16.get_value
269
+ ae(unbound(), None) # self is not obj16; guard returns None
270
+
271
+ # With bound_methods flag, extraction works correctly:
272
+ class _AutoBound:
273
+ from __python__ import bound_methods
274
+ def __init__(self, value):
275
+ self.value = value
276
+ def get_value(self):
277
+ return self.value
278
+
279
+ obj16b = _AutoBound(77)
280
+ bound_fn = obj16b.get_value
281
+ ae(bound_fn(), 77) # self is correctly bound to obj16b
282
+
283
+ # ── 17. `global` keyword — prefers outer scope over module global ─────────────
284
+ # Python: `global x` inside a nested function always refers to the module-level x.
285
+ # RapydScript: if x also exists in an intermediate outer scope, that outer scope
286
+ # variable is used instead of the module-level one.
287
+ #
288
+ # NOTE: This is a documented incompatibility. The README says:
289
+ # "if a variable is present both in the outer scope and the global scope,
290
+ # the variable from the outer scope will be used"
291
+ #
292
+ # SKIP: This behavior is inherently tied to how JavaScript closures work and is
293
+ # difficult to exercise meaningfully in a compiled-to-JS test environment,
294
+ # because the "global" and "outer" scopes collapse to different things
295
+ # depending on context. The behavior is documented but not testable with a
296
+ # simple assertion here without knowing the exact generated JS scope.
297
+ #
298
+ # Example (would be run in a context where x is BOTH a global and a local):
299
+ # _x17 = 'global_value'
300
+ # def _outer17():
301
+ # _x17 = 'outer_value'
302
+ # def _inner17():
303
+ # global _x17
304
+ # return _x17
305
+ # return _inner17()
306
+ # # Python: _outer17() == 'global_value' (global keyword forces module level)
307
+ # # RapydScript: _outer17() == 'outer_value' (outer scope wins)
308
+
309
+ # ── 18. Truthiness — JS defaults ─────────────────────────────────────────────
310
+ # Tested more fully in python_features.pyj #40, but demonstrating JS defaults here.
311
+ # Without `from __python__ import truthiness`:
312
+
313
+ _empty_list = []
314
+ _empty_dict = {}
315
+
316
+ # In JS, empty arrays and objects are truthy:
317
+ ok(_empty_list) # True (JS truthiness: any object is truthy)
318
+ ok(_empty_dict) # True (JS truthiness: any object is truthy)
319
+
320
+ # Enable Python truthiness:
321
+ from __python__ import truthiness
322
+
323
+ ok(not []) # False (Python semantics)
324
+ ok(not {}) # False (Python semantics)
325
+ ok([1]) # True
326
+ ok({'a': 1}) # True
@@ -228,6 +228,9 @@ ae(str.isspace('a '), False)
228
228
  ae(str.casefold('Hello World'), 'hello world')
229
229
  ae(str.casefold('ABC'), 'abc')
230
230
  ae(str.casefold('already'), 'already')
231
+ # Unicode: Latin capital letters with diacritics fold to lowercase equivalents
232
+ ae(str.casefold('ÄÖÜ'), 'äöü')
233
+ ae(str.casefold('ÄÖÜ'), str.casefold('äöü'))
231
234
 
232
235
  # ── 12. str.removeprefix() / str.removesuffix() ──────────────────────────────
233
236
  # STATUS: ✓ WORKS (added in this release)
@@ -496,7 +499,7 @@ _test_raise_from_none()
496
499
  # STATUS: ✗ NOT ENFORCED — __slots__ is accepted but does not restrict attrs.
497
500
 
498
501
  # ── 24. Nested classes ───────────────────────────────────────────────────────
499
- # STATUS: NOT SUPPORTED see comment in test/classes.pyj
502
+ # STATUS: WORKSadded in commit 44c9802; tested fully in test/classes.pyj
500
503
 
501
504
  # ── 25. Generator .throw() ───────────────────────────────────────────────────
502
505
  # STATUS: ✓ WORKS
@@ -937,13 +940,8 @@ ae(_c(), 3)
937
940
  # STATUS: ✗ NOT SUPPORTED — no attribute-lookup override.
938
941
 
939
942
  # ── 50. __format__ dunder ────────────────────────────────────────────────────
940
- # STATUS: NOT SUPPORTED — format(obj, spec) is not a defined builtin;
941
- # __format__ is not dispatched.
942
- # SKIP:
943
- # class _Fmt:
944
- # def __format__(self, spec):
945
- # return '<' + spec + '>'
946
- # ae(format(_Fmt(), 'test'), '<test>')
943
+ # STATUS: WORKS — format(obj, spec) dispatches to obj.__format__(spec).
944
+ # Tested fully in test/str.pyj.
947
945
 
948
946
  # ── 51. __class_getitem__ ────────────────────────────────────────────────────
949
947
  # STATUS: ✗ NOT SUPPORTED — no generic subscript MyClass[T] syntax.
@@ -1087,9 +1085,9 @@ _check_unhashable({1, 2})
1087
1085
  _check_unhashable({'a': 1})
1088
1086
 
1089
1087
  # ── 55. format(value, spec) builtin ──────────────────────────────────────────
1090
- # STATUS: NOT IMPLEMENTED — format(value, spec) builtin is not defined.
1091
- # str.format() and f-strings work fine.
1092
- # SKIP:
1088
+ # STATUS: WORKS — format(value, spec) is defined and dispatches to __format__.
1089
+ # Tested fully in test/str.pyj.
1090
+ #
1093
1091
  # ae(format(3.14159, '.2f'), '3.14')
1094
1092
 
1095
1093
  # ── 56. next(iterator[, default]) builtin ────────────────────────────────────
@@ -1132,20 +1130,60 @@ ae(next(g), 200)
1132
1130
  ae(next(g, 'stop'), 'stop')
1133
1131
 
1134
1132
  # ── 57. iter(callable, sentinel) two-arg form ────────────────────────────────
1135
- # STATUS: NOT IMPLEMENTED — two-argument iter(callable, sentinel) not supported.
1136
- # SKIP:
1137
- # count = [0]
1138
- # def counter():
1139
- # count[0] += 1; return count[0]
1140
- # ae(list(iter(counter, 3)), [1, 2])
1133
+ # Basic usage: collect values until sentinel
1134
+ _count57 = [0]
1135
+ def _counter57():
1136
+ _count57[0] += 1
1137
+ return _count57[0]
1138
+ ade(list(iter(_counter57, 3)), [1, 2])
1139
+ # Sentinel never reached: stops at exact match
1140
+ _vals57 = ['a', 'b', 'c', None]
1141
+ _idx57 = [0]
1142
+ def _src57():
1143
+ v = _vals57[_idx57[0]]
1144
+ _idx57[0] += 1
1145
+ return v
1146
+ ade(list(iter(_src57, None)), ['a', 'b', 'c'])
1147
+ # Sentinel on first call returns empty iterator
1148
+ _first57 = [0]
1149
+ def _fn57():
1150
+ _first57[0] += 1
1151
+ return 'stop'
1152
+ ade(list(iter(_fn57, 'stop')), [])
1153
+ # Works with for-loop
1154
+ _out57 = []
1155
+ _i57 = [0]
1156
+ def _seq57():
1157
+ _i57[0] += 1
1158
+ return _i57[0]
1159
+ for _v57 in iter(_seq57, 4):
1160
+ _out57.append(_v57)
1161
+ ade(_out57, [1, 2, 3])
1162
+ # Works with next()
1163
+ _j57 = [0]
1164
+ def _f57():
1165
+ _j57[0] += 1
1166
+ return _j57[0]
1167
+ _it57 = iter(_f57, 5)
1168
+ ae(next(_it57), 1)
1169
+ ae(next(_it57), 2)
1170
+ ae(next(_it57), 3)
1171
+ ae(next(_it57), 4)
1172
+ ae(next(_it57, 'done'), 'done')
1173
+ # Callable object with __call__
1174
+ class _Caller57:
1175
+ def __init__(self):
1176
+ self.n = 0
1177
+ def __call__(self):
1178
+ self.n += 1
1179
+ return self.n
1180
+ _caller57 = _Caller57()
1181
+ ade(list(iter(_caller57, 3)), [1, 2])
1141
1182
 
1142
1183
  # ── 58. slice() as builtin object ────────────────────────────────────────────
1143
- # STATUS: NOT IMPLEMENTED slice() constructor is not a builtin.
1144
- # SKIP:
1145
- # s = slice(1, 5, 2)
1146
- # ae(s.start, 1)
1147
- # ae(s.stop, 5)
1148
- # ae(s.step, 2)
1184
+ # STATUS: WORKSimplemented (see item #63 below for full tests).
1185
+ # NOTE: This item was previously marked NOT IMPLEMENTED but was added in a
1186
+ # recent release. Full tests live in item #63 and test/slice.pyj.
1149
1187
 
1150
1188
  # ── 59. complex() builtin ────────────────────────────────────────────────────
1151
1189
  # STATUS: ✗ NOT SUPPORTED — no complex number type.
@@ -1182,3 +1220,52 @@ def _ko(a, *, b):
1182
1220
 
1183
1221
  ade(_ko(1, b=2), [1, 2])
1184
1222
  ade(_ko(b=2, a=1), [1, 2])
1223
+
1224
+ # ── 63. slice() builtin ───────────────────────────────────────────────────────
1225
+ # STATUS: ✓ WORKS
1226
+ # Subscript read/write tests (lst[s], lst[s]=val) are in test/slice.pyj
1227
+ # because they require `from __python__ import overload_getitem`.
1228
+
1229
+ # Construction forms
1230
+ _s1 = slice(5)
1231
+ ae(_s1.start, None)
1232
+ ae(_s1.stop, 5)
1233
+ ae(_s1.step, None)
1234
+
1235
+ _s2 = slice(2, 8)
1236
+ ae(_s2.start, 2)
1237
+ ae(_s2.stop, 8)
1238
+ ae(_s2.step, None)
1239
+
1240
+ _s3 = slice(1, 9, 2)
1241
+ ae(_s3.start, 1)
1242
+ ae(_s3.stop, 9)
1243
+ ae(_s3.step, 2)
1244
+
1245
+ # None values preserved
1246
+ _sn = slice(None, None, -1)
1247
+ ae(_sn.start, None)
1248
+ ae(_sn.stop, None)
1249
+ ae(_sn.step, -1)
1250
+
1251
+ # indices() method
1252
+ ade(_s2.indices(10), [2, 8, 1])
1253
+ ade(slice(None, None, 1).indices(5), [0, 5, 1])
1254
+ ade(slice(-3, -1).indices(10), [7, 9, 1])
1255
+ ade(slice(None, None, -1).indices(5), [4, -1, -1])
1256
+
1257
+ # repr / str
1258
+ ae(str(slice(1, 5, None)), 'slice(1, 5, None)')
1259
+ ae(str(slice(None, 5, 2)), 'slice(None, 5, 2)')
1260
+
1261
+ # Equality
1262
+ ok(slice(1, 5) == slice(1, 5))
1263
+ ok(not (slice(1, 5) == slice(1, 6)))
1264
+
1265
+ # isinstance
1266
+ ok(isinstance(slice(1, 5), slice))
1267
+
1268
+ # Delete with slice object uses ρσ_delitem (no overload_getitem needed)
1269
+ _b = [0, 1, 2, 3, 4]
1270
+ del _b[slice(1, 3)]
1271
+ ade(_b, [0, 3, 4])
package/test/slice.pyj ADDED
@@ -0,0 +1,105 @@
1
+ # globals: assrt
2
+ # vim:fileencoding=utf-8
3
+ #
4
+ # slice.pyj — tests for the slice() builtin class.
5
+ # Subscript read/write via a slice object (lst[s], lst[s]=val) requires
6
+ # overload_getitem so that the compiler routes [] through ρσ_getitem.
7
+
8
+ from __python__ import overload_getitem
9
+
10
+ ae = assrt.equal
11
+ ade = assrt.deepEqual
12
+ ok = assrt.ok
13
+
14
+ # ── Construction forms ────────────────────────────────────────────────────────
15
+
16
+ s1 = slice(5)
17
+ ae(s1.start, None)
18
+ ae(s1.stop, 5)
19
+ ae(s1.step, None)
20
+
21
+ s2 = slice(2, 8)
22
+ ae(s2.start, 2)
23
+ ae(s2.stop, 8)
24
+ ae(s2.step, None)
25
+
26
+ s3 = slice(1, 9, 2)
27
+ ae(s3.start, 1)
28
+ ae(s3.stop, 9)
29
+ ae(s3.step, 2)
30
+
31
+ # None values preserved
32
+ sn = slice(None, None, -1)
33
+ ae(sn.start, None)
34
+ ae(sn.stop, None)
35
+ ae(sn.step, -1)
36
+
37
+ # ── indices() ─────────────────────────────────────────────────────────────────
38
+
39
+ ade(s2.indices(10), [2, 8, 1])
40
+ ade(slice(None, None, 1).indices(5), [0, 5, 1])
41
+ ade(slice(-3, -1).indices(10), [7, 9, 1])
42
+ ade(slice(None, None, -1).indices(5), [4, -1, -1])
43
+ ade(slice(0, 100).indices(10), [0, 10, 1]) # stop clamped to length
44
+ ade(slice(-100, 5).indices(10), [0, 5, 1]) # start clamped to 0
45
+
46
+ # ── repr / str ────────────────────────────────────────────────────────────────
47
+
48
+ ae(str(slice(1, 5, None)), 'slice(1, 5, None)')
49
+ ae(str(slice(None, 5, 2)), 'slice(None, 5, 2)')
50
+ ae(str(slice(None, None, None)), 'slice(None, None, None)')
51
+
52
+ # ── Equality ──────────────────────────────────────────────────────────────────
53
+
54
+ ok(slice(1, 5) == slice(1, 5))
55
+ ok(not (slice(1, 5) == slice(1, 6)))
56
+ ok(not (slice(1, 5) == slice(0, 5)))
57
+ ok(slice(1, 5, 2) == slice(1, 5, 2))
58
+ ok(not (slice(1, 5, 2) == slice(1, 5, 3)))
59
+
60
+ # ── isinstance ────────────────────────────────────────────────────────────────
61
+
62
+ ok(isinstance(slice(1, 5), slice))
63
+ ok(not isinstance(42, slice))
64
+ ok(not isinstance([1, 2, 3], slice))
65
+
66
+ # ── List subscript read ───────────────────────────────────────────────────────
67
+
68
+ lst = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
69
+
70
+ ade(lst[slice(2, 5)], [2, 3, 4])
71
+ ade(lst[slice(0, 6, 2)], [0, 2, 4])
72
+ ade(lst[slice(None, 4)], [0, 1, 2, 3])
73
+ ade(lst[slice(None)], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
74
+
75
+ # Negative step
76
+ ade(lst[slice(5, 1, -1)], [5, 4, 3, 2])
77
+ ade(lst[slice(None, None, -1)], [9, 8, 7, 6, 5, 4, 3, 2, 1, 0])
78
+ ade(lst[slice(9, 0, -2)], [9, 7, 5, 3, 1])
79
+
80
+ # ── String subscript read ─────────────────────────────────────────────────────
81
+
82
+ s = 'hello'
83
+ ae(s[slice(1, 4)], 'ell')
84
+ ae(s[slice(None, None, -1)], 'olleh')
85
+ ae(s[slice(0, 5, 2)], 'hlo')
86
+
87
+ # ── List subscript write ──────────────────────────────────────────────────────
88
+
89
+ a = [0, 1, 2, 3, 4]
90
+ a[slice(1, 3)] = [10, 20]
91
+ ade(a, [0, 10, 20, 3, 4])
92
+
93
+ b = [0, 1, 2, 3, 4]
94
+ b[slice(0, 2)] = [99]
95
+ ade(b, [99, 2, 3, 4])
96
+
97
+ # ── List subscript delete ─────────────────────────────────────────────────────
98
+
99
+ c = [0, 1, 2, 3, 4]
100
+ del c[slice(1, 3)]
101
+ ade(c, [0, 3, 4])
102
+
103
+ d = [0, 1, 2, 3, 4]
104
+ del d[slice(0, 1)]
105
+ ade(d, [1, 2, 3, 4])
package/test/str.pyj CHANGED
@@ -98,6 +98,31 @@ test_throws(IndexError, '{1} {2}', 1)
98
98
  test_throws(ValueError, '{1')
99
99
  test_interpolation()
100
100
 
101
+ # Test format() builtin
102
+ ae('3.14', format(3.14159, '.2f'))
103
+ ae(' 42', format(42, '>4d'))
104
+ ae('42 ', format(42, '<4d'))
105
+ ae(' 42 ', format(42, '^4d'))
106
+ ae('0042', format(42, '04d'))
107
+ ae('2a', format(42, 'x'))
108
+ ae('0b101010', format(42, '#b'))
109
+ ae('hello', format('hello'))
110
+ ae('42', format(42))
111
+ ae('None', format(None))
112
+ ae('hello ', format('hello', '<10'))
113
+ ae(' hello', format('hello', '>10'))
114
+ ae(' hello ', format('hello', '^10'))
115
+ ae('1.23e+2', format(123.456, '.2e').toLowerCase())
116
+ ae('hello', format('hello world', '.5'))
117
+
118
+ class FormattableNum:
119
+ def __init__(self, n):
120
+ self.n = n
121
+ def __format__(self, spec):
122
+ return 'FN:' + format(self.n, spec)
123
+
124
+ ae('FN:042', format(FormattableNum(42), '03d'))
125
+
101
126
  # Test miscellaneous services
102
127
  ae('Abc', str.capitalize('aBC'))
103
128
  ae(' 1 ', str.center('1', 3))
@@ -30,7 +30,7 @@ var fib = memoize((function() {
30
30
  } else if ((n === 1 || typeof n === "object" && ρσ_equals(n, 1))) {
31
31
  return 1;
32
32
  } else {
33
- return fib(n - 1) + fib(n - 2);
33
+ return ρσ_list_add(fib(n - 1), fib(n - 2));
34
34
  }
35
35
  };
36
36
  if (!ρσ_anonfunc.__argnames__) Object.defineProperties(ρσ_anonfunc, {