@tishlang/tish-lsp 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.
Files changed (42) hide show
  1. package/Cargo.toml +1 -0
  2. package/bin/tish-lsp +0 -0
  3. package/crates/tish/Cargo.toml +4 -1
  4. package/crates/tish/src/cli_help.rs +84 -0
  5. package/crates/tish/src/main.rs +93 -1
  6. package/crates/tish/tests/scope_merge.rs +90 -0
  7. package/crates/tish/tests/test_runner.rs +673 -0
  8. package/crates/tish_bytecode/src/compiler.rs +25 -4
  9. package/crates/tish_compile/src/check.rs +178 -0
  10. package/crates/tish_compile/src/codegen.rs +383 -63
  11. package/crates/tish_compile/src/lib.rs +33 -1
  12. package/crates/tish_compile/src/resolve.rs +121 -19
  13. package/crates/tish_compile/src/types.rs +35 -1
  14. package/crates/tish_compile/tests/call_arity_641.rs +104 -0
  15. package/crates/tish_core/src/lib.rs +103 -11
  16. package/crates/tish_eval/src/eval.rs +16 -0
  17. package/crates/tish_eval/src/value_convert.rs +10 -0
  18. package/crates/tish_lsp/Cargo.toml +1 -1
  19. package/crates/tish_test/Cargo.toml +47 -0
  20. package/crates/tish_test/src/assert.rs +435 -0
  21. package/crates/tish_test/src/config.rs +85 -0
  22. package/crates/tish_test/src/coverage.rs +237 -0
  23. package/crates/tish_test/src/deep_equal.rs +261 -0
  24. package/crates/tish_test/src/discovery.rs +70 -0
  25. package/crates/tish_test/src/expect.rs +977 -0
  26. package/crates/tish_test/src/instrument.rs +457 -0
  27. package/crates/tish_test/src/isolation.rs +14 -0
  28. package/crates/tish_test/src/lib.rs +22 -0
  29. package/crates/tish_test/src/load.rs +152 -0
  30. package/crates/tish_test/src/mocks.rs +270 -0
  31. package/crates/tish_test/src/module.rs +318 -0
  32. package/crates/tish_test/src/registry.rs +235 -0
  33. package/crates/tish_test/src/report.rs +312 -0
  34. package/crates/tish_test/src/runner.rs +797 -0
  35. package/crates/tish_test/src/snapshots.rs +197 -0
  36. package/crates/tish_vm/src/vm.rs +17 -14
  37. package/package.json +1 -1
  38. package/platform/darwin-arm64/tish-lsp +0 -0
  39. package/platform/darwin-x64/tish-lsp +0 -0
  40. package/platform/linux-arm64/tish-lsp +0 -0
  41. package/platform/linux-x64/tish-lsp +0 -0
  42. package/platform/win32-x64/tish-lsp.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.compile_statement(body)?;
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.compile_statement(body)?;
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(&param_names[..formal_len], &slots)?;
2023
2044
  inner_comp.emit_param_defaults_prologue(params)?;
2024
- inner_comp.compile_statement(body)?;
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.compile_statement(s)?;
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
+ }