@tishlang/tish 1.13.1 → 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.
Files changed (106) hide show
  1. package/Cargo.toml +2 -0
  2. package/bin/tish +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +1 -0
  4. package/crates/tish/Cargo.toml +11 -3
  5. package/crates/tish/build.rs +21 -0
  6. package/crates/tish/src/cli_help.rs +15 -4
  7. package/crates/tish/src/main.rs +93 -21
  8. package/crates/tish/src/repl_completion.rs +0 -1
  9. package/crates/tish/tests/error_source_location.rs +36 -0
  10. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  11. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  12. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  13. package/crates/tish/tests/integration_test.rs +402 -91
  14. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  15. package/crates/tish/tests/tty_capability.rs +43 -0
  16. package/crates/tish_ast/src/ast.rs +37 -8
  17. package/crates/tish_builtins/Cargo.toml +2 -0
  18. package/crates/tish_builtins/src/array.rs +375 -13
  19. package/crates/tish_builtins/src/collections.rs +481 -0
  20. package/crates/tish_builtins/src/construct.rs +59 -19
  21. package/crates/tish_builtins/src/date.rs +538 -0
  22. package/crates/tish_builtins/src/globals.rs +86 -6
  23. package/crates/tish_builtins/src/iterator.rs +129 -0
  24. package/crates/tish_builtins/src/lib.rs +5 -0
  25. package/crates/tish_builtins/src/number.rs +96 -0
  26. package/crates/tish_builtins/src/object.rs +2 -2
  27. package/crates/tish_builtins/src/string.rs +19 -20
  28. package/crates/tish_builtins/src/symbol.rs +1 -1
  29. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  30. package/crates/tish_bytecode/src/chunk.rs +69 -1
  31. package/crates/tish_bytecode/src/compiler.rs +933 -89
  32. package/crates/tish_bytecode/src/encoding.rs +2 -0
  33. package/crates/tish_bytecode/src/lib.rs +2 -1
  34. package/crates/tish_bytecode/src/opcode.rs +47 -4
  35. package/crates/tish_bytecode/src/serialize.rs +31 -1
  36. package/crates/tish_compile/Cargo.toml +1 -0
  37. package/crates/tish_compile/src/check.rs +774 -0
  38. package/crates/tish_compile/src/codegen.rs +2334 -349
  39. package/crates/tish_compile/src/infer.rs +1395 -6
  40. package/crates/tish_compile/src/lib.rs +50 -8
  41. package/crates/tish_compile/src/resolve.rs +584 -21
  42. package/crates/tish_compile/src/types.rs +106 -2
  43. package/crates/tish_compile_js/src/codegen.rs +67 -0
  44. package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
  45. package/crates/tish_core/Cargo.toml +7 -1
  46. package/crates/tish_core/src/console_style.rs +11 -1
  47. package/crates/tish_core/src/json.rs +81 -38
  48. package/crates/tish_core/src/lib.rs +3 -0
  49. package/crates/tish_core/src/shape.rs +85 -0
  50. package/crates/tish_core/src/value.rs +679 -25
  51. package/crates/tish_core/src/vmref.rs +13 -8
  52. package/crates/tish_cranelift/src/link.rs +17 -4
  53. package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
  54. package/crates/tish_eval/Cargo.toml +6 -0
  55. package/crates/tish_eval/src/eval.rs +665 -117
  56. package/crates/tish_eval/src/http.rs +4 -1
  57. package/crates/tish_eval/src/natives.rs +165 -13
  58. package/crates/tish_eval/src/value.rs +31 -13
  59. package/crates/tish_eval/src/value_convert.rs +10 -4
  60. package/crates/tish_ffi/Cargo.toml +26 -0
  61. package/crates/tish_ffi/src/lib.rs +518 -0
  62. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  63. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  64. package/crates/tish_ffi/tests/loader.rs +65 -0
  65. package/crates/tish_fmt/src/lib.rs +43 -5
  66. package/crates/tish_lexer/src/lib.rs +397 -9
  67. package/crates/tish_lexer/src/token.rs +7 -0
  68. package/crates/tish_lint/src/lib.rs +2 -10
  69. package/crates/tish_lsp/src/import_goto.rs +2 -0
  70. package/crates/tish_lsp/src/main.rs +439 -26
  71. package/crates/tish_native/src/build.rs +55 -1
  72. package/crates/tish_opt/src/lib.rs +126 -23
  73. package/crates/tish_parser/src/lib.rs +55 -1
  74. package/crates/tish_parser/src/parser.rs +456 -34
  75. package/crates/tish_pg/src/lib.rs +3 -3
  76. package/crates/tish_resolve/src/lib.rs +99 -59
  77. package/crates/tish_runtime/Cargo.toml +4 -0
  78. package/crates/tish_runtime/src/http.rs +66 -17
  79. package/crates/tish_runtime/src/http_fetch.rs +29 -8
  80. package/crates/tish_runtime/src/http_hyper.rs +25 -2
  81. package/crates/tish_runtime/src/lib.rs +299 -44
  82. package/crates/tish_runtime/src/promise.rs +328 -18
  83. package/crates/tish_runtime/src/timers.rs +13 -7
  84. package/crates/tish_runtime/src/tty.rs +226 -0
  85. package/crates/tish_runtime/src/ws.rs +35 -18
  86. package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
  87. package/crates/tish_ui/src/jsx.rs +10 -0
  88. package/crates/tish_ui/src/runtime/hooks.rs +19 -15
  89. package/crates/tish_ui/src/runtime/mod.rs +15 -12
  90. package/crates/tish_vm/Cargo.toml +14 -1
  91. package/crates/tish_vm/src/jit.rs +1050 -0
  92. package/crates/tish_vm/src/lib.rs +2 -0
  93. package/crates/tish_vm/src/vm.rs +1546 -202
  94. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  95. package/crates/tish_wasm/src/lib.rs +6 -2
  96. package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
  97. package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
  98. package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
  99. package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
  100. package/justfile +8 -0
  101. package/package.json +1 -1
  102. package/platform/darwin-arm64/tish +0 -0
  103. package/platform/darwin-x64/tish +0 -0
  104. package/platform/linux-arm64/tish +0 -0
  105. package/platform/linux-x64/tish +0 -0
  106. package/platform/win32-x64/tish.exe +0 -0
@@ -121,7 +121,7 @@ fn document_symbol(
121
121
  }
122
122
  }
123
123
 
124
- fn publish_parse_and_lint(client: &Client, uri: Url, text: &str) {
124
+ async fn publish_parse_and_lint(client: &Client, uri: Url, text: &str) {
125
125
  let mut diags = Vec::new();
126
126
  match tishlang_parser::parse(text) {
127
127
  Ok(program) => {
@@ -172,6 +172,17 @@ fn publish_parse_and_lint(client: &Client, uri: Url, text: &str) {
172
172
  ..Default::default()
173
173
  });
174
174
  }
175
+ // Gradual type checker (Phase 2): surface provable annotation violations as warnings.
176
+ for d in tishlang_compile::check_program(&program) {
177
+ diags.push(Diagnostic {
178
+ range: span_to_range(&d.span, text),
179
+ severity: Some(DiagnosticSeverity::WARNING),
180
+ code: Some(NumberOrString::String("tish-type".into())),
181
+ message: d.message,
182
+ source: Some("tish".into()),
183
+ ..Default::default()
184
+ });
185
+ }
175
186
  }
176
187
  Err(e) => {
177
188
  let (l, c) = parse_error_pos(&e);
@@ -183,7 +194,9 @@ fn publish_parse_and_lint(client: &Client, uri: Url, text: &str) {
183
194
  });
184
195
  }
185
196
  }
186
- let _ = client.publish_diagnostics(uri, diags, None);
197
+ // MUST be awaited — `publish_diagnostics` is async; a bare `let _ = …` drops the future
198
+ // unsent, which silently disables ALL LSP diagnostics (parse errors, lints, unused bindings).
199
+ client.publish_diagnostics(uri, diags, None).await;
187
200
  }
188
201
 
189
202
  #[tower_lsp::async_trait]
@@ -268,7 +281,7 @@ impl LanguageServer for Backend {
268
281
  let uri = p.text_document.uri;
269
282
  let text = p.text_document.text;
270
283
  self.docs.write().unwrap().insert(uri.clone(), text.clone());
271
- publish_parse_and_lint(&self.client, uri, &text);
284
+ publish_parse_and_lint(&self.client, uri, &text).await;
272
285
  }
273
286
 
274
287
  async fn did_change(&self, p: DidChangeTextDocumentParams) {
@@ -278,15 +291,15 @@ impl LanguageServer for Backend {
278
291
  .write()
279
292
  .unwrap()
280
293
  .insert(uri.clone(), chg.text.clone());
281
- publish_parse_and_lint(&self.client, uri, &chg.text);
294
+ publish_parse_and_lint(&self.client, uri, &chg.text).await;
282
295
  }
283
296
  }
284
297
 
285
298
  async fn did_close(&self, p: DidCloseTextDocumentParams) {
286
299
  self.docs.write().unwrap().remove(&p.text_document.uri);
287
- let _ = self
288
- .client
289
- .publish_diagnostics(p.text_document.uri, vec![], None);
300
+ self.client
301
+ .publish_diagnostics(p.text_document.uri, vec![], None)
302
+ .await;
290
303
  }
291
304
 
292
305
  async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
@@ -399,7 +412,17 @@ impl LanguageServer for Backend {
399
412
  return Ok(None);
400
413
  }
401
414
 
402
- if let Some(ref file_path) = uri.to_file_path().ok() {
415
+ // Type reference (`: SomeType`, `extends SomeType`, `as SomeType`) → jump to its
416
+ // `type`/`interface` declaration. Value bindings are resolved above, so this only
417
+ // fires for genuine type names.
418
+ if let Some(sp) = type_decl_span(&program, word.as_str()) {
419
+ return Ok(Some(GotoDefinitionResponse::Scalar(Location {
420
+ uri: uri.clone(),
421
+ range: span_to_range(&sp, &text),
422
+ })));
423
+ }
424
+
425
+ if let Ok(ref file_path) = uri.to_file_path() {
403
426
  let roots = self.roots.read().unwrap().clone();
404
427
  if let Some(loc) = import_goto::definition_for_import(
405
428
  &program,
@@ -456,10 +479,29 @@ impl LanguageServer for Backend {
456
479
  let Some(use_site) =
457
480
  tishlang_resolve::name_at_cursor(&program, &text, pos.line, pos.character)
458
481
  else {
482
+ // Not a value name at the cursor — it may be a type reference (`: SomeType`,
483
+ // `extends SomeType`). Type annotations carry no spans, so match by word.
484
+ let word = word_at_position(&text, pos);
485
+ if let Some(ty) = type_alias_body(&program, &word) {
486
+ let value = format!("**`{}`**{}", word, code_hint(&format!("type {} = {}", word, ty)));
487
+ return Ok(Some(Hover {
488
+ range: None,
489
+ contents: HoverContents::Markup(MarkupContent {
490
+ kind: MarkupKind::Markdown,
491
+ value,
492
+ }),
493
+ }));
494
+ }
459
495
  return Ok(None);
460
496
  };
461
497
  let def = tishlang_resolve::definition_span(&program, &text, pos.line, pos.character);
462
498
  let mut md = format!("**`{}`**", use_site.name);
499
+ // Type-aware hover: show the declared (or simply-inferred) type / fn signature.
500
+ if let Some(ref dspan) = def {
501
+ if let Some(hint) = type_hint_at_def(&program, dspan) {
502
+ md.push_str(&hint);
503
+ }
504
+ }
463
505
  match def {
464
506
  Some(def) if def.start == use_site.span.start && def.end == use_site.span.end => {
465
507
  md.push_str("\n\n_(binding site)_");
@@ -789,13 +831,10 @@ pub(crate) fn find_export(
789
831
  range: span_to_range(name_span, text),
790
832
  });
791
833
  }
792
- tishlang_ast::Statement::Export { declaration, .. } => match declaration.as_ref() {
793
- tishlang_ast::ExportDeclaration::Named(inner) => {
794
- if let Some(loc) = find_decl_in_stmt(inner, name, uri, text) {
795
- return Some(loc);
796
- }
834
+ tishlang_ast::Statement::Export { declaration, .. } => if let tishlang_ast::ExportDeclaration::Named(inner) = declaration.as_ref() {
835
+ if let Some(loc) = find_decl_in_stmt(inner, name, uri, text) {
836
+ return Some(loc);
797
837
  }
798
- _ => {}
799
838
  },
800
839
  _ => {}
801
840
  }
@@ -856,27 +895,263 @@ fn span_to_range(span: &tishlang_ast::Span, text: &str) -> Range {
856
895
 
857
896
  fn word_at_position(text: &str, position: Position) -> String {
858
897
  let line = text.lines().nth(position.line as usize).unwrap_or("");
859
- let col = position.character as usize;
860
- let bytes: Vec<(usize, char)> = line.char_indices().collect();
861
- let mut start = col.min(bytes.len().saturating_sub(1));
862
- while start > 0 && !is_ident_char(bytes.get(start).map(|(_, c)| *c).unwrap_or(' ')) {
863
- start = start.saturating_sub(1);
898
+ let chars: Vec<(usize, char)> = line.char_indices().collect();
899
+ let col = (position.character as usize).min(chars.len());
900
+ // Pick the identifier the cursor is on. If the cursor sits just past a word's end
901
+ // (on whitespace/punct or EOL), fall back to the identifier immediately to its left.
902
+ let mut start = col;
903
+ if start >= chars.len() || !is_ident_char(chars[start].1) {
904
+ if start == 0 || !is_ident_char(chars[start - 1].1) {
905
+ return String::new();
906
+ }
907
+ start -= 1;
864
908
  }
865
- let mut i = start;
866
- while i < bytes.len() && is_ident_char(bytes[i].1) {
867
- i += 1;
909
+ // Scan left to the word start, then right to the word end (the original missed the prefix
910
+ // when the cursor landed in the middle of a word).
911
+ while start > 0 && is_ident_char(chars[start - 1].1) {
912
+ start -= 1;
868
913
  }
869
- if start < bytes.len() {
870
- line[bytes[start].0..bytes.get(i).map(|(p, _)| *p).unwrap_or(line.len())].to_string()
871
- } else {
872
- String::new()
914
+ let mut end = start;
915
+ while end < chars.len() && is_ident_char(chars[end].1) {
916
+ end += 1;
873
917
  }
918
+ let s = chars[start].0;
919
+ let e = chars.get(end).map(|(p, _)| *p).unwrap_or(line.len());
920
+ line[s..e].to_string()
874
921
  }
875
922
 
876
923
  fn is_ident_char(c: char) -> bool {
877
924
  c.is_alphanumeric() || c == '_'
878
925
  }
879
926
 
927
+ // ── Type-aware hover ─────────────────────────────────────────────────────────
928
+
929
+ /// Render a `TypeAnnotation` to a readable, TypeScript-ish string for hover.
930
+ fn render_type(t: &tishlang_ast::TypeAnnotation) -> String {
931
+ use tishlang_ast::{TypeAnnotation as T, TypeLiteral as L};
932
+ match t {
933
+ T::Simple(s) => s.to_string(),
934
+ T::Array(inner) => {
935
+ // Parenthesize composite element types so `(A | B)[]` reads unambiguously.
936
+ if matches!(
937
+ inner.as_ref(),
938
+ T::Union(_) | T::Intersection(_) | T::Function { .. }
939
+ ) {
940
+ format!("({})[]", render_type(inner))
941
+ } else {
942
+ format!("{}[]", render_type(inner))
943
+ }
944
+ }
945
+ T::Object(fields) => format!(
946
+ "{{ {} }}",
947
+ fields
948
+ .iter()
949
+ .map(|(k, v)| format!("{}: {}", k, render_type(v)))
950
+ .collect::<Vec<_>>()
951
+ .join(", ")
952
+ ),
953
+ T::Function { params, returns } => format!(
954
+ "({}) => {}",
955
+ params.iter().map(render_type).collect::<Vec<_>>().join(", "),
956
+ render_type(returns)
957
+ ),
958
+ T::Union(ts) => ts.iter().map(render_type).collect::<Vec<_>>().join(" | "),
959
+ T::Tuple(ts) => format!(
960
+ "[{}]",
961
+ ts.iter().map(render_type).collect::<Vec<_>>().join(", ")
962
+ ),
963
+ T::Intersection(ts) => ts.iter().map(render_type).collect::<Vec<_>>().join(" & "),
964
+ T::Literal(L::Str(s)) => format!("\"{}\"", s),
965
+ T::Literal(L::Num(n)) => {
966
+ if n.fract() == 0.0 && n.is_finite() {
967
+ format!("{}", *n as i64)
968
+ } else {
969
+ n.to_string()
970
+ }
971
+ }
972
+ T::Literal(L::Bool(b)) => b.to_string(),
973
+ }
974
+ }
975
+
976
+ /// Best-effort type of a simple initializer (literals only). Anything non-trivial returns `None`,
977
+ /// so hover omits the type rather than guessing wrong.
978
+ fn shallow_expr_type(e: &tishlang_ast::Expr) -> Option<tishlang_ast::TypeAnnotation> {
979
+ use tishlang_ast::{Expr, Literal, TypeAnnotation as T};
980
+ if let Expr::Literal { value, .. } = e {
981
+ let name = match value {
982
+ Literal::Number(_) => "number",
983
+ Literal::String(_) => "string",
984
+ Literal::Bool(_) => "boolean",
985
+ Literal::Null => "null",
986
+ };
987
+ Some(T::Simple(Arc::from(name)))
988
+ } else {
989
+ None
990
+ }
991
+ }
992
+
993
+ /// Render a function parameter as `name: T` (or just `name` when unannotated).
994
+ fn render_param(p: &tishlang_ast::FunParam) -> String {
995
+ use tishlang_ast::FunParam;
996
+ match p {
997
+ FunParam::Simple(tp) => match &tp.type_ann {
998
+ Some(t) => format!("{}: {}", tp.name, render_type(t)),
999
+ None => tp.name.to_string(),
1000
+ },
1001
+ FunParam::Destructure { type_ann, .. } => match type_ann {
1002
+ Some(t) => format!("{{…}}: {}", render_type(t)),
1003
+ None => "{…}".to_string(),
1004
+ },
1005
+ }
1006
+ }
1007
+
1008
+ /// `fn name(params): R` signature line for a function declaration.
1009
+ fn fn_signature(
1010
+ name: &str,
1011
+ params: &[tishlang_ast::FunParam],
1012
+ rest: &Option<tishlang_ast::TypedParam>,
1013
+ ret: &Option<tishlang_ast::TypeAnnotation>,
1014
+ ) -> String {
1015
+ let mut ps: Vec<String> = params.iter().map(render_param).collect();
1016
+ if let Some(r) = rest {
1017
+ let t = r
1018
+ .type_ann
1019
+ .as_ref()
1020
+ .map(|t| format!(": {}", render_type(t)))
1021
+ .unwrap_or_default();
1022
+ ps.push(format!("...{}{}", r.name, t));
1023
+ }
1024
+ let ret_s = ret
1025
+ .as_ref()
1026
+ .map(render_type)
1027
+ .unwrap_or_else(|| "void".to_string());
1028
+ format!("fn {}({}): {}", name, ps.join(", "), ret_s)
1029
+ }
1030
+
1031
+ /// Definition spans are name spans; match on the start position.
1032
+ fn same_start(a: &tishlang_ast::Span, b: &tishlang_ast::Span) -> bool {
1033
+ a.start == b.start
1034
+ }
1035
+
1036
+ /// Wrap a one-line type hint in a tish code fence for hover.
1037
+ fn code_hint(line: &str) -> String {
1038
+ format!("\n\n```tish\n{}\n```", line)
1039
+ }
1040
+
1041
+ /// Find the declaration whose name is at `def` and produce a hover type line (markdown), if any.
1042
+ fn type_hint_at_def(program: &tishlang_ast::Program, def: &tishlang_ast::Span) -> Option<String> {
1043
+ program.statements.iter().find_map(|s| hint_in_stmt(s, def))
1044
+ }
1045
+
1046
+ /// `name_span` of a `type`/`interface` declaration named `name` (both parse to `TypeAlias`).
1047
+ /// Used so cmd+click on a `: SomeType` reference jumps to its declaration. Type annotations
1048
+ /// carry no spans, so this is a name match — sound because value bindings resolve first.
1049
+ fn type_decl_span(program: &tishlang_ast::Program, name: &str) -> Option<tishlang_ast::Span> {
1050
+ program.statements.iter().find_map(|s| match s {
1051
+ tishlang_ast::Statement::TypeAlias {
1052
+ name: n, name_span, ..
1053
+ } if n.as_ref() == name => Some(*name_span),
1054
+ _ => None,
1055
+ })
1056
+ }
1057
+
1058
+ /// Rendered body of a `type`/`interface` declaration named `name`, for hover.
1059
+ fn type_alias_body(program: &tishlang_ast::Program, name: &str) -> Option<String> {
1060
+ program.statements.iter().find_map(|s| match s {
1061
+ tishlang_ast::Statement::TypeAlias { name: n, ty, .. } if n.as_ref() == name => {
1062
+ Some(render_type(ty))
1063
+ }
1064
+ _ => None,
1065
+ })
1066
+ }
1067
+
1068
+ fn hint_in_stmt(s: &tishlang_ast::Statement, def: &tishlang_ast::Span) -> Option<String> {
1069
+ use tishlang_ast::{FunParam, Statement as St};
1070
+ match s {
1071
+ St::VarDecl {
1072
+ name,
1073
+ name_span,
1074
+ mutable,
1075
+ type_ann,
1076
+ init,
1077
+ ..
1078
+ } => {
1079
+ if same_start(name_span, def) {
1080
+ let ty = type_ann
1081
+ .clone()
1082
+ .or_else(|| init.as_ref().and_then(shallow_expr_type))?;
1083
+ let kw = if *mutable { "let" } else { "const" };
1084
+ return Some(code_hint(&format!(
1085
+ "{} {}: {}",
1086
+ kw,
1087
+ name,
1088
+ render_type(&ty)
1089
+ )));
1090
+ }
1091
+ None
1092
+ }
1093
+ St::FunDecl {
1094
+ name,
1095
+ name_span,
1096
+ params,
1097
+ rest_param,
1098
+ return_type,
1099
+ body,
1100
+ ..
1101
+ } => {
1102
+ if same_start(name_span, def) {
1103
+ return Some(code_hint(&fn_signature(
1104
+ name,
1105
+ params,
1106
+ rest_param,
1107
+ return_type,
1108
+ )));
1109
+ }
1110
+ for p in params {
1111
+ if let FunParam::Simple(tp) = p {
1112
+ if same_start(&tp.name_span, def) {
1113
+ let ty = tp
1114
+ .type_ann
1115
+ .as_ref()
1116
+ .map(render_type)
1117
+ .unwrap_or_else(|| "any".to_string());
1118
+ return Some(code_hint(&format!("(parameter) {}: {}", tp.name, ty)));
1119
+ }
1120
+ }
1121
+ }
1122
+ if let Some(r) = rest_param {
1123
+ if same_start(&r.name_span, def) {
1124
+ let ty = r
1125
+ .type_ann
1126
+ .as_ref()
1127
+ .map(render_type)
1128
+ .unwrap_or_else(|| "any[]".to_string());
1129
+ return Some(code_hint(&format!("(parameter) ...{}: {}", r.name, ty)));
1130
+ }
1131
+ }
1132
+ hint_in_stmt(body, def)
1133
+ }
1134
+ St::Block { statements, .. } | St::Multi { statements, .. } => {
1135
+ statements.iter().find_map(|s| hint_in_stmt(s, def))
1136
+ }
1137
+ St::If {
1138
+ then_branch,
1139
+ else_branch,
1140
+ ..
1141
+ } => hint_in_stmt(then_branch, def)
1142
+ .or_else(|| else_branch.as_ref().and_then(|e| hint_in_stmt(e, def))),
1143
+ St::For { init, body, .. } => init
1144
+ .as_ref()
1145
+ .and_then(|i| hint_in_stmt(i, def))
1146
+ .or_else(|| hint_in_stmt(body, def)),
1147
+ St::While { body, .. } | St::DoWhile { body, .. } | St::ForOf { body, .. } => {
1148
+ hint_in_stmt(body, def)
1149
+ }
1150
+ St::Try { body, .. } => hint_in_stmt(body, def),
1151
+ _ => None,
1152
+ }
1153
+ }
1154
+
880
1155
  fn value_completion_kind(program: &tishlang_ast::Program, name: &str) -> CompletionItemKind {
881
1156
  for s in &program.statements {
882
1157
  if let Some(k) = value_completion_kind_stmt(s, name) {
@@ -1044,3 +1319,141 @@ fn collect_child_syms(
1044
1319
  _ => doc_symbol_stmt(s, text, out),
1045
1320
  }
1046
1321
  }
1322
+
1323
+ #[cfg(test)]
1324
+ mod hover_tests {
1325
+ use super::*;
1326
+ use tishlang_ast::{FunParam, Span, Statement};
1327
+
1328
+ fn parse(src: &str) -> tishlang_ast::Program {
1329
+ tishlang_parser::parse(src).expect("parse")
1330
+ }
1331
+
1332
+ /// name_span of the first VarDecl/FunDecl named `name`, searched recursively.
1333
+ fn decl_span(s: &Statement, name: &str) -> Option<Span> {
1334
+ match s {
1335
+ Statement::VarDecl { name: n, name_span, .. } if n.as_ref() == name => Some(*name_span),
1336
+ Statement::FunDecl { name: n, name_span, body, .. } => {
1337
+ if n.as_ref() == name {
1338
+ Some(*name_span)
1339
+ } else {
1340
+ decl_span(body, name)
1341
+ }
1342
+ }
1343
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
1344
+ statements.iter().find_map(|x| decl_span(x, name))
1345
+ }
1346
+ Statement::If { then_branch, else_branch, .. } => decl_span(then_branch, name)
1347
+ .or_else(|| else_branch.as_ref().and_then(|e| decl_span(e, name))),
1348
+ Statement::For { body, .. }
1349
+ | Statement::While { body, .. }
1350
+ | Statement::DoWhile { body, .. }
1351
+ | Statement::ForOf { body, .. } => decl_span(body, name),
1352
+ _ => None,
1353
+ }
1354
+ }
1355
+
1356
+ fn span_of(p: &tishlang_ast::Program, name: &str) -> Span {
1357
+ p.statements
1358
+ .iter()
1359
+ .find_map(|s| decl_span(s, name))
1360
+ .unwrap_or_else(|| panic!("decl `{name}` not found"))
1361
+ }
1362
+
1363
+ fn param_span(p: &tishlang_ast::Program, fname: &str, pname: &str) -> Span {
1364
+ for s in &p.statements {
1365
+ if let Statement::FunDecl { name, params, .. } = s {
1366
+ if name.as_ref() == fname {
1367
+ for fp in params {
1368
+ if let FunParam::Simple(tp) = fp {
1369
+ if tp.name.as_ref() == pname {
1370
+ return tp.name_span;
1371
+ }
1372
+ }
1373
+ }
1374
+ }
1375
+ }
1376
+ }
1377
+ panic!("param `{fname}.{pname}` not found")
1378
+ }
1379
+
1380
+ fn hint(p: &tishlang_ast::Program, span: &Span) -> String {
1381
+ type_hint_at_def(p, span).expect("expected a type hint")
1382
+ }
1383
+
1384
+ #[test]
1385
+ fn annotated_var() {
1386
+ let p = parse("let count: number = 0\n");
1387
+ assert!(hint(&p, &span_of(&p, "count")).contains("let count: number"));
1388
+ }
1389
+
1390
+ #[test]
1391
+ fn inferred_var_and_const() {
1392
+ let p = parse("let x = 42\nconst label = \"hi\"\nlet ok = true\n");
1393
+ assert!(hint(&p, &span_of(&p, "x")).contains("let x: number"));
1394
+ assert!(hint(&p, &span_of(&p, "label")).contains("const label: string"));
1395
+ assert!(hint(&p, &span_of(&p, "ok")).contains("let ok: boolean"));
1396
+ }
1397
+
1398
+ #[test]
1399
+ fn function_signature() {
1400
+ let p = parse("fn add(a: number, b: number): number { return a + b }\n");
1401
+ assert!(hint(&p, &span_of(&p, "add")).contains("fn add(a: number, b: number): number"));
1402
+ }
1403
+
1404
+ #[test]
1405
+ fn parameter_hover() {
1406
+ let p = parse("fn f(p: string) { return p }\n");
1407
+ assert!(hint(&p, &param_span(&p, "f", "p")).contains("(parameter) p: string"));
1408
+ }
1409
+
1410
+ #[test]
1411
+ fn nested_decl_resolves() {
1412
+ let p = parse("fn g() {\n let inner: boolean = true\n return inner\n}\n");
1413
+ assert!(hint(&p, &span_of(&p, "inner")).contains("let inner: boolean"));
1414
+ }
1415
+
1416
+ #[test]
1417
+ fn composite_types_render() {
1418
+ use tishlang_ast::{TypeAnnotation as T, TypeLiteral as L};
1419
+ let arr = T::Array(Box::new(T::Simple("number".into())));
1420
+ assert_eq!(render_type(&arr), "number[]");
1421
+ let tup = T::Tuple(vec![T::Simple("number".into()), T::Simple("string".into())]);
1422
+ assert_eq!(render_type(&tup), "[number, string]");
1423
+ let uni = T::Union(vec![T::Simple("number".into()), T::Simple("null".into())]);
1424
+ assert_eq!(render_type(&uni), "number | null");
1425
+ assert_eq!(render_type(&T::Literal(L::Str("on".into()))), "\"on\"");
1426
+ let arr_of_union = T::Array(Box::new(uni));
1427
+ assert_eq!(render_type(&arr_of_union), "(number | null)[]");
1428
+ }
1429
+ }
1430
+
1431
+ #[cfg(test)]
1432
+ mod type_ref_tests {
1433
+ use super::*;
1434
+
1435
+ const SRC: &str =
1436
+ "interface Point { x: number, y: number }\ntype Status = \"on\" | \"off\"\nlet p: Point = { x: 1, y: 2 }\n";
1437
+
1438
+ #[test]
1439
+ fn type_decl_lookup_and_body() {
1440
+ let p = tishlang_parser::parse(SRC).expect("parse");
1441
+ assert!(type_decl_span(&p, "Point").is_some());
1442
+ assert!(type_decl_span(&p, "Status").is_some());
1443
+ assert_eq!(type_alias_body(&p, "Point").as_deref(), Some("{ x: number, y: number }"));
1444
+ assert_eq!(type_alias_body(&p, "Status").as_deref(), Some("\"on\" | \"off\""));
1445
+ assert!(type_decl_span(&p, "Nope").is_none());
1446
+ }
1447
+
1448
+ #[test]
1449
+ fn word_at_position_finds_whole_word() {
1450
+ // Cursor in the MIDDLE of `Point` (line 2, the `o`) must yield the whole word.
1451
+ assert_eq!(word_at_position(SRC, Position { line: 2, character: 8 }), "Point");
1452
+ // At the word start.
1453
+ assert_eq!(word_at_position(SRC, Position { line: 2, character: 7 }), "Point");
1454
+ // Just past the end (on the space) falls back to the word on the left.
1455
+ assert_eq!(word_at_position(SRC, Position { line: 2, character: 12 }), "Point");
1456
+ // On punctuation between words → empty.
1457
+ assert_eq!(word_at_position("a = b\n", Position { line: 0, character: 2 }), "");
1458
+ }
1459
+ }
@@ -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(&tokio_dep);
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))?;