@tishlang/tish 2.10.1 → 2.16.13

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 (75) hide show
  1. package/Cargo.toml +3 -0
  2. package/bin/tish +0 -0
  3. package/crates/tish/Cargo.toml +1 -1
  4. package/crates/tish/src/cli_help.rs +16 -0
  5. package/crates/tish/src/main.rs +24 -4
  6. package/crates/tish/tests/integration_test.rs +149 -0
  7. package/crates/tish_ast/src/ast.rs +10 -0
  8. package/crates/tish_builtins/src/array.rs +529 -56
  9. package/crates/tish_builtins/src/collections.rs +114 -40
  10. package/crates/tish_builtins/src/string.rs +95 -8
  11. package/crates/tish_bytecode/src/chunk.rs +7 -0
  12. package/crates/tish_bytecode/src/compiler.rs +560 -64
  13. package/crates/tish_bytecode/src/lib.rs +1 -1
  14. package/crates/tish_bytecode/src/opcode.rs +154 -2
  15. package/crates/tish_bytecode/src/serialize.rs +2 -0
  16. package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
  17. package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
  18. package/crates/tish_compile/src/codegen.rs +17736 -5269
  19. package/crates/tish_compile/src/infer.rs +1707 -190
  20. package/crates/tish_compile/src/lib.rs +29 -4
  21. package/crates/tish_compile/src/resolve.rs +66 -5
  22. package/crates/tish_compile/src/types.rs +224 -28
  23. package/crates/tish_compile/tests/dump_codegen.rs +21 -0
  24. package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
  25. package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
  26. package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
  27. package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
  28. package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
  29. package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
  30. package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
  31. package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
  32. package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
  33. package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
  34. package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
  35. package/crates/tish_compile_js/src/codegen.rs +91 -4
  36. package/crates/tish_compile_js/src/lib.rs +5 -2
  37. package/crates/tish_compile_js/src/tests_jsx.rs +175 -7
  38. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +100 -2
  39. package/crates/tish_core/Cargo.toml +4 -0
  40. package/crates/tish_core/src/json.rs +104 -5
  41. package/crates/tish_core/src/lib.rs +174 -0
  42. package/crates/tish_core/src/shape.rs +4 -2
  43. package/crates/tish_core/src/value.rs +565 -35
  44. package/crates/tish_core/src/vmref.rs +14 -0
  45. package/crates/tish_eval/src/eval.rs +675 -76
  46. package/crates/tish_eval/src/natives.rs +19 -0
  47. package/crates/tish_eval/src/value.rs +76 -21
  48. package/crates/tish_ffi/src/lib.rs +11 -1
  49. package/crates/tish_ffi/tests/double_free.rs +35 -0
  50. package/crates/tish_fmt/src/lib.rs +75 -1
  51. package/crates/tish_lexer/src/lib.rs +76 -0
  52. package/crates/tish_lexer/src/token.rs +4 -0
  53. package/crates/tish_lint/src/lib.rs +126 -0
  54. package/crates/tish_lsp/README.md +2 -1
  55. package/crates/tish_lsp/src/main.rs +378 -28
  56. package/crates/tish_native/src/build.rs +41 -0
  57. package/crates/tish_parser/Cargo.toml +4 -0
  58. package/crates/tish_parser/src/lib.rs +27 -0
  59. package/crates/tish_parser/src/parser.rs +302 -20
  60. package/crates/tish_resolve/src/lib.rs +9 -0
  61. package/crates/tish_runtime/Cargo.toml +5 -0
  62. package/crates/tish_runtime/src/http.rs +28 -10
  63. package/crates/tish_runtime/src/http_fetch.rs +150 -1
  64. package/crates/tish_runtime/src/http_prefork.rs +72 -11
  65. package/crates/tish_runtime/src/lib.rs +523 -42
  66. package/crates/tish_runtime/src/timers.rs +49 -2
  67. package/crates/tish_ui/src/jsx.rs +253 -0
  68. package/crates/tish_vm/src/jit.rs +2514 -117
  69. package/crates/tish_vm/src/vm.rs +1227 -182
  70. package/package.json +1 -1
  71. package/platform/darwin-arm64/tish +0 -0
  72. package/platform/darwin-x64/tish +0 -0
  73. package/platform/linux-arm64/tish +0 -0
  74. package/platform/linux-x64/tish +0 -0
  75. package/platform/win32-x64/tish.exe +0 -0
@@ -16,5 +16,5 @@ pub use compiler::{
16
16
  compile_with_source, CompileError,
17
17
  };
18
18
  pub use encoding::{binop_to_u8, compound_op_to_u8, u8_to_binop, u8_to_unaryop, unaryop_to_u8};
19
- pub use opcode::Opcode;
19
+ pub use opcode::{MathUnaryFn, Opcode};
20
20
  pub use serialize::{deserialize, serialize};
@@ -133,13 +133,165 @@ pub enum Opcode {
133
133
  /// `delete obj[key]` / `delete obj.prop`. Pops `[obj, key]`, removes the property
134
134
  /// (objects: drop the key; arrays: set the index to a null hole), pushes `true`.
135
135
  DeleteIndex = 53,
136
+ /// String-builder append for statement-position `acc += rhs` where `acc` is a frame slot local
137
+ /// (operand: u16 slot index). Pops `rhs`; appends it to the accumulator in amortized O(1) by
138
+ /// keeping a growable buffer for the slot (see the frame-local builder in `run_chunk`). Leaves
139
+ /// nothing on the stack — only emitted where the assignment's result is discarded. Slots are
140
+ /// frame-private (never captured), so the buffer needs no cross-frame sharing.
141
+ AppendLocal = 54,
142
+ /// #186 — apply a unary `Math.<fn>` intrinsic to the top-of-stack number: pop one value, push
143
+ /// `Math.fn(x)`. The u16 operand is the [`MathUnaryFn`] id. Emitted by the compiler ONLY when
144
+ /// `Math` is provably the global builtin (never shadowed in the program), so the numeric JIT can
145
+ /// lower it to a native op / libcall without a runtime shape guard. Behaviour is identical to
146
+ /// `LoadVar Math; GetMember fn; <arg>; Call 1` on a number.
147
+ MathUnary = 55,
148
+ }
149
+
150
+ /// The unary `Math` functions the [`Opcode::MathUnary`] fast path recognizes (#186). f64→f64;
151
+ /// the discriminant is the opcode operand. Kept in sync with the VM handler and the JIT lowering.
152
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
153
+ #[repr(u16)]
154
+ pub enum MathUnaryFn {
155
+ Sqrt = 0,
156
+ Cbrt = 1,
157
+ Abs = 2,
158
+ Floor = 3,
159
+ Ceil = 4,
160
+ Round = 5,
161
+ Trunc = 6,
162
+ Sign = 7,
163
+ Sin = 8,
164
+ Cos = 9,
165
+ Tan = 10,
166
+ Asin = 11,
167
+ Acos = 12,
168
+ Atan = 13,
169
+ Sinh = 14,
170
+ Cosh = 15,
171
+ Tanh = 16,
172
+ Exp = 17,
173
+ Log = 18,
174
+ Log2 = 19,
175
+ Log10 = 20,
176
+ }
177
+
178
+ impl MathUnaryFn {
179
+ /// Map a `Math.<name>` member to its id, or `None` if it isn't a supported unary intrinsic.
180
+ pub fn from_name(name: &str) -> Option<MathUnaryFn> {
181
+ Some(match name {
182
+ "sqrt" => MathUnaryFn::Sqrt,
183
+ "cbrt" => MathUnaryFn::Cbrt,
184
+ "abs" => MathUnaryFn::Abs,
185
+ "floor" => MathUnaryFn::Floor,
186
+ "ceil" => MathUnaryFn::Ceil,
187
+ "round" => MathUnaryFn::Round,
188
+ "trunc" => MathUnaryFn::Trunc,
189
+ "sign" => MathUnaryFn::Sign,
190
+ "sin" => MathUnaryFn::Sin,
191
+ "cos" => MathUnaryFn::Cos,
192
+ "tan" => MathUnaryFn::Tan,
193
+ "asin" => MathUnaryFn::Asin,
194
+ "acos" => MathUnaryFn::Acos,
195
+ "atan" => MathUnaryFn::Atan,
196
+ "sinh" => MathUnaryFn::Sinh,
197
+ "cosh" => MathUnaryFn::Cosh,
198
+ "tanh" => MathUnaryFn::Tanh,
199
+ "exp" => MathUnaryFn::Exp,
200
+ "log" => MathUnaryFn::Log,
201
+ "log2" => MathUnaryFn::Log2,
202
+ "log10" => MathUnaryFn::Log10,
203
+ _ => return None,
204
+ })
205
+ }
206
+
207
+ /// Decode an opcode operand to the fn id (safe match — no transmute).
208
+ pub fn from_u16(v: u16) -> Option<MathUnaryFn> {
209
+ use MathUnaryFn::*;
210
+ Some(match v {
211
+ 0 => Sqrt,
212
+ 1 => Cbrt,
213
+ 2 => Abs,
214
+ 3 => Floor,
215
+ 4 => Ceil,
216
+ 5 => Round,
217
+ 6 => Trunc,
218
+ 7 => Sign,
219
+ 8 => Sin,
220
+ 9 => Cos,
221
+ 10 => Tan,
222
+ 11 => Asin,
223
+ 12 => Acos,
224
+ 13 => Atan,
225
+ 14 => Sinh,
226
+ 15 => Cosh,
227
+ 16 => Tanh,
228
+ 17 => Exp,
229
+ 18 => Log,
230
+ 19 => Log2,
231
+ 20 => Log10,
232
+ _ => return None,
233
+ })
234
+ }
235
+
236
+ /// Apply the intrinsic (the single source of truth for VM + JIT + interp result parity).
237
+ #[inline]
238
+ pub fn apply(self, x: f64) -> f64 {
239
+ match self {
240
+ MathUnaryFn::Sqrt => x.sqrt(),
241
+ MathUnaryFn::Cbrt => x.cbrt(),
242
+ MathUnaryFn::Abs => x.abs(),
243
+ MathUnaryFn::Floor => x.floor(),
244
+ MathUnaryFn::Ceil => x.ceil(),
245
+ // JS `Math.round`: ties toward +∞, `[-0.5, 0) → -0`; EXACTLY `tishlang_builtins::math::
246
+ // round_f64` (replicated here — this crate sits below builtins — so VM/JIT == interp).
247
+ MathUnaryFn::Round => {
248
+ if x.is_nan() || x.is_infinite() || x == 0.0 {
249
+ x
250
+ } else if (-0.5..0.5).contains(&x) {
251
+ if x < 0.0 {
252
+ -0.0
253
+ } else {
254
+ 0.0
255
+ }
256
+ } else {
257
+ (x + 0.5).floor()
258
+ }
259
+ }
260
+ MathUnaryFn::Trunc => x.trunc(),
261
+ // JS `Math.sign` (matches `tishlang_builtins::math::sign`): NaN→NaN, else ±1, and 0/-0→+0.
262
+ MathUnaryFn::Sign => {
263
+ if x.is_nan() {
264
+ f64::NAN
265
+ } else if x > 0.0 {
266
+ 1.0
267
+ } else if x < 0.0 {
268
+ -1.0
269
+ } else {
270
+ 0.0
271
+ }
272
+ }
273
+ MathUnaryFn::Sin => x.sin(),
274
+ MathUnaryFn::Cos => x.cos(),
275
+ MathUnaryFn::Tan => x.tan(),
276
+ MathUnaryFn::Asin => x.asin(),
277
+ MathUnaryFn::Acos => x.acos(),
278
+ MathUnaryFn::Atan => x.atan(),
279
+ MathUnaryFn::Sinh => x.sinh(),
280
+ MathUnaryFn::Cosh => x.cosh(),
281
+ MathUnaryFn::Tanh => x.tanh(),
282
+ MathUnaryFn::Exp => x.exp(),
283
+ MathUnaryFn::Log => x.ln(),
284
+ MathUnaryFn::Log2 => x.log2(),
285
+ MathUnaryFn::Log10 => x.log10(),
286
+ }
287
+ }
136
288
  }
137
289
 
138
290
  impl Opcode {
139
- /// Decode byte to opcode. Safe for b in 0..=53 (matches #[repr(u8)] discriminants).
291
+ /// Decode byte to opcode. Safe for b in 0..=55 (matches #[repr(u8)] discriminants).
140
292
  #[inline]
141
293
  pub fn from_u8(b: u8) -> Option<Opcode> {
142
- if b <= 53 {
294
+ if b <= 55 {
143
295
  Some(unsafe { std::mem::transmute::<u8, Opcode>(b) })
144
296
  } else {
145
297
  None
@@ -189,5 +189,7 @@ pub fn deserialize(mut data: &[u8]) -> Result<Chunk, String> {
189
189
  // Debug-only; not part of the serialized format (issue #74).
190
190
  lines: Vec::new(),
191
191
  source: None,
192
+ // #187: runtime-only — a deserialized program forgoes the cross-function-call optimization.
193
+ global_name: None,
192
194
  })
193
195
  }
@@ -0,0 +1,63 @@
1
+ //! #186 — the plain-assign string-builder fast path: `s = s + <str>` compiles to `AppendLocal`, but a
2
+ //! numeric accumulator `i = i + 1` must NOT (or the VM's single string-builder slot thrashes → O(n²)).
3
+
4
+ use tishlang_bytecode::{compile, Chunk, Opcode};
5
+ use tishlang_parser::parse;
6
+
7
+ fn count_opcode(chunk: &Chunk, op: u8) -> usize {
8
+ // Opcodes are variable-width; the compiler here emits only slot-carrying ops, so a byte-value
9
+ // scan is a sufficient upper bound for these targeted single-loop programs. We assert presence
10
+ // vs absence, not exact counts, so an incidental operand collision can't cause a false pass.
11
+ let mut n = chunk.code.iter().filter(|&&b| b == op).count();
12
+ for nested in &chunk.nested {
13
+ n += count_opcode(nested, op);
14
+ }
15
+ n
16
+ }
17
+
18
+ fn chunk_of(src: &str) -> Chunk {
19
+ let program = parse(src).expect("parse");
20
+ let optimized = tishlang_opt::optimize(&program);
21
+ compile(&optimized).expect("compile")
22
+ }
23
+
24
+ #[test]
25
+ fn string_plus_assign_uses_append_local() {
26
+ // `s = s + "x"` must route through the builder (the string_concat 55x→2.5x win).
27
+ let chunk = chunk_of("let s = \"\"\nlet i = 0\nwhile (i < 10) { s = s + \"x\"; i = i + 1 }\n");
28
+ assert!(
29
+ count_opcode(&chunk, Opcode::AppendLocal as u8) >= 1,
30
+ "`s = s + \"x\"` must emit AppendLocal"
31
+ );
32
+ }
33
+
34
+ #[test]
35
+ fn numeric_plus_assign_does_not_use_append_local() {
36
+ // A pure numeric loop must NOT builder-ize — no AppendLocal at all.
37
+ let chunk = chunk_of("let n = 0\nlet i = 0\nwhile (i < 10) { n = n + 2; i = i + 1 }\n");
38
+ assert_eq!(
39
+ count_opcode(&chunk, Opcode::AppendLocal as u8),
40
+ 0,
41
+ "`n = n + 2` / `i = i + 1` must NOT emit AppendLocal (would thrash the single builder slot)"
42
+ );
43
+ }
44
+
45
+ #[test]
46
+ fn template_literal_rhs_uses_append_local() {
47
+ let chunk = chunk_of("let t = \"\"\nlet j = 0\nwhile (j < 3) { t = t + `[${j}]`; j = j + 1 }\n");
48
+ assert!(
49
+ count_opcode(&chunk, Opcode::AppendLocal as u8) >= 1,
50
+ "`t = t + `[${{j}}]`` (template RHS) must emit AppendLocal"
51
+ );
52
+ }
53
+
54
+ #[test]
55
+ fn prepend_does_not_use_append_local() {
56
+ // `p = "a" + p` is a prepend — the builder appends, so it must NOT match.
57
+ let chunk = chunk_of("let p = \"z\"\np = \"a\" + p\n");
58
+ assert_eq!(
59
+ count_opcode(&chunk, Opcode::AppendLocal as u8),
60
+ 0,
61
+ "prepend `p = \"a\" + p` must not use the append builder"
62
+ );
63
+ }
@@ -0,0 +1,47 @@
1
+ //! #186 — `Math.<unaryfn>(arg)` lowers to `MathUnary` ONLY when `Math` is the unshadowed global; a
2
+ //! rebound `Math`, a 2-arg `Math.*`, or a non-math member must keep the general call path.
3
+
4
+ use tishlang_bytecode::{compile, Chunk, Opcode};
5
+ use tishlang_parser::parse;
6
+
7
+ fn has_math_unary(chunk: &Chunk) -> bool {
8
+ if chunk.code.contains(&(Opcode::MathUnary as u8)) {
9
+ return true;
10
+ }
11
+ chunk.nested.iter().any(has_math_unary)
12
+ }
13
+
14
+ fn chunk_of(src: &str) -> Chunk {
15
+ let program = parse(src).expect("parse");
16
+ let optimized = tishlang_opt::optimize(&program);
17
+ compile(&optimized).expect("compile")
18
+ }
19
+
20
+ #[test]
21
+ fn global_math_unary_emits_intrinsic() {
22
+ assert!(has_math_unary(&chunk_of("let x = Math.sqrt(4.0)\n")), "Math.sqrt → MathUnary");
23
+ assert!(has_math_unary(&chunk_of("let x = Math.sin(1.0) + Math.floor(2.5)\n")));
24
+ }
25
+
26
+ #[test]
27
+ fn shadowed_math_keeps_general_call() {
28
+ // `Math` rebound as a local → the intrinsic would be a miscompile; must NOT emit MathUnary.
29
+ assert!(
30
+ !has_math_unary(&chunk_of("let Math = 5\nlet x = Math\n")),
31
+ "a program that rebinds `Math` must not intrinsify"
32
+ );
33
+ assert!(
34
+ !has_math_unary(&chunk_of(
35
+ "fn mysqrt(x) { return x }\nlet Math = { sqrt: mysqrt }\nlet x = Math.sqrt(4.0)\n"
36
+ )),
37
+ "a shadowed Math.sqrt must call the user's method"
38
+ );
39
+ }
40
+
41
+ #[test]
42
+ fn non_unary_math_stays_a_call() {
43
+ // Math.max is 2-arg (not a unary intrinsic) → general call, no MathUnary.
44
+ assert!(!has_math_unary(&chunk_of("let x = Math.max(1.0, 2.0)\n")), "Math.max is not unary");
45
+ // Math.PI is a property, not a call.
46
+ assert!(!has_math_unary(&chunk_of("let x = Math.PI\n")), "Math.PI is a property");
47
+ }