@tishlang/tish 2.12.0 → 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.
- package/Cargo.toml +3 -0
- package/bin/tish +0 -0
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/tests/integration_test.rs +80 -0
- package/crates/tish_ast/src/ast.rs +10 -0
- package/crates/tish_builtins/src/array.rs +529 -56
- package/crates/tish_builtins/src/collections.rs +114 -40
- package/crates/tish_builtins/src/string.rs +95 -8
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +560 -64
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +154 -2
- package/crates/tish_bytecode/src/serialize.rs +2 -0
- package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
- package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
- package/crates/tish_compile/src/codegen.rs +17736 -5269
- package/crates/tish_compile/src/infer.rs +1707 -190
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +66 -5
- package/crates/tish_compile/src/types.rs +224 -28
- package/crates/tish_compile/tests/dump_codegen.rs +21 -0
- package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
- package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
- package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
- package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
- package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
- package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
- package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
- package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
- package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
- package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
- package/crates/tish_compile_js/src/codegen.rs +33 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +100 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +104 -5
- package/crates/tish_core/src/lib.rs +174 -0
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/value.rs +565 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/src/eval.rs +675 -76
- package/crates/tish_eval/src/natives.rs +19 -0
- package/crates/tish_eval/src/value.rs +76 -21
- package/crates/tish_ffi/src/lib.rs +11 -1
- package/crates/tish_ffi/tests/double_free.rs +35 -0
- package/crates/tish_fmt/src/lib.rs +75 -1
- package/crates/tish_lexer/src/lib.rs +76 -0
- package/crates/tish_lexer/src/token.rs +4 -0
- package/crates/tish_lint/src/lib.rs +126 -0
- package/crates/tish_lsp/README.md +2 -1
- package/crates/tish_lsp/src/main.rs +378 -28
- package/crates/tish_native/src/build.rs +41 -0
- package/crates/tish_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +27 -0
- package/crates/tish_parser/src/parser.rs +302 -20
- package/crates/tish_resolve/src/lib.rs +9 -0
- package/crates/tish_runtime/Cargo.toml +5 -0
- package/crates/tish_runtime/src/http.rs +28 -10
- package/crates/tish_runtime/src/http_fetch.rs +150 -1
- package/crates/tish_runtime/src/http_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +523 -42
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +3 -0
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1227 -182
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- 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..=
|
|
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 <=
|
|
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
|
+
}
|