@tishlang/tish-lint 3.3.3 → 3.7.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 +1 -0
- package/bin/tish-lint +0 -0
- package/crates/tish/Cargo.toml +4 -1
- package/crates/tish/src/cli_help.rs +84 -0
- package/crates/tish/src/main.rs +93 -1
- package/crates/tish/tests/scope_merge.rs +90 -0
- package/crates/tish/tests/test_runner.rs +673 -0
- package/crates/tish_bytecode/src/compiler.rs +25 -4
- package/crates/tish_compile/src/check.rs +178 -0
- package/crates/tish_compile/src/codegen.rs +383 -63
- package/crates/tish_compile/src/lib.rs +33 -1
- package/crates/tish_compile/src/resolve.rs +121 -19
- package/crates/tish_compile/src/types.rs +35 -1
- package/crates/tish_compile/tests/call_arity_641.rs +104 -0
- package/crates/tish_core/src/lib.rs +103 -11
- package/crates/tish_eval/src/eval.rs +16 -0
- package/crates/tish_eval/src/value_convert.rs +10 -0
- package/crates/tish_lint/Cargo.toml +1 -1
- package/crates/tish_test/Cargo.toml +47 -0
- package/crates/tish_test/src/assert.rs +435 -0
- package/crates/tish_test/src/config.rs +85 -0
- package/crates/tish_test/src/coverage.rs +237 -0
- package/crates/tish_test/src/deep_equal.rs +261 -0
- package/crates/tish_test/src/discovery.rs +70 -0
- package/crates/tish_test/src/expect.rs +977 -0
- package/crates/tish_test/src/instrument.rs +457 -0
- package/crates/tish_test/src/isolation.rs +14 -0
- package/crates/tish_test/src/lib.rs +22 -0
- package/crates/tish_test/src/load.rs +152 -0
- package/crates/tish_test/src/mocks.rs +270 -0
- package/crates/tish_test/src/module.rs +318 -0
- package/crates/tish_test/src/registry.rs +235 -0
- package/crates/tish_test/src/report.rs +312 -0
- package/crates/tish_test/src/runner.rs +797 -0
- package/crates/tish_test/src/snapshots.rs +197 -0
- package/crates/tish_vm/src/vm.rs +17 -14
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lint +0 -0
- package/platform/darwin-x64/tish-lint +0 -0
- package/platform/linux-arm64/tish-lint +0 -0
- package/platform/linux-x64/tish-lint +0 -0
- package/platform/win32-x64/tish-lint.exe +0 -0
|
@@ -1521,6 +1521,27 @@ impl<'a> Compiler<'a> {
|
|
|
1521
1521
|
Ok(())
|
|
1522
1522
|
}
|
|
1523
1523
|
|
|
1524
|
+
/// Compile a function/arrow body **without** the outermost block's `EnterBlock`/`ExitBlock`.
|
|
1525
|
+
///
|
|
1526
|
+
/// A function body's top-level block is the frame's own scope: its bindings live in the
|
|
1527
|
+
/// frame-local map that is created fresh per call and dropped when the call returns, so there
|
|
1528
|
+
/// is nothing for `ExitBlock` to restore or hide. Emitting it is not merely wasted work — it is
|
|
1529
|
+
/// actively wrong. `ExitBlock` REMOVES the block's names from that map, and closures capture
|
|
1530
|
+
/// the map **by reference**, so any closure that outlived the call lost its captures
|
|
1531
|
+
/// ("Undefined variable"). An explicit `return` masked the bug by jumping over the teardown.
|
|
1532
|
+
/// See tests/core/parity_block_scope_capture.tish (#247).
|
|
1533
|
+
fn compile_fn_body(&mut self, body: &Statement) -> Result<(), CompileError> {
|
|
1534
|
+
if let Statement::Block { statements, .. } = body {
|
|
1535
|
+
self.enter_block_scope();
|
|
1536
|
+
for stmt in statements {
|
|
1537
|
+
self.compile_statement(stmt)?;
|
|
1538
|
+
}
|
|
1539
|
+
self.exit_block_scope();
|
|
1540
|
+
return Ok(());
|
|
1541
|
+
}
|
|
1542
|
+
self.compile_statement(body)
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1524
1545
|
fn compile_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
|
|
1525
1546
|
self.mark_line(stmt.span());
|
|
1526
1547
|
match stmt {
|
|
@@ -1999,7 +2020,7 @@ impl<'a> Compiler<'a> {
|
|
|
1999
2020
|
if let Some(map) = simple_slots {
|
|
2000
2021
|
inner_comp.slot_ctx = Some(map);
|
|
2001
2022
|
inner_comp.emit_param_defaults_prologue(params)?;
|
|
2002
|
-
inner_comp.
|
|
2023
|
+
inner_comp.compile_fn_body(body)?;
|
|
2003
2024
|
} else if let Some(cap) = captured {
|
|
2004
2025
|
// Params (all uncaptured — gated) → slots 0..n (matching the VM's param binding);
|
|
2005
2026
|
// uncaptured body `let`s get fresh slots via the scope-aware allocator; captured
|
|
@@ -2011,7 +2032,7 @@ impl<'a> Compiler<'a> {
|
|
|
2011
2032
|
inner_comp.declare_slot(p);
|
|
2012
2033
|
}
|
|
2013
2034
|
inner_comp.emit_param_defaults_prologue(params)?;
|
|
2014
|
-
inner_comp.
|
|
2035
|
+
inner_comp.compile_fn_body(body)?;
|
|
2015
2036
|
general_frame_slots = Some(inner_comp.next_slot);
|
|
2016
2037
|
} else {
|
|
2017
2038
|
inner_comp.scope = vec![param_names
|
|
@@ -2021,7 +2042,7 @@ impl<'a> Compiler<'a> {
|
|
|
2021
2042
|
inner_comp
|
|
2022
2043
|
.emit_param_destructure_prologue(¶m_names[..formal_len], &slots)?;
|
|
2023
2044
|
inner_comp.emit_param_defaults_prologue(params)?;
|
|
2024
|
-
inner_comp.
|
|
2045
|
+
inner_comp.compile_fn_body(body)?;
|
|
2025
2046
|
}
|
|
2026
2047
|
inner_comp.emit(Opcode::LoadConst);
|
|
2027
2048
|
let idx = inner_comp.constant_idx(Constant::Null);
|
|
@@ -2769,7 +2790,7 @@ impl<'a> Compiler<'a> {
|
|
|
2769
2790
|
inner_comp.emit(Opcode::Return);
|
|
2770
2791
|
}
|
|
2771
2792
|
ArrowBody::Block(s) => {
|
|
2772
|
-
inner_comp.
|
|
2793
|
+
inner_comp.compile_fn_body(s)?;
|
|
2773
2794
|
let idx = inner_comp.constant_idx(Constant::Null);
|
|
2774
2795
|
inner_comp.emit(Opcode::LoadConst);
|
|
2775
2796
|
inner_comp.chunk.write_u16(idx);
|
|
@@ -773,3 +773,181 @@ mod tests {
|
|
|
773
773
|
assert_eq!(diags("fn f(a: number) {}\nf(someUnknown())"), Vec::<String>::new());
|
|
774
774
|
}
|
|
775
775
|
}
|
|
776
|
+
|
|
777
|
+
// ── #641: call arity ────────────────────────────────────────────────────────────────────────────
|
|
778
|
+
//
|
|
779
|
+
// tish did not check arity at all. A call with too few arguments compiled, the missing parameter
|
|
780
|
+
// became `Value::Null`, and what happened next depended on which lowering the CALLEE happened to
|
|
781
|
+
// get — a property of the callee, not of the call:
|
|
782
|
+
//
|
|
783
|
+
// boxed `Value::native` + typed params -> builds clean, PANICS at runtime the first time that
|
|
784
|
+
// path runs ("expected number", pointing at generated
|
|
785
|
+
// code, saying nothing about the call site)
|
|
786
|
+
// direct native `fn` + typed params -> rustc E0061 against generated code
|
|
787
|
+
// interpreter + typed params -> silently returns NaN / null
|
|
788
|
+
// untyped params -> `null` arrives; a deliberate, widely-used idiom
|
|
789
|
+
//
|
|
790
|
+
// So the same mistake is a build error in one function and a shipped crash in the next.
|
|
791
|
+
//
|
|
792
|
+
// THE RULE, and why it is exactly this narrow: an omitted argument bound to a TYPED parameter can
|
|
793
|
+
// never be correct, because `Value::Null` cannot satisfy a `Value::Number` prologue. That is a fact
|
|
794
|
+
// about the lowering, not a style preference, so it can be a hard error without a policy debate.
|
|
795
|
+
//
|
|
796
|
+
// An omitted argument bound to an UNTYPED parameter stays legal, because it is an established
|
|
797
|
+
// idiom in shipping code — `packages/shop.tish` has six such calls, `packages/ui.tish` one:
|
|
798
|
+
//
|
|
799
|
+
// function renderTab(defer, stream) { if (present(defer) === 1) { … } }
|
|
800
|
+
// renderTab() // deliberate: "render now, no defer, no stream"
|
|
801
|
+
//
|
|
802
|
+
// A parameter with a DEFAULT imposes no minimum either: the default is what fills it. Rest params
|
|
803
|
+
// impose none by definition.
|
|
804
|
+
|
|
805
|
+
/// The minimum argument count a call to `params` must supply: one past the LAST parameter that is
|
|
806
|
+
/// typed and has no default. A typed parameter at index 2 forces arity 3 even when 0 and 1 are
|
|
807
|
+
/// untyped, because the call cannot skip a position to reach it.
|
|
808
|
+
fn min_required_arity(params: &[FunParam]) -> usize {
|
|
809
|
+
let mut min = 0;
|
|
810
|
+
for (i, p) in params.iter().enumerate() {
|
|
811
|
+
if let FunParam::Simple(tp) = p {
|
|
812
|
+
if tp.type_ann.is_some() && tp.default.is_none() {
|
|
813
|
+
min = i + 1;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
min
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/// #641: report every call that supplies fewer arguments than a typed parameter requires. Runs
|
|
821
|
+
/// unconditionally in the compile path — unlike [`check_program`], which is opt-in behind
|
|
822
|
+
/// `TISH_CHECK` and would therefore never have caught the shipped case this exists for.
|
|
823
|
+
pub fn check_call_arity(program: &Program) -> Vec<TypeDiagnostic> {
|
|
824
|
+
use std::collections::HashMap;
|
|
825
|
+
|
|
826
|
+
// name -> (minimum arity, declared parameter count). Nested and top-level fns alike; a later
|
|
827
|
+
// declaration of the same name wins, matching the runtime's own last-wins binding.
|
|
828
|
+
let mut sigs: HashMap<String, (usize, usize)> = HashMap::new();
|
|
829
|
+
fn collect(stmts: &[Statement], sigs: &mut HashMap<String, (usize, usize)>) {
|
|
830
|
+
for s in stmts {
|
|
831
|
+
if let Statement::FunDecl { name, params, rest_param, body, .. } = s {
|
|
832
|
+
// A rest param swallows any surplus, but the leading typed params still bind
|
|
833
|
+
// positionally, so the minimum is unchanged.
|
|
834
|
+
let _ = rest_param;
|
|
835
|
+
sigs.insert(name.to_string(), (min_required_arity(params), params.len()));
|
|
836
|
+
collect(std::slice::from_ref(body.as_ref()), sigs);
|
|
837
|
+
} else {
|
|
838
|
+
for_each_child_block(s, &mut |b| collect(b, sigs));
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
collect(&program.statements, &mut sigs);
|
|
843
|
+
|
|
844
|
+
let mut diags = Vec::new();
|
|
845
|
+
for s in &program.statements {
|
|
846
|
+
crate::codegen::Codegen::for_each_stmt_expr(s, &mut |root| {
|
|
847
|
+
walk_expr(root, &mut |e| {
|
|
848
|
+
let Expr::Call { callee, args, span } = e else { return };
|
|
849
|
+
let Expr::Ident { name, .. } = callee.as_ref() else { return };
|
|
850
|
+
let Some((min, declared)) = sigs.get(name.as_ref()).copied() else { return };
|
|
851
|
+
// A spread makes the count unknowable at compile time — say nothing rather than guess.
|
|
852
|
+
if args.iter().any(|a| matches!(a, CallArg::Spread(_))) {
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
if args.len() < min {
|
|
856
|
+
diags.push(TypeDiagnostic {
|
|
857
|
+
message: format!(
|
|
858
|
+
"'{}' needs {} argument(s) but {} were supplied; parameter {} is typed, and \
|
|
859
|
+
an omitted argument arrives as null, which a typed parameter cannot accept. \
|
|
860
|
+
(An untyped parameter may be omitted — that idiom is unaffected.)",
|
|
861
|
+
name,
|
|
862
|
+
min,
|
|
863
|
+
args.len(),
|
|
864
|
+
min,
|
|
865
|
+
),
|
|
866
|
+
span: *span,
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
let _ = declared;
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
diags
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/// Visit `e` and every sub-expression. `for_each_stmt_expr` hands out only a statement's ROOT
|
|
877
|
+
/// expression, so without this the checker never sees `paintTyped(1)` inside `log("x" +
|
|
878
|
+
/// paintTyped(1))` — which is the shape the reported bug actually had.
|
|
879
|
+
///
|
|
880
|
+
/// The catch-all arm is deliberate: a variant not enumerated here yields a MISSED diagnostic, never
|
|
881
|
+
/// a wrong one, so a future AST addition degrades this check quietly instead of breaking builds.
|
|
882
|
+
fn walk_expr(e: &Expr, f: &mut dyn FnMut(&Expr)) {
|
|
883
|
+
f(e);
|
|
884
|
+
let mut go = |x: &Expr| walk_expr(x, f);
|
|
885
|
+
match e {
|
|
886
|
+
Expr::Binary { left, right, .. }
|
|
887
|
+
| Expr::NullishCoalesce { left, right, .. }
|
|
888
|
+
| Expr::Index { object: left, index: right, .. } => {
|
|
889
|
+
go(left);
|
|
890
|
+
go(right);
|
|
891
|
+
}
|
|
892
|
+
Expr::Unary { operand, .. } | Expr::TypeOf { operand, .. } | Expr::Await { operand, .. } => {
|
|
893
|
+
go(operand)
|
|
894
|
+
}
|
|
895
|
+
// The inc/dec forms name a variable rather than holding a sub-expression, so there is
|
|
896
|
+
// nothing to descend into.
|
|
897
|
+
Expr::Delete { target, .. } => go(target),
|
|
898
|
+
Expr::Member { object, .. } => go(object),
|
|
899
|
+
Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } => {
|
|
900
|
+
go(callee);
|
|
901
|
+
for a in args {
|
|
902
|
+
match a {
|
|
903
|
+
CallArg::Expr(x) | CallArg::Spread(x) => go(x),
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
Expr::Conditional { cond, then_branch, else_branch, .. } => {
|
|
908
|
+
go(cond);
|
|
909
|
+
go(then_branch);
|
|
910
|
+
go(else_branch);
|
|
911
|
+
}
|
|
912
|
+
Expr::Array { elements, .. } => {
|
|
913
|
+
for el in elements {
|
|
914
|
+
match el {
|
|
915
|
+
tishlang_ast::ArrayElement::Expr(x)
|
|
916
|
+
| tishlang_ast::ArrayElement::Spread(x) => go(x),
|
|
917
|
+
_ => {}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
Expr::Object { props, .. } => {
|
|
922
|
+
for pr in props {
|
|
923
|
+
match pr {
|
|
924
|
+
tishlang_ast::ObjectProp::KeyValue(_, x, _) => go(x),
|
|
925
|
+
tishlang_ast::ObjectProp::Spread(x) => go(x),
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
Expr::Assign { value, .. }
|
|
930
|
+
| Expr::CompoundAssign { value, .. }
|
|
931
|
+
| Expr::LogicalAssign { value, .. } => go(value),
|
|
932
|
+
Expr::MemberAssign { object, value, .. } => {
|
|
933
|
+
go(object);
|
|
934
|
+
go(value);
|
|
935
|
+
}
|
|
936
|
+
Expr::IndexAssign { object, index, value, .. } => {
|
|
937
|
+
go(object);
|
|
938
|
+
go(index);
|
|
939
|
+
go(value);
|
|
940
|
+
}
|
|
941
|
+
Expr::TemplateLiteral { exprs, .. } => {
|
|
942
|
+
for x in exprs {
|
|
943
|
+
go(x);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
Expr::ArrowFunction { body, .. } => {
|
|
947
|
+
if let tishlang_ast::ArrowBody::Expr(x) = body {
|
|
948
|
+
go(x.as_ref());
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
_ => {}
|
|
952
|
+
}
|
|
953
|
+
}
|