@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.
- 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
|
@@ -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 _ =
|
|
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
|
-
|
|
288
|
-
.
|
|
289
|
-
.
|
|
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
|
-
|
|
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, .. } =>
|
|
793
|
-
|
|
794
|
-
|
|
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
|
|
860
|
-
let
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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, ¶m_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(
|
|
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))?;
|