@tishlang/tish 1.13.2 → 2.0.1
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 +2 -0
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +1 -0
- package/crates/tish/Cargo.toml +11 -3
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cli_help.rs +15 -4
- package/crates/tish/src/main.rs +93 -21
- package/crates/tish/src/repl_completion.rs +0 -1
- package/crates/tish/tests/error_source_location.rs +36 -0
- package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
- package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
- package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
- package/crates/tish/tests/integration_test.rs +402 -91
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/src/ast.rs +37 -8
- package/crates/tish_builtins/Cargo.toml +2 -0
- package/crates/tish_builtins/src/array.rs +375 -13
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +59 -19
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +86 -6
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +5 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +2 -2
- package/crates/tish_builtins/src/string.rs +19 -20
- package/crates/tish_builtins/src/symbol.rs +1 -1
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/src/chunk.rs +69 -1
- package/crates/tish_bytecode/src/compiler.rs +933 -89
- package/crates/tish_bytecode/src/encoding.rs +2 -0
- package/crates/tish_bytecode/src/lib.rs +2 -1
- package/crates/tish_bytecode/src/opcode.rs +47 -4
- package/crates/tish_bytecode/src/serialize.rs +31 -1
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +2334 -349
- package/crates/tish_compile/src/infer.rs +1395 -6
- package/crates/tish_compile/src/lib.rs +50 -8
- package/crates/tish_compile/src/resolve.rs +584 -21
- package/crates/tish_compile/src/types.rs +106 -2
- package/crates/tish_compile_js/src/codegen.rs +67 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
- package/crates/tish_core/Cargo.toml +7 -1
- package/crates/tish_core/src/console_style.rs +11 -1
- package/crates/tish_core/src/json.rs +81 -38
- package/crates/tish_core/src/lib.rs +3 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/value.rs +679 -25
- package/crates/tish_core/src/vmref.rs +13 -8
- package/crates/tish_cranelift/src/link.rs +17 -4
- package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
- package/crates/tish_eval/Cargo.toml +6 -0
- package/crates/tish_eval/src/eval.rs +665 -117
- package/crates/tish_eval/src/http.rs +4 -1
- package/crates/tish_eval/src/natives.rs +165 -13
- package/crates/tish_eval/src/value.rs +31 -13
- package/crates/tish_eval/src/value_convert.rs +10 -4
- package/crates/tish_ffi/Cargo.toml +26 -0
- package/crates/tish_ffi/src/lib.rs +518 -0
- package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
- package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
- package/crates/tish_ffi/tests/loader.rs +65 -0
- package/crates/tish_fmt/src/lib.rs +61 -5
- package/crates/tish_lexer/src/lib.rs +397 -9
- package/crates/tish_lexer/src/token.rs +7 -0
- package/crates/tish_lint/src/lib.rs +2 -10
- package/crates/tish_lsp/src/import_goto.rs +2 -0
- package/crates/tish_lsp/src/main.rs +439 -26
- package/crates/tish_native/src/build.rs +55 -1
- package/crates/tish_opt/src/lib.rs +126 -23
- package/crates/tish_parser/src/lib.rs +55 -1
- package/crates/tish_parser/src/parser.rs +456 -34
- package/crates/tish_pg/src/lib.rs +3 -3
- package/crates/tish_resolve/src/lib.rs +99 -59
- package/crates/tish_runtime/Cargo.toml +4 -0
- package/crates/tish_runtime/src/http.rs +66 -17
- package/crates/tish_runtime/src/http_fetch.rs +29 -8
- package/crates/tish_runtime/src/http_hyper.rs +25 -2
- package/crates/tish_runtime/src/lib.rs +299 -44
- package/crates/tish_runtime/src/promise.rs +328 -18
- package/crates/tish_runtime/src/timers.rs +13 -7
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +35 -18
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
- package/crates/tish_ui/src/jsx.rs +10 -0
- package/crates/tish_ui/src/runtime/hooks.rs +19 -15
- package/crates/tish_ui/src/runtime/mod.rs +15 -12
- package/crates/tish_vm/Cargo.toml +14 -1
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +2 -0
- package/crates/tish_vm/src/vm.rs +1546 -202
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_wasm/src/lib.rs +6 -2
- package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
- package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
- package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
- package/justfile +8 -0
- 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,6 +16,7 @@ const RUNTIME_CARGO_FEATURES: &[&str] = &[
|
|
|
16
16
|
"process",
|
|
17
17
|
"regex",
|
|
18
18
|
"ws",
|
|
19
|
+
"tty",
|
|
19
20
|
];
|
|
20
21
|
|
|
21
22
|
/// Map CLI/compile features to flags passed to `tishlang_runtime` in the temp crate's Cargo.toml.
|
|
@@ -71,6 +72,32 @@ fn inject_generated_native_mod(rust_code: &str) -> String {
|
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
/// Whether to embed mimalloc as the `#[global_allocator]` of rust-AOT BINARY output. tish workloads
|
|
76
|
+
/// are allocation-bound (a sampling profile of object/array code spends most time in malloc/free — see
|
|
77
|
+
/// `docs/perf.md`); mimalloc gives ~20% on object/array/bundle code, the same lever as the `tish` CLI's
|
|
78
|
+
/// own `fast-alloc` and the reason JSC ships bmalloc. Default ON; `TISH_NATIVE_FAST_ALLOC=0` opts out
|
|
79
|
+
/// (e.g. a target whose C toolchain can't build mimalloc). Callers also skip it for staticlib output (a
|
|
80
|
+
/// library does not own the final program's allocator) and cross builds (avoid cross-compiling C).
|
|
81
|
+
fn fast_alloc_enabled() -> bool {
|
|
82
|
+
std::env::var("TISH_NATIVE_FAST_ALLOC")
|
|
83
|
+
.map(|v| v != "0")
|
|
84
|
+
.unwrap_or(true)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Insert a mimalloc `#[global_allocator]` into the generated crate root, after the leading
|
|
88
|
+
/// `#![allow(...)]` inner attribute (mirrors [`inject_generated_native_mod`]; an inner attribute must
|
|
89
|
+
/// precede any item, and the codegen emits exactly one — `#![allow(unused, non_snake_case)]`).
|
|
90
|
+
fn inject_global_allocator(rust_code: &str) -> String {
|
|
91
|
+
const STMT: &str =
|
|
92
|
+
"#[global_allocator]\nstatic TISH_GLOBAL_ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;\n\n";
|
|
93
|
+
if let Some(pos) = rust_code.find("\n\n") {
|
|
94
|
+
let (a, b) = rust_code.split_at(pos + 2);
|
|
95
|
+
format!("{a}{STMT}{b}")
|
|
96
|
+
} else {
|
|
97
|
+
format!("{rust_code}\n\n{STMT}")
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
74
101
|
pub(crate) fn rust_code_needs_tokio(rust_code: &str) -> bool {
|
|
75
102
|
rust_code.contains("#[tokio::main]") || rust_code.contains("tokio::runtime::Runtime")
|
|
76
103
|
}
|
|
@@ -96,6 +123,7 @@ pub fn build_via_cargo(
|
|
|
96
123
|
)
|
|
97
124
|
}
|
|
98
125
|
|
|
126
|
+
#[allow(clippy::too_many_arguments)] // orthogonal cargo build inputs; bundling would just relocate the same fields
|
|
99
127
|
pub fn build_via_cargo_with_config(
|
|
100
128
|
rust_code: &str,
|
|
101
129
|
native_modules: Vec<ResolvedNativeModule>,
|
|
@@ -140,7 +168,7 @@ pub fn build_via_cargo_with_config(
|
|
|
140
168
|
.collect();
|
|
141
169
|
|
|
142
170
|
let mut more_deps = String::new();
|
|
143
|
-
more_deps.push_str(
|
|
171
|
+
more_deps.push_str(tokio_dep);
|
|
144
172
|
if !native_deps.is_empty() {
|
|
145
173
|
more_deps.push_str(&format!("\n{}", native_deps));
|
|
146
174
|
}
|
|
@@ -154,6 +182,21 @@ pub fn build_via_cargo_with_config(
|
|
|
154
182
|
rust_code.to_string()
|
|
155
183
|
};
|
|
156
184
|
|
|
185
|
+
// mimalloc as the program's global allocator — binary output only (a staticlib does not own the
|
|
186
|
+
// allocator), native only (don't cross-compile mimalloc's C). Adds one cached dep + a global_alloc
|
|
187
|
+
// statement; semantically transparent. `TISH_NATIVE_FAST_ALLOC=0` opts out.
|
|
188
|
+
let use_fast_alloc = fast_alloc_enabled()
|
|
189
|
+
&& build_config.artifact != NativeArtifact::StaticLib
|
|
190
|
+
&& build_config.cargo_target.is_none();
|
|
191
|
+
if use_fast_alloc {
|
|
192
|
+
more_deps.push_str("\nmimalloc = \"0.1\"\n");
|
|
193
|
+
}
|
|
194
|
+
let rust_main = if use_fast_alloc {
|
|
195
|
+
inject_global_allocator(&rust_main)
|
|
196
|
+
} else {
|
|
197
|
+
rust_main
|
|
198
|
+
};
|
|
199
|
+
|
|
157
200
|
let tish_ui_path = std::path::Path::new(&runtime_path)
|
|
158
201
|
.parent()
|
|
159
202
|
.ok_or_else(|| "invalid tishlang_runtime path (no parent)".to_string())?
|
|
@@ -258,6 +301,7 @@ tishlang_runtime = {{ path = {:?}{} }}
|
|
|
258
301
|
///
|
|
259
302
|
/// `bins` order must match `outputs`: each `(stem, rust_code, generated_native_rs)` pairs with
|
|
260
303
|
/// `outputs[i].0` (entry path — used only for validation) and `outputs[i].1` (final binary path).
|
|
304
|
+
#[allow(clippy::too_many_arguments)] // orthogonal batch-build inputs (bins/outputs/modules/flags)
|
|
261
305
|
pub(crate) fn build_many_via_cargo(
|
|
262
306
|
bins: Vec<(String, String, Option<String>)>,
|
|
263
307
|
native_modules: Vec<ResolvedNativeModule>,
|
|
@@ -322,6 +366,11 @@ pub(crate) fn build_many_via_cargo(
|
|
|
322
366
|
if !extra_dependencies_toml.trim().is_empty() {
|
|
323
367
|
more_deps.push_str(&format!("\n{}", extra_dependencies_toml));
|
|
324
368
|
}
|
|
369
|
+
// mimalloc global allocator for every binary in the batch (all are executables, always native here).
|
|
370
|
+
let use_fast_alloc = fast_alloc_enabled();
|
|
371
|
+
if use_fast_alloc {
|
|
372
|
+
more_deps.push_str("\nmimalloc = \"0.1\"\n");
|
|
373
|
+
}
|
|
325
374
|
|
|
326
375
|
let tish_ui_path = std::path::Path::new(&runtime_path)
|
|
327
376
|
.parent()
|
|
@@ -346,6 +395,11 @@ pub(crate) fn build_many_via_cargo(
|
|
|
346
395
|
} else {
|
|
347
396
|
rust_code.clone()
|
|
348
397
|
};
|
|
398
|
+
let rust_main = if use_fast_alloc {
|
|
399
|
+
inject_global_allocator(&rust_main)
|
|
400
|
+
} else {
|
|
401
|
+
rust_main
|
|
402
|
+
};
|
|
349
403
|
|
|
350
404
|
fs::write(bin_dir.join("main.rs"), rust_main)
|
|
351
405
|
.map_err(|e| format!("write main.rs for {}: {}", stem, e))?;
|
|
@@ -23,6 +23,10 @@ fn optimize_statement(stmt: &Statement) -> Statement {
|
|
|
23
23
|
span: *span,
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
Statement::Multi { statements, span } => Statement::Multi {
|
|
27
|
+
statements: statements.iter().map(optimize_statement).collect(),
|
|
28
|
+
span: *span,
|
|
29
|
+
},
|
|
26
30
|
Statement::VarDecl {
|
|
27
31
|
name,
|
|
28
32
|
name_span,
|
|
@@ -444,6 +448,10 @@ fn optimize_expr(expr: &Expr) -> Expr {
|
|
|
444
448
|
operand: Box::new(optimize_expr(operand)),
|
|
445
449
|
span: *span,
|
|
446
450
|
},
|
|
451
|
+
Expr::Delete { target, span } => Expr::Delete {
|
|
452
|
+
target: Box::new(optimize_expr(target)),
|
|
453
|
+
span: *span,
|
|
454
|
+
},
|
|
447
455
|
Expr::PostfixInc { .. }
|
|
448
456
|
| Expr::PostfixDec { .. }
|
|
449
457
|
| Expr::PrefixInc { .. }
|
|
@@ -554,19 +562,70 @@ fn literal_strict_eq(a: &Literal, b: &Literal) -> bool {
|
|
|
554
562
|
}
|
|
555
563
|
}
|
|
556
564
|
|
|
565
|
+
/// JS `Number.prototype.toString` (radix 10). **Kept byte-for-byte in sync with
|
|
566
|
+
/// `tishlang_core::js_number_to_string`** so a constant-folded `"" + n` here matches the
|
|
567
|
+
/// runtime conversion there; `tish_opt` deliberately does not depend on `tish_core` (it is a
|
|
568
|
+
/// lean AST pass), hence the small duplication of this fixed-spec algorithm. See that function
|
|
569
|
+
/// for the full commentary.
|
|
570
|
+
fn js_number_to_string(value: f64) -> String {
|
|
571
|
+
if value.is_nan() {
|
|
572
|
+
return "NaN".to_string();
|
|
573
|
+
}
|
|
574
|
+
if value == f64::INFINITY {
|
|
575
|
+
return "Infinity".to_string();
|
|
576
|
+
}
|
|
577
|
+
if value == f64::NEG_INFINITY {
|
|
578
|
+
return "-Infinity".to_string();
|
|
579
|
+
}
|
|
580
|
+
if value == 0.0 {
|
|
581
|
+
return if value.is_sign_negative() { "-0" } else { "0" }.to_string();
|
|
582
|
+
}
|
|
583
|
+
let negative = value < 0.0;
|
|
584
|
+
let sci = format!("{:e}", value.abs());
|
|
585
|
+
let (mantissa, exp_str) = sci
|
|
586
|
+
.split_once('e')
|
|
587
|
+
.expect("LowerExp formatting always contains 'e'");
|
|
588
|
+
let exp: i32 = exp_str
|
|
589
|
+
.parse()
|
|
590
|
+
.expect("LowerExp exponent is a valid integer");
|
|
591
|
+
let digits: String = mantissa.chars().filter(|&c| c != '.').collect();
|
|
592
|
+
let k = digits.len() as i32;
|
|
593
|
+
let point = exp + 1;
|
|
594
|
+
|
|
595
|
+
let mut out = String::new();
|
|
596
|
+
if negative {
|
|
597
|
+
out.push('-');
|
|
598
|
+
}
|
|
599
|
+
if k <= point && point <= 21 {
|
|
600
|
+
out.push_str(&digits);
|
|
601
|
+
out.push_str(&"0".repeat((point - k) as usize));
|
|
602
|
+
} else if 0 < point && point <= 21 {
|
|
603
|
+
out.push_str(&digits[..point as usize]);
|
|
604
|
+
out.push('.');
|
|
605
|
+
out.push_str(&digits[point as usize..]);
|
|
606
|
+
} else if -6 < point && point <= 0 {
|
|
607
|
+
out.push_str("0.");
|
|
608
|
+
out.push_str(&"0".repeat((-point) as usize));
|
|
609
|
+
out.push_str(&digits);
|
|
610
|
+
} else {
|
|
611
|
+
let e = point - 1;
|
|
612
|
+
out.push_str(&digits[..1]);
|
|
613
|
+
if k > 1 {
|
|
614
|
+
out.push('.');
|
|
615
|
+
out.push_str(&digits[1..]);
|
|
616
|
+
}
|
|
617
|
+
out.push('e');
|
|
618
|
+
out.push(if e >= 0 { '+' } else { '-' });
|
|
619
|
+
out.push_str(&e.abs().to_string());
|
|
620
|
+
}
|
|
621
|
+
out
|
|
622
|
+
}
|
|
623
|
+
|
|
557
624
|
fn literal_to_display_string(lit: &Literal) -> String {
|
|
558
625
|
match lit {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
} else if *n == f64::INFINITY {
|
|
563
|
-
"Infinity".to_string()
|
|
564
|
-
} else if *n == f64::NEG_INFINITY {
|
|
565
|
-
"-Infinity".to_string()
|
|
566
|
-
} else {
|
|
567
|
-
n.to_string()
|
|
568
|
-
}
|
|
569
|
-
}
|
|
626
|
+
// Must match the runtime exactly so constant-folded `"" + n` agrees with the
|
|
627
|
+
// unfolded path (see `js_number_to_string` below).
|
|
628
|
+
Literal::Number(n) => js_number_to_string(*n),
|
|
570
629
|
Literal::String(s) => s.to_string(),
|
|
571
630
|
Literal::Bool(b) => b.to_string(),
|
|
572
631
|
Literal::Null => "null".to_string(),
|
|
@@ -708,6 +767,41 @@ fn try_algebraic_simplify(
|
|
|
708
767
|
None
|
|
709
768
|
}
|
|
710
769
|
|
|
770
|
+
// JS ToInt32/ToUint32 for the constant folder. NaN/±Infinity → 0 (`f64 as i64` saturates, so a
|
|
771
|
+
// folded `(1e308 * 10) | 0` would otherwise give -1 while the runtime gives 0). `tish_opt` has no
|
|
772
|
+
// `tish_core` dep, so these mirror `tishlang_core::to_int32`/`to_uint32` (kept in sync, pure spec).
|
|
773
|
+
#[inline]
|
|
774
|
+
fn fold_to_int32(x: f64) -> i32 {
|
|
775
|
+
if x.is_finite() {
|
|
776
|
+
x as i64 as i32
|
|
777
|
+
} else {
|
|
778
|
+
0
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
#[inline]
|
|
782
|
+
fn fold_to_uint32(x: f64) -> u32 {
|
|
783
|
+
if x.is_finite() {
|
|
784
|
+
x as i64 as u32
|
|
785
|
+
} else {
|
|
786
|
+
0
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/// Constant-fold a relational comparison (`<` `<=` `>` `>=`). Two string literals
|
|
791
|
+
/// compare lexicographically; otherwise the numeric coercions `ln`/`rn` are used.
|
|
792
|
+
/// `pred` maps the `Ordering` to a bool; a NaN-involved numeric comparison has no
|
|
793
|
+
/// ordering and is `false` — matching the VM/interp/native runtime exactly.
|
|
794
|
+
fn fold_relational<F>(left: &Literal, right: &Literal, ln: f64, rn: f64, pred: F) -> bool
|
|
795
|
+
where
|
|
796
|
+
F: FnOnce(std::cmp::Ordering) -> bool,
|
|
797
|
+
{
|
|
798
|
+
let ord = match (left, right) {
|
|
799
|
+
(Literal::String(a), Literal::String(b)) => Some(a.as_ref().cmp(b.as_ref())),
|
|
800
|
+
_ => ln.partial_cmp(&rn),
|
|
801
|
+
};
|
|
802
|
+
ord.map(pred).unwrap_or(false)
|
|
803
|
+
}
|
|
804
|
+
|
|
711
805
|
fn try_fold_binop(left: &Literal, op: BinOp, right: &Literal) -> Option<Literal> {
|
|
712
806
|
use BinOp::*;
|
|
713
807
|
let ln = literal_as_number(left);
|
|
@@ -729,24 +823,33 @@ fn try_fold_binop(left: &Literal, op: BinOp, right: &Literal) -> Option<Literal>
|
|
|
729
823
|
}
|
|
730
824
|
Sub => Literal::Number(ln - rn),
|
|
731
825
|
Mul => Literal::Number(ln * rn),
|
|
732
|
-
|
|
733
|
-
|
|
826
|
+
// IEEE division/remainder, matching JS + the VM's `eval_binop` + interp + rust-AOT:
|
|
827
|
+
// `5/0` → Infinity, `-5/0` → -Infinity, `0/0` → NaN, `5%0` → NaN. The former
|
|
828
|
+
// `if rn == 0.0 { NaN }` folded `5/0` to NaN at compile time, diverging from every runtime
|
|
829
|
+
// path (which all produce Infinity) — a constant-fold-vs-runtime inconsistency.
|
|
830
|
+
Div => Literal::Number(ln / rn),
|
|
831
|
+
Mod => Literal::Number(ln % rn),
|
|
734
832
|
Pow => Literal::Number(ln.powf(rn)),
|
|
735
833
|
Eq => Literal::Bool(literal_strict_eq(left, right)),
|
|
736
834
|
Ne => Literal::Bool(!literal_strict_eq(left, right)),
|
|
737
835
|
StrictEq => Literal::Bool(literal_strict_eq(left, right)),
|
|
738
836
|
StrictNe => Literal::Bool(!literal_strict_eq(left, right)),
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
837
|
+
// Relational ops fold lexicographically when BOTH operands are string
|
|
838
|
+
// literals (JS semantics — must match the VM/interp/native runtime), else
|
|
839
|
+
// numerically. A NaN-involved numeric comparison is always false.
|
|
840
|
+
Lt => Literal::Bool(fold_relational(left, right, ln, rn, |o| o.is_lt())),
|
|
841
|
+
Le => Literal::Bool(fold_relational(left, right, ln, rn, |o| o.is_le())),
|
|
842
|
+
Gt => Literal::Bool(fold_relational(left, right, ln, rn, |o| o.is_gt())),
|
|
843
|
+
Ge => Literal::Bool(fold_relational(left, right, ln, rn, |o| o.is_ge())),
|
|
743
844
|
And => Literal::Bool(literal_is_truthy(left) && literal_is_truthy(right)),
|
|
744
845
|
Or => Literal::Bool(literal_is_truthy(left) || literal_is_truthy(right)),
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
846
|
+
// ToInt32/ToUint32 (modulo 2³², NaN/±Infinity → 0), matching the VM/interp exactly.
|
|
847
|
+
BitAnd => Literal::Number((fold_to_int32(ln) & fold_to_int32(rn)) as f64),
|
|
848
|
+
BitOr => Literal::Number((fold_to_int32(ln) | fold_to_int32(rn)) as f64),
|
|
849
|
+
BitXor => Literal::Number((fold_to_int32(ln) ^ fold_to_int32(rn)) as f64),
|
|
850
|
+
Shl => Literal::Number(fold_to_int32(ln).wrapping_shl(fold_to_uint32(rn)) as f64),
|
|
851
|
+
Shr => Literal::Number(fold_to_int32(ln).wrapping_shr(fold_to_uint32(rn)) as f64),
|
|
852
|
+
UShr => Literal::Number(fold_to_uint32(ln).wrapping_shr(fold_to_uint32(rn)) as f64),
|
|
750
853
|
In => return None, // Requires object/array on right
|
|
751
854
|
};
|
|
752
855
|
Some(result)
|
|
@@ -936,7 +1039,7 @@ fn try_fold_unary(op: UnaryOp, operand: &Literal) -> Option<Literal> {
|
|
|
936
1039
|
Not => Literal::Bool(!literal_is_truthy(operand)),
|
|
937
1040
|
Neg => Literal::Number(-literal_as_number(operand)),
|
|
938
1041
|
Pos => Literal::Number(literal_as_number(operand)),
|
|
939
|
-
BitNot => Literal::Number(!(literal_as_number(operand)
|
|
1042
|
+
BitNot => Literal::Number(!fold_to_int32(literal_as_number(operand)) as f64),
|
|
940
1043
|
Void => Literal::Null,
|
|
941
1044
|
};
|
|
942
1045
|
Some(result)
|
|
@@ -6,9 +6,22 @@ use parser::Parser;
|
|
|
6
6
|
|
|
7
7
|
use tishlang_ast::Program;
|
|
8
8
|
use tishlang_lexer::Lexer;
|
|
9
|
+
pub use tishlang_lexer::LexerOptions;
|
|
9
10
|
|
|
11
|
+
/// Parse `source`, reading lexer options from the environment (e.g. `TISH_IGNORE_INDENT=1`
|
|
12
|
+
/// to ignore indentation syntax). Every backend funnels through here, so the env toggle
|
|
13
|
+
/// reaches run/build/dump-ast/fmt/lint/lsp uniformly.
|
|
10
14
|
pub fn parse(source: &str) -> Result<Program, String> {
|
|
11
|
-
|
|
15
|
+
parse_with_options(source, LexerOptions::from_env())
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Parse with explicit lexer options, bypassing the environment.
|
|
19
|
+
///
|
|
20
|
+
/// With `LexerOptions { ignore_indent: true }`, indentation is treated as ordinary
|
|
21
|
+
/// whitespace and blocks must be brace-delimited — useful for debugging how nested
|
|
22
|
+
/// blocks transpile, since fully brace-delimited code parses identically either way.
|
|
23
|
+
pub fn parse_with_options(source: &str, options: LexerOptions) -> Result<Program, String> {
|
|
24
|
+
let lexer = Lexer::with_options(source, options);
|
|
12
25
|
let tokens: Result<Vec<_>, _> = lexer.collect();
|
|
13
26
|
let tokens = tokens?;
|
|
14
27
|
let mut parser = Parser::new(&tokens);
|
|
@@ -329,4 +342,45 @@ mod tests {
|
|
|
329
342
|
assert!(matches!(stmts[1], Statement::VarDecl { .. }));
|
|
330
343
|
assert!(matches!(stmts[2], Statement::If { .. }));
|
|
331
344
|
}
|
|
345
|
+
|
|
346
|
+
#[test]
|
|
347
|
+
fn ignore_indent_parses_brace_blocks_identically() {
|
|
348
|
+
// Fully brace-delimited code: braces are authoritative, indentation is decoration.
|
|
349
|
+
// Ignoring indentation must therefore produce an identical AST.
|
|
350
|
+
let src = "fn f() {\n let a = 1\n if (a) {\n let b = 2\n g(b)\n }\n}\n";
|
|
351
|
+
let normal = parse(src).expect("parse (indentation significant)");
|
|
352
|
+
let ignored = parse_with_options(src, LexerOptions { ignore_indent: true })
|
|
353
|
+
.expect("parse (indentation ignored)");
|
|
354
|
+
assert_eq!(
|
|
355
|
+
format!("{normal:#?}"),
|
|
356
|
+
format!("{ignored:#?}"),
|
|
357
|
+
"brace-delimited code must parse identically with indentation ignored"
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
#[test]
|
|
362
|
+
fn ignore_indent_drops_indentation_induced_block() {
|
|
363
|
+
// A leading-indented line makes the lexer open an indent level, so the parser wraps
|
|
364
|
+
// `a()` in a `Block` — the kind of stray, indentation-driven nesting that can give
|
|
365
|
+
// transpiled JS the wrong lexical scope. Ignoring indentation removes that wrapper.
|
|
366
|
+
let src = " a()\nb()\n";
|
|
367
|
+
|
|
368
|
+
let normal = parse(src).expect("parse normal");
|
|
369
|
+
assert!(
|
|
370
|
+
matches!(normal.statements.first(), Some(Statement::Block { .. })),
|
|
371
|
+
"indentation should wrap a() in a Block, got: {:?}",
|
|
372
|
+
normal.statements
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
let ignored = parse_with_options(src, LexerOptions { ignore_indent: true })
|
|
376
|
+
.expect("parse ignored");
|
|
377
|
+
assert!(
|
|
378
|
+
ignored
|
|
379
|
+
.statements
|
|
380
|
+
.iter()
|
|
381
|
+
.all(|s| matches!(s, Statement::ExprStmt { .. })),
|
|
382
|
+
"with indentation ignored, both calls are flat expression statements, got: {:?}",
|
|
383
|
+
ignored.statements
|
|
384
|
+
);
|
|
385
|
+
}
|
|
332
386
|
}
|