@tishlang/tish 2.2.5 → 2.8.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.
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +5 -1
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/src/cli_help.rs +12 -0
- package/crates/tish/src/main.rs +69 -11
- package/crates/tish_ast/src/ast.rs +12 -5
- package/crates/tish_build_utils/src/lib.rs +37 -0
- package/crates/tish_builtins/src/array.rs +82 -1
- package/crates/tish_builtins/src/globals.rs +50 -16
- package/crates/tish_builtins/src/math.rs +63 -9
- package/crates/tish_builtins/src/number.rs +20 -1
- package/crates/tish_builtins/src/string.rs +68 -4
- package/crates/tish_builtins/src/typedarrays.rs +23 -16
- package/crates/tish_bytecode/src/compiler.rs +94 -28
- package/crates/tish_bytecode/tests/break_continue_bytecode.rs +19 -0
- package/crates/tish_compile/src/check.rs +11 -10
- package/crates/tish_compile/src/codegen.rs +1386 -42
- package/crates/tish_compile/src/infer.rs +16 -16
- package/crates/tish_compile/src/lib.rs +38 -0
- package/crates/tish_compile/src/resolve.rs +3 -2
- package/crates/tish_compile/src/types.rs +26 -9
- package/crates/tish_compile_js/src/codegen.rs +55 -4
- package/crates/tish_compile_js/src/tests_jsx.rs +44 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +1 -0
- package/crates/tish_core/Cargo.toml +2 -0
- package/crates/tish_core/src/json.rs +135 -34
- package/crates/tish_core/src/lib.rs +23 -1
- package/crates/tish_core/src/value.rs +64 -11
- package/crates/tish_eval/src/eval.rs +144 -197
- package/crates/tish_eval/src/natives.rs +45 -45
- package/crates/tish_eval/src/regex.rs +35 -27
- package/crates/tish_eval/src/value.rs +8 -0
- package/crates/tish_fmt/src/lib.rs +46 -2
- package/crates/tish_lint/src/bin/tish-lint.rs +17 -8
- package/crates/tish_lint/src/lib.rs +197 -21
- package/crates/tish_lsp/Cargo.toml +7 -1
- package/crates/tish_lsp/src/import_goto.rs +52 -7
- package/crates/tish_lsp/src/main.rs +913 -145
- package/crates/tish_opt/src/lib.rs +5 -3
- package/crates/tish_parser/src/lib.rs +23 -5
- package/crates/tish_parser/src/parser.rs +35 -35
- package/crates/tish_resolve/src/lib.rs +567 -17
- package/crates/tish_runtime/src/lib.rs +58 -18
- package/crates/tish_ui/src/jsx.rs +2 -2
- package/crates/tish_vm/src/jit.rs +212 -10
- package/crates/tish_vm/src/vm.rs +39 -18
- package/crates/tish_wasm/src/lib.rs +116 -22
- 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
|
@@ -52,15 +52,15 @@ impl InferCtx {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
fn is_number(ann: &TypeAnnotation) -> bool {
|
|
55
|
-
matches!(ann, TypeAnnotation::Simple(s) if s.as_ref() == "number")
|
|
55
|
+
matches!(ann, TypeAnnotation::Simple(s, _) if s.as_ref() == "number")
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
fn is_string(ann: &TypeAnnotation) -> bool {
|
|
59
|
-
matches!(ann, TypeAnnotation::Simple(s) if s.as_ref() == "string")
|
|
59
|
+
matches!(ann, TypeAnnotation::Simple(s, _) if s.as_ref() == "string")
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
fn is_bool(ann: &TypeAnnotation) -> bool {
|
|
63
|
-
matches!(ann, TypeAnnotation::Simple(s) if s.as_ref() == "boolean")
|
|
63
|
+
matches!(ann, TypeAnnotation::Simple(s, _) if s.as_ref() == "boolean")
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/// Element type of an array literal of uniform native scalars (number/string/boolean), for
|
|
@@ -91,15 +91,15 @@ fn infer_array_elem(elements: &[tishlang_ast::ArrayElement], ctx: &InferCtx) ->
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
fn number_ann() -> TypeAnnotation {
|
|
94
|
-
TypeAnnotation::Simple("number".into())
|
|
94
|
+
TypeAnnotation::Simple("number".into(), tishlang_ast::Span::default())
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
fn string_ann() -> TypeAnnotation {
|
|
98
|
-
TypeAnnotation::Simple("string".into())
|
|
98
|
+
TypeAnnotation::Simple("string".into(), tishlang_ast::Span::default())
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
fn bool_ann() -> TypeAnnotation {
|
|
102
|
-
TypeAnnotation::Simple("boolean".into())
|
|
102
|
+
TypeAnnotation::Simple("boolean".into(), tishlang_ast::Span::default())
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/// Infer the `TypeAnnotation` for an expression, if unambiguous.
|
|
@@ -237,7 +237,7 @@ fn pi_stmt(s: Statement) -> Statement {
|
|
|
237
237
|
&& tp.default.is_none()
|
|
238
238
|
&& nus_stmt(&body, tp.name.as_ref(), &nums)
|
|
239
239
|
{
|
|
240
|
-
tp.type_ann = Some(TypeAnnotation::Simple(std::sync::Arc::from("number")));
|
|
240
|
+
tp.type_ann = Some(TypeAnnotation::Simple(std::sync::Arc::from("number"), tishlang_ast::Span::default()));
|
|
241
241
|
}
|
|
242
242
|
FunParam::Simple(tp)
|
|
243
243
|
}
|
|
@@ -558,7 +558,7 @@ pub(crate) fn pi_mentions(e: &Expr, name: &str) -> bool {
|
|
|
558
558
|
}
|
|
559
559
|
}),
|
|
560
560
|
Object { props, .. } => props.iter().any(|p| match p {
|
|
561
|
-
tishlang_ast::ObjectProp::KeyValue(_, v) => pi_mentions(v, name),
|
|
561
|
+
tishlang_ast::ObjectProp::KeyValue(_, v, _) => pi_mentions(v, name),
|
|
562
562
|
tishlang_ast::ObjectProp::Spread(x) => pi_mentions(x, name),
|
|
563
563
|
}),
|
|
564
564
|
TemplateLiteral { exprs, .. } => exprs.iter().any(|x| pi_mentions(x, name)),
|
|
@@ -612,7 +612,7 @@ impl StructRegistry {
|
|
|
612
612
|
|
|
613
613
|
fn type_canon(t: &TypeAnnotation) -> String {
|
|
614
614
|
match t {
|
|
615
|
-
TypeAnnotation::Simple(s) => s.to_string(),
|
|
615
|
+
TypeAnnotation::Simple(s, _) => s.to_string(),
|
|
616
616
|
TypeAnnotation::Object(fields) => format!(
|
|
617
617
|
"{{{}}}",
|
|
618
618
|
fields
|
|
@@ -634,10 +634,10 @@ fn infer_object_shape(
|
|
|
634
634
|
let mut fields = Vec::with_capacity(props.len());
|
|
635
635
|
for p in props {
|
|
636
636
|
match p {
|
|
637
|
-
tishlang_ast::ObjectProp::KeyValue(k, v) => {
|
|
637
|
+
tishlang_ast::ObjectProp::KeyValue(k, v, _) => {
|
|
638
638
|
let ty = infer_expr_type(v, ctx)?;
|
|
639
639
|
// Only primitive field types in this conservative version.
|
|
640
|
-
if !matches!(&ty, TypeAnnotation::Simple(s)
|
|
640
|
+
if !matches!(&ty, TypeAnnotation::Simple(s, _)
|
|
641
641
|
if matches!(s.as_ref(), "number" | "string" | "boolean"))
|
|
642
642
|
{
|
|
643
643
|
return None;
|
|
@@ -776,12 +776,12 @@ fn si_block(stmts: Vec<Statement>, reg: &mut StructRegistry, ctx: &mut InferCtx)
|
|
|
776
776
|
// Sound: every later use in this block must be a literal-key read.
|
|
777
777
|
if uses_are_struct_safe(name.as_ref(), &keys, &stmts[i + 1..]) {
|
|
778
778
|
let alias = reg.intern(&fields);
|
|
779
|
-
ctx.define(name.as_ref(), TypeAnnotation::Simple(alias.as_str().into()));
|
|
779
|
+
ctx.define(name.as_ref(), TypeAnnotation::Simple(alias.as_str().into(), tishlang_ast::Span::default()));
|
|
780
780
|
out.push(Statement::VarDecl {
|
|
781
781
|
name: name.clone(),
|
|
782
782
|
name_span: *name_span,
|
|
783
783
|
mutable: *mutable,
|
|
784
|
-
type_ann: Some(TypeAnnotation::Simple(alias.as_str().into())),
|
|
784
|
+
type_ann: Some(TypeAnnotation::Simple(alias.as_str().into(), tishlang_ast::Span::default())),
|
|
785
785
|
init: stmt_init_clone(stmt),
|
|
786
786
|
span: *span,
|
|
787
787
|
});
|
|
@@ -1303,7 +1303,7 @@ fn arr_expr_safe(e: &Expr, name: &str) -> bool {
|
|
|
1303
1303
|
}
|
|
1304
1304
|
}),
|
|
1305
1305
|
Object { props, .. } => props.iter().all(|p| match p {
|
|
1306
|
-
tishlang_ast::ObjectProp::KeyValue(_, v) => arr_expr_safe(v, name),
|
|
1306
|
+
tishlang_ast::ObjectProp::KeyValue(_, v, _) => arr_expr_safe(v, name),
|
|
1307
1307
|
tishlang_ast::ObjectProp::Spread(v) => arr_expr_safe(v, name),
|
|
1308
1308
|
}),
|
|
1309
1309
|
// Anything else that mentions `name`: be safe, bail.
|
|
@@ -1423,7 +1423,7 @@ fn expr_name_safe(e: &Expr, name: &str, keys: &std::collections::HashSet<&str>)
|
|
|
1423
1423
|
}
|
|
1424
1424
|
}),
|
|
1425
1425
|
Object { props, .. } => props.iter().all(|p| match p {
|
|
1426
|
-
tishlang_ast::ObjectProp::KeyValue(_, v) => expr_name_safe(v, name, keys),
|
|
1426
|
+
tishlang_ast::ObjectProp::KeyValue(_, v, _) => expr_name_safe(v, name, keys),
|
|
1427
1427
|
tishlang_ast::ObjectProp::Spread(e) => expr_name_safe(e, name, keys),
|
|
1428
1428
|
}),
|
|
1429
1429
|
TemplateLiteral { exprs, .. } => exprs.iter().all(|e| expr_name_safe(e, name, keys)),
|
|
@@ -1642,7 +1642,7 @@ mod param_infer_tests {
|
|
|
1642
1642
|
if let FunParam::Simple(tp) = p {
|
|
1643
1643
|
if tp.name.as_ref() == param {
|
|
1644
1644
|
return tp.type_ann.as_ref().map(|a| match a {
|
|
1645
|
-
TypeAnnotation::Simple(s) => s.to_string(),
|
|
1645
|
+
TypeAnnotation::Simple(s, _) => s.to_string(),
|
|
1646
1646
|
_ => "<complex>".to_string(),
|
|
1647
1647
|
});
|
|
1648
1648
|
}
|
|
@@ -186,6 +186,44 @@ fn factory() {
|
|
|
186
186
|
"expected outerVar to be inferred as f64 (Copy, no clone needed)"
|
|
187
187
|
);
|
|
188
188
|
}
|
|
189
|
+
|
|
190
|
+
/// i32-loop-var lowering: an FNV-style integer/bitwise hash accumulator declared before a `for`
|
|
191
|
+
/// and reassigned only by `>>> 0`/bitwise ops lives in an `i32` register across the loop — no
|
|
192
|
+
/// per-op `to_int32(h)`↔`f64` round-trip — with a single `f64` excursion for the `h * C`
|
|
193
|
+
/// multiply (which exceeds 2^53, so it must round in f64 before `>>> 0`, exactly as V8 does).
|
|
194
|
+
#[test]
|
|
195
|
+
fn fnv_accumulator_lowers_to_i32_register() {
|
|
196
|
+
let src = r#"
|
|
197
|
+
let h = 2166136261
|
|
198
|
+
for (let i = 0; i < 100; i++) {
|
|
199
|
+
h = h ^ (i & 255)
|
|
200
|
+
h = (h * 16777619) >>> 0
|
|
201
|
+
h = ((h << 13) | (h >>> 19)) >>> 0
|
|
202
|
+
}
|
|
203
|
+
let check = h >>> 0
|
|
204
|
+
console.log(check)
|
|
205
|
+
"#;
|
|
206
|
+
let rust = compile(&parse(src).unwrap()).unwrap();
|
|
207
|
+
// The accumulator is an i32 register, initialized via the u32 reinterpretation so the
|
|
208
|
+
// > i32::MAX seed keeps its JS ToInt32 bit-pattern.
|
|
209
|
+
assert!(
|
|
210
|
+
rust.contains("let mut h: i32 = (2166136261u32) as i32;"),
|
|
211
|
+
"expected `h` to be an i32 register seeded via u32 reinterpretation; got:\n{}",
|
|
212
|
+
rust.lines().filter(|l| l.contains("h")).take(8).collect::<Vec<_>>().join("\n")
|
|
213
|
+
);
|
|
214
|
+
// The per-iteration `to_int32(h)` round-trips are gone: `h` is read straight from the
|
|
215
|
+
// register inside the bitwise chain (no `to_int32(h)` substring referencing the accumulator).
|
|
216
|
+
assert!(
|
|
217
|
+
!rust.contains("to_int32(h)"),
|
|
218
|
+
"expected NO per-op `to_int32(h)` round-trip on the i32 accumulator"
|
|
219
|
+
);
|
|
220
|
+
// The only f64 excursion is the multiply, lowered as an unchecked truncation of the
|
|
221
|
+
// provably-finite product (`h as f64 * 16777619`).
|
|
222
|
+
assert!(
|
|
223
|
+
rust.contains(".to_int_unchecked::<i64>()") && rust.contains("16777619"),
|
|
224
|
+
"expected the `h * 16777619` excursion to lower to an unchecked f64 truncation"
|
|
225
|
+
);
|
|
226
|
+
}
|
|
189
227
|
}
|
|
190
228
|
|
|
191
229
|
#[cfg(test)]
|
|
@@ -173,7 +173,7 @@ pub fn program_uses_document(program: &Program) -> bool {
|
|
|
173
173
|
ArrayElement::Expr(e) | ArrayElement::Spread(e) => expr_uses_document(e),
|
|
174
174
|
}),
|
|
175
175
|
Expr::Object { props, .. } => props.iter().any(|p| match p {
|
|
176
|
-
ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => expr_uses_document(e),
|
|
176
|
+
ObjectProp::KeyValue(_, e, _) | ObjectProp::Spread(e) => expr_uses_document(e),
|
|
177
177
|
}),
|
|
178
178
|
Expr::Assign { value, .. }
|
|
179
179
|
| Expr::CompoundAssign { value, .. }
|
|
@@ -1601,7 +1601,7 @@ fn rewrite_expr_scope(expr: &mut Expr, active: &HashMap<String, Arc<str>>) {
|
|
|
1601
1601
|
Expr::Object { props, .. } => {
|
|
1602
1602
|
for p in props {
|
|
1603
1603
|
match p {
|
|
1604
|
-
ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => {
|
|
1604
|
+
ObjectProp::KeyValue(_, e, _) | ObjectProp::Spread(e) => {
|
|
1605
1605
|
rewrite_expr_scope(e, active) // key is a property name; value recurses
|
|
1606
1606
|
}
|
|
1607
1607
|
}
|
|
@@ -1819,6 +1819,7 @@ pub fn merge_modules(mut modules: Vec<ResolvedModule>) -> Result<MergedProgram,
|
|
|
1819
1819
|
name: Arc::from(v.clone()),
|
|
1820
1820
|
span: *span,
|
|
1821
1821
|
},
|
|
1822
|
+
*name_span,
|
|
1822
1823
|
));
|
|
1823
1824
|
}
|
|
1824
1825
|
merge_push(
|
|
@@ -13,6 +13,12 @@ pub enum RustType {
|
|
|
13
13
|
Value,
|
|
14
14
|
/// f64 (for number)
|
|
15
15
|
F64,
|
|
16
|
+
/// i32 — a `number` local PROVEN to always hold an integer reinterpretable as a JS ToInt32
|
|
17
|
+
/// bit-pattern, kept in an integer register across a bitwise/hash hot loop (bun/JSC-style)
|
|
18
|
+
/// instead of round-tripping `f64`↔`i32` on every op. The value is the signed int32
|
|
19
|
+
/// (= JS `ToInt32`) view; `>>> 0` results are uint32 reinterpreted into this i32. Only the
|
|
20
|
+
/// codegen's i32-loop-var lowering produces this type — never `from_annotation`.
|
|
21
|
+
I32,
|
|
16
22
|
/// String (for string)
|
|
17
23
|
String,
|
|
18
24
|
/// bool (for boolean)
|
|
@@ -62,7 +68,7 @@ impl RustType {
|
|
|
62
68
|
aliases: &HashMap<String, RustType>,
|
|
63
69
|
) -> Self {
|
|
64
70
|
match ann {
|
|
65
|
-
TypeAnnotation::Simple(name) => match name.as_ref() {
|
|
71
|
+
TypeAnnotation::Simple(name, _) => match name.as_ref() {
|
|
66
72
|
"number" => RustType::F64,
|
|
67
73
|
"string" => RustType::String,
|
|
68
74
|
"boolean" | "bool" => RustType::Bool,
|
|
@@ -111,10 +117,10 @@ impl RustType {
|
|
|
111
117
|
if types.len() == 2 {
|
|
112
118
|
let has_null = types
|
|
113
119
|
.iter()
|
|
114
|
-
.any(|t| matches!(t, TypeAnnotation::Simple(s) if s.as_ref() == "null"));
|
|
120
|
+
.any(|t| matches!(t, TypeAnnotation::Simple(s, _) if s.as_ref() == "null"));
|
|
115
121
|
if has_null {
|
|
116
122
|
let non_null = types.iter().find(
|
|
117
|
-
|t| !matches!(t, TypeAnnotation::Simple(s) if s.as_ref() == "null"),
|
|
123
|
+
|t| !matches!(t, TypeAnnotation::Simple(s, _) if s.as_ref() == "null"),
|
|
118
124
|
);
|
|
119
125
|
if let Some(inner) = non_null {
|
|
120
126
|
return RustType::Option(Box::new(Self::from_annotation_with_aliases(
|
|
@@ -222,6 +228,7 @@ impl RustType {
|
|
|
222
228
|
match self {
|
|
223
229
|
RustType::Value => "Value".to_string(),
|
|
224
230
|
RustType::F64 => "f64".to_string(),
|
|
231
|
+
RustType::I32 => "i32".to_string(),
|
|
225
232
|
RustType::String => "String".to_string(),
|
|
226
233
|
RustType::Bool => "bool".to_string(),
|
|
227
234
|
RustType::Unit => "()".to_string(),
|
|
@@ -250,6 +257,7 @@ impl RustType {
|
|
|
250
257
|
match self {
|
|
251
258
|
RustType::Value => "Value::Null".to_string(),
|
|
252
259
|
RustType::F64 => "0.0".to_string(),
|
|
260
|
+
RustType::I32 => "0i32".to_string(),
|
|
253
261
|
RustType::String => "String::new()".to_string(),
|
|
254
262
|
RustType::Bool => "false".to_string(),
|
|
255
263
|
RustType::Unit => "()".to_string(),
|
|
@@ -306,6 +314,12 @@ impl RustType {
|
|
|
306
314
|
"match &{} {{ Value::Number(n) => *n, _ => panic!(\"expected number\") }}",
|
|
307
315
|
value_expr
|
|
308
316
|
),
|
|
317
|
+
// A `Value::Number` narrowed to its JS ToInt32 bit-pattern (NaN/±Inf → 0, exactly as
|
|
318
|
+
// the bitwise/shift path coerces). Only reached for an `I32`-typed binding boundary.
|
|
319
|
+
RustType::I32 => format!(
|
|
320
|
+
"match &{} {{ Value::Number(n) => tishlang_runtime::to_int32(*n), _ => panic!(\"expected number\") }}",
|
|
321
|
+
value_expr
|
|
322
|
+
),
|
|
309
323
|
RustType::String => format!(
|
|
310
324
|
"match &{} {{ Value::String(s) => s.to_string(), _ => panic!(\"expected string\") }}",
|
|
311
325
|
value_expr
|
|
@@ -363,6 +377,9 @@ impl RustType {
|
|
|
363
377
|
}
|
|
364
378
|
RustType::Value => native_expr.to_string(),
|
|
365
379
|
RustType::F64 => format!("Value::Number({})", native_expr),
|
|
380
|
+
// The signed int32 view boxes as a JS Number (every i32 is exactly representable in
|
|
381
|
+
// f64). The uint32 `>>> 0` reinterpretation is applied at the boxing site, not here.
|
|
382
|
+
RustType::I32 => format!("Value::Number(({}) as f64)", native_expr),
|
|
366
383
|
RustType::String => format!("Value::String({}.clone().into())", native_expr),
|
|
367
384
|
RustType::Bool => format!("Value::Bool({})", native_expr),
|
|
368
385
|
RustType::Unit => "Value::Null".to_string(),
|
|
@@ -532,22 +549,22 @@ mod tests {
|
|
|
532
549
|
#[test]
|
|
533
550
|
fn test_simple_types() {
|
|
534
551
|
assert_eq!(
|
|
535
|
-
RustType::from_annotation(&TypeAnnotation::Simple("number".into())),
|
|
552
|
+
RustType::from_annotation(&TypeAnnotation::Simple("number".into(), tishlang_ast::Span::default())),
|
|
536
553
|
RustType::F64
|
|
537
554
|
);
|
|
538
555
|
assert_eq!(
|
|
539
|
-
RustType::from_annotation(&TypeAnnotation::Simple("string".into())),
|
|
556
|
+
RustType::from_annotation(&TypeAnnotation::Simple("string".into(), tishlang_ast::Span::default())),
|
|
540
557
|
RustType::String
|
|
541
558
|
);
|
|
542
559
|
assert_eq!(
|
|
543
|
-
RustType::from_annotation(&TypeAnnotation::Simple("boolean".into())),
|
|
560
|
+
RustType::from_annotation(&TypeAnnotation::Simple("boolean".into(), tishlang_ast::Span::default())),
|
|
544
561
|
RustType::Bool
|
|
545
562
|
);
|
|
546
563
|
}
|
|
547
564
|
|
|
548
565
|
#[test]
|
|
549
566
|
fn test_array_type() {
|
|
550
|
-
let arr_type = TypeAnnotation::Array(Box::new(TypeAnnotation::Simple("number".into())));
|
|
567
|
+
let arr_type = TypeAnnotation::Array(Box::new(TypeAnnotation::Simple("number".into(), tishlang_ast::Span::default())));
|
|
551
568
|
assert_eq!(
|
|
552
569
|
RustType::from_annotation(&arr_type),
|
|
553
570
|
RustType::Vec(Box::new(RustType::F64))
|
|
@@ -557,8 +574,8 @@ mod tests {
|
|
|
557
574
|
#[test]
|
|
558
575
|
fn test_nullable_type() {
|
|
559
576
|
let nullable = TypeAnnotation::Union(vec![
|
|
560
|
-
TypeAnnotation::Simple("string".into()),
|
|
561
|
-
TypeAnnotation::Simple("null".into()),
|
|
577
|
+
TypeAnnotation::Simple("string".into(), tishlang_ast::Span::default()),
|
|
578
|
+
TypeAnnotation::Simple("null".into(), tishlang_ast::Span::default()),
|
|
562
579
|
]);
|
|
563
580
|
assert_eq!(
|
|
564
581
|
RustType::from_annotation(&nullable),
|
|
@@ -487,9 +487,29 @@ impl Codegen {
|
|
|
487
487
|
}
|
|
488
488
|
}
|
|
489
489
|
|
|
490
|
+
/// Is `expr` the `null` literal? Used to lower `x === null` / `x !== null` to JS `== null` /
|
|
491
|
+
/// `!= null` so the check catches `undefined` too (tish treats a missing/undefined value as
|
|
492
|
+
/// null on every other backend) — see the StrictEq/StrictNe arms below.
|
|
493
|
+
fn is_null_literal(expr: &Expr) -> bool {
|
|
494
|
+
matches!(
|
|
495
|
+
expr,
|
|
496
|
+
Expr::Literal {
|
|
497
|
+
value: Literal::Null,
|
|
498
|
+
..
|
|
499
|
+
}
|
|
500
|
+
)
|
|
501
|
+
}
|
|
502
|
+
|
|
490
503
|
fn emit_expr(&mut self, expr: &Expr) -> Result<String, CompileError> {
|
|
491
504
|
Ok(match expr {
|
|
492
505
|
Expr::Literal { value, .. } => match value {
|
|
506
|
+
// Rust's `{}` prints non-finite f64 as `inf` / `-inf` / `NaN`; only `NaN` is valid JS.
|
|
507
|
+
// Emit the JS spellings so a folded `1/0` / `-1/0` doesn't become an undefined `inf`
|
|
508
|
+
// identifier in the output.
|
|
509
|
+
Literal::Number(n) if n.is_nan() => "NaN".to_string(),
|
|
510
|
+
Literal::Number(n) if n.is_infinite() => {
|
|
511
|
+
if *n < 0.0 { "-Infinity".to_string() } else { "Infinity".to_string() }
|
|
512
|
+
}
|
|
493
513
|
Literal::Number(n) => format!("{}", n),
|
|
494
514
|
Literal::String(s) => format!("{:?}", s.as_ref()),
|
|
495
515
|
Literal::Bool(b) => format!("{}", b),
|
|
@@ -510,8 +530,26 @@ impl Codegen {
|
|
|
510
530
|
BinOp::Pow => "**",
|
|
511
531
|
BinOp::Eq => "==",
|
|
512
532
|
BinOp::Ne => "!=",
|
|
513
|
-
|
|
514
|
-
|
|
533
|
+
// tish has no `undefined`: a missing/absent value reads back as `null`, so
|
|
534
|
+
// `x === null` means "is nullish" — exactly how interp/vm/native behave (a
|
|
535
|
+
// missing property is null there). In the JS runtime a missing property is
|
|
536
|
+
// `undefined`, so lower `=== null` / `!== null` to loose `== null` / `!= null`,
|
|
537
|
+
// which match BOTH null and undefined — keeping `=== null` mean the same thing on
|
|
538
|
+
// every target. Strict equality between non-null operands is unaffected.
|
|
539
|
+
BinOp::StrictEq => {
|
|
540
|
+
if Self::is_null_literal(left) || Self::is_null_literal(right) {
|
|
541
|
+
"=="
|
|
542
|
+
} else {
|
|
543
|
+
"==="
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
BinOp::StrictNe => {
|
|
547
|
+
if Self::is_null_literal(left) || Self::is_null_literal(right) {
|
|
548
|
+
"!="
|
|
549
|
+
} else {
|
|
550
|
+
"!=="
|
|
551
|
+
}
|
|
552
|
+
}
|
|
515
553
|
BinOp::Lt => "<",
|
|
516
554
|
BinOp::Le => "<=",
|
|
517
555
|
BinOp::Gt => ">",
|
|
@@ -563,6 +601,15 @@ impl Codegen {
|
|
|
563
601
|
..
|
|
564
602
|
} => {
|
|
565
603
|
let obj = self.emit_expr(object)?;
|
|
604
|
+
// `255.toString()` is a JS syntax error — the lexer reads `255.` as a float and
|
|
605
|
+
// then chokes on the method name. Parenthesize a numeric-literal object so member
|
|
606
|
+
// access / method calls stay valid: `(255).toString()`. (Folded constants reach
|
|
607
|
+
// codegen as number literals too, so this covers e.g. `(100 * 2).toString()`.)
|
|
608
|
+
let obj = if matches!(&**object, Expr::Literal { value: Literal::Number(_), .. }) {
|
|
609
|
+
format!("({})", obj)
|
|
610
|
+
} else {
|
|
611
|
+
obj
|
|
612
|
+
};
|
|
566
613
|
let expr = match prop {
|
|
567
614
|
MemberProp::Name { name, .. } => {
|
|
568
615
|
if name.parse::<u32>().is_ok()
|
|
@@ -629,7 +676,7 @@ impl Codegen {
|
|
|
629
676
|
let parts: Result<Vec<_>, _> = props
|
|
630
677
|
.iter()
|
|
631
678
|
.map(|p| match p {
|
|
632
|
-
ObjectProp::KeyValue(k, v) => {
|
|
679
|
+
ObjectProp::KeyValue(k, v, _) => {
|
|
633
680
|
let key = k.as_ref();
|
|
634
681
|
let val = self.emit_expr(v)?;
|
|
635
682
|
Ok(if key.chars().all(|c| c.is_alphanumeric() || c == '_') {
|
|
@@ -650,7 +697,11 @@ impl Codegen {
|
|
|
650
697
|
}
|
|
651
698
|
Expr::TypeOf { operand, .. } => {
|
|
652
699
|
let o = self.emit_expr(operand)?;
|
|
653
|
-
|
|
700
|
+
// tish `typeof null` is "null" (interp/vm/native all agree — null is a first-class
|
|
701
|
+
// type, not JS's `typeof null === "object"` wart). tish has no `undefined`, so any
|
|
702
|
+
// nullish operand (incl. a JS-runtime `undefined`) maps to "null". Evaluate the
|
|
703
|
+
// operand once via the arrow arg so side effects don't run twice.
|
|
704
|
+
format!("((__v) => __v == null ? \"null\" : typeof __v)({})", o)
|
|
654
705
|
}
|
|
655
706
|
Expr::Delete { target, .. } => {
|
|
656
707
|
// Emit the raw property *reference*, not a value: `emit_expr` wraps Index /
|
|
@@ -411,4 +411,48 @@ fn factory() {
|
|
|
411
411
|
);
|
|
412
412
|
}
|
|
413
413
|
}
|
|
414
|
+
|
|
415
|
+
// tish `=== null` / `!== null` lower to JS `== null` / `!= null` so the nullish check catches the
|
|
416
|
+
// JS-runtime `undefined` (missing props / holes) too — matching interp/vm/native, which read a
|
|
417
|
+
// missing property back as null. Strict equality between non-null operands stays strict.
|
|
418
|
+
#[test]
|
|
419
|
+
fn strict_eq_null_lowers_to_loose_null() {
|
|
420
|
+
let program = parse("let x = 1\nconsole.log(x === null)\nconsole.log(x !== null)\n").unwrap();
|
|
421
|
+
let js = crate::compile(&program, false).unwrap();
|
|
422
|
+
assert!(!js.contains("=== null"), "`=== null` must lower to `== null`:\n{js}");
|
|
423
|
+
assert!(!js.contains("!== null"), "`!== null` must lower to `!= null`:\n{js}");
|
|
424
|
+
assert!(
|
|
425
|
+
js.contains("== null") && js.contains("!= null"),
|
|
426
|
+
"expected loose null checks:\n{js}"
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
#[test]
|
|
431
|
+
fn strict_eq_between_non_null_operands_stays_strict() {
|
|
432
|
+
let program = parse("let a = 1\nlet b = 2\nconsole.log(a === b)\nconsole.log(a !== b)\n").unwrap();
|
|
433
|
+
let js = crate::compile(&program, false).unwrap();
|
|
434
|
+
assert!(js.contains("==="), "non-null `===` must stay strict:\n{js}");
|
|
435
|
+
assert!(js.contains("!=="), "non-null `!==` must stay strict:\n{js}");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// `typeof null` is "null" in tish (interp/vm/native agree — null is a first-class type), not JS's
|
|
439
|
+
// `typeof null === "object"` wart. The JS backend must map a nullish operand to "null".
|
|
440
|
+
#[test]
|
|
441
|
+
fn typeof_null_emits_null_not_object() {
|
|
442
|
+
let program = parse("console.log(typeof null)\n").unwrap();
|
|
443
|
+
let js = crate::compile(&program, false).unwrap();
|
|
444
|
+
assert!(!js.contains("(typeof null)"), "must not emit raw `typeof null`:\n{js}");
|
|
445
|
+
assert!(js.contains("\"null\""), "typeof of a nullish value must yield \"null\":\n{js}");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// 1/0 and -1/0 fold to Infinity / -Infinity; emit the JS spellings, not Rust's `inf` / `-inf`
|
|
449
|
+
// (which would be undefined identifiers in the output).
|
|
450
|
+
#[test]
|
|
451
|
+
fn non_finite_number_literals_use_js_spellings() {
|
|
452
|
+
let pos = crate::compile(&parse("console.log(1 / 0)\n").unwrap(), true).unwrap();
|
|
453
|
+
assert!(pos.contains("Infinity"), "1/0 must emit Infinity:\n{pos}");
|
|
454
|
+
assert!(!pos.contains("inf"), "must not emit Rust's lowercase `inf`:\n{pos}");
|
|
455
|
+
let neg = crate::compile(&parse("console.log(-1 / 0)\n").unwrap(), true).unwrap();
|
|
456
|
+
assert!(neg.contains("-Infinity"), "-1/0 must emit -Infinity:\n{neg}");
|
|
457
|
+
}
|
|
414
458
|
}
|
|
@@ -23,6 +23,8 @@ ahash = "0.8.11"
|
|
|
23
23
|
arcstr = "1"
|
|
24
24
|
smallvec = "1"
|
|
25
25
|
indexmap = "2"
|
|
26
|
+
# Fast integer→decimal for the JSON.stringify number fast-path (already in the workspace lock).
|
|
27
|
+
itoa = "1"
|
|
26
28
|
fancy-regex = { version = "0.17.0", optional = true }
|
|
27
29
|
# Only under `send-values` (the Arc<Mutex> path): parking_lot's uncontended lock is a
|
|
28
30
|
# single atomic with no pthread syscall — profiled as a top cost on object/array access.
|