@tishlang/tish 1.13.2 → 2.0.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/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 +43 -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
|
@@ -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
|
}
|