@tishlang/tish 1.7.0 → 1.8.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/README.md +2 -0
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +28 -8
- package/crates/js_to_tish/src/transform/stmt.rs +49 -22
- package/crates/tish/Cargo.toml +15 -5
- package/crates/tish/src/cargo_native_registry.rs +29 -0
- package/crates/tish/src/cli_help.rs +16 -10
- package/crates/tish/src/main.rs +87 -32
- package/crates/tish/src/repl_completion.rs +3 -3
- package/crates/tish/tests/cargo_example_compile.rs +1 -1
- package/crates/tish/tests/integration_test.rs +19 -7
- package/crates/tish/tests/shortcircuit.rs +1 -1
- package/crates/tish_ast/src/ast.rs +80 -9
- package/crates/tish_build_utils/Cargo.toml +4 -0
- package/crates/tish_build_utils/src/lib.rs +105 -2
- package/crates/tish_builtins/Cargo.toml +5 -1
- package/crates/tish_builtins/src/array.rs +13 -12
- package/crates/tish_builtins/src/construct.rs +34 -33
- package/crates/tish_builtins/src/globals.rs +12 -11
- package/crates/tish_builtins/src/helpers.rs +2 -1
- package/crates/tish_builtins/src/object.rs +3 -2
- package/crates/tish_builtins/src/string.rs +73 -3
- package/crates/tish_bytecode/src/compiler.rs +12 -14
- package/crates/tish_bytecode/src/opcode.rs +12 -3
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/codegen.rs +745 -199
- package/crates/tish_compile/src/infer.rs +6 -0
- package/crates/tish_compile/src/lib.rs +4 -3
- package/crates/tish_compile/src/resolve.rs +180 -82
- package/crates/tish_compile/src/types.rs +175 -11
- package/crates/tish_compile_js/Cargo.toml +1 -0
- package/crates/tish_compile_js/src/codegen.rs +152 -29
- package/crates/tish_compile_js/src/lib.rs +3 -1
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +31 -12
- package/crates/tish_core/Cargo.toml +8 -0
- package/crates/tish_core/src/json.rs +102 -53
- package/crates/tish_core/src/lib.rs +3 -1
- package/crates/tish_core/src/macros.rs +5 -5
- package/crates/tish_core/src/value.rs +53 -15
- package/crates/tish_core/src/vmref.rs +178 -0
- package/crates/tish_eval/Cargo.toml +17 -2
- package/crates/tish_eval/src/eval.rs +90 -28
- package/crates/tish_eval/src/http.rs +61 -0
- package/crates/tish_eval/src/lib.rs +3 -3
- package/crates/tish_eval/src/natives.rs +41 -0
- package/crates/tish_eval/src/value.rs +7 -3
- package/crates/tish_eval/src/value_convert.rs +13 -5
- package/crates/tish_fmt/src/lib.rs +120 -30
- package/crates/tish_lexer/src/lib.rs +20 -5
- package/crates/tish_lexer/src/token.rs +4 -0
- package/crates/tish_llvm/src/lib.rs +3 -1
- package/crates/tish_lsp/Cargo.toml +4 -1
- package/crates/tish_lsp/README.md +1 -1
- package/crates/tish_lsp/src/builtin_goto.rs +261 -0
- package/crates/tish_lsp/src/import_goto.rs +549 -0
- package/crates/tish_lsp/src/main.rs +502 -102
- package/crates/tish_native/src/build.rs +3 -2
- package/crates/tish_native/src/lib.rs +6 -2
- package/crates/tish_opt/src/lib.rs +17 -2
- package/crates/tish_parser/src/lib.rs +10 -3
- package/crates/tish_parser/src/parser.rs +346 -56
- package/crates/tish_resolve/Cargo.toml +13 -0
- package/crates/tish_resolve/src/lib.rs +3436 -0
- package/crates/tish_resolve/src/pos.rs +133 -0
- package/crates/tish_runtime/Cargo.toml +68 -3
- package/crates/tish_runtime/src/http.rs +1123 -141
- package/crates/tish_runtime/src/http_fetch.rs +15 -14
- package/crates/tish_runtime/src/http_hyper.rs +418 -0
- package/crates/tish_runtime/src/http_prefork.rs +189 -0
- package/crates/tish_runtime/src/lib.rs +159 -29
- package/crates/tish_runtime/src/promise.rs +199 -36
- package/crates/tish_runtime/src/promise_io.rs +2 -1
- package/crates/tish_runtime/src/timers.rs +37 -1
- package/crates/tish_runtime/src/ws.rs +26 -28
- package/crates/tish_ui/src/jsx.rs +279 -8
- package/crates/tish_ui/src/lib.rs +5 -2
- package/crates/tish_ui/src/runtime/hooks.rs +406 -45
- package/crates/tish_ui/src/runtime/mod.rs +36 -9
- package/crates/tish_vm/Cargo.toml +15 -5
- package/crates/tish_vm/src/vm.rs +506 -259
- package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +3 -1
- package/crates/tish_wasm/src/lib.rs +17 -14
- package/crates/tish_wasm_runtime/Cargo.toml +2 -1
- package/crates/tish_wasm_runtime/src/lib.rs +1 -1
- package/crates/tishlang_cargo_bindgen/Cargo.toml +1 -0
- package/crates/tishlang_cargo_bindgen/src/discover.rs +68 -0
- package/crates/tishlang_cargo_bindgen/src/lib.rs +5 -4
- 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
|
@@ -4,7 +4,9 @@ use std::cell::RefCell;
|
|
|
4
4
|
use std::rc::Rc;
|
|
5
5
|
use std::sync::Arc;
|
|
6
6
|
|
|
7
|
-
use tishlang_core::{ObjectMap, Value as CoreValue};
|
|
7
|
+
use tishlang_core::{ObjectMap, Value as CoreValue, VmRef};
|
|
8
|
+
#[cfg(feature = "regex")]
|
|
9
|
+
use tishlang_core::TishRegExp;
|
|
8
10
|
|
|
9
11
|
use crate::value::{PropMap, Value};
|
|
10
12
|
|
|
@@ -20,14 +22,14 @@ pub fn eval_to_core(v: &Value) -> Result<CoreValue, String> {
|
|
|
20
22
|
for item in arr.borrow().iter() {
|
|
21
23
|
out.push(eval_to_core(item)?);
|
|
22
24
|
}
|
|
23
|
-
Ok(CoreValue::Array(
|
|
25
|
+
Ok(CoreValue::Array(VmRef::new(out)))
|
|
24
26
|
}
|
|
25
27
|
Value::Object(map) => {
|
|
26
28
|
let mut out = ObjectMap::default();
|
|
27
29
|
for (k, v) in map.borrow().iter() {
|
|
28
30
|
out.insert(Arc::clone(k), eval_to_core(v)?);
|
|
29
31
|
}
|
|
30
|
-
Ok(CoreValue::Object(
|
|
32
|
+
Ok(CoreValue::Object(VmRef::new(out)))
|
|
31
33
|
}
|
|
32
34
|
Value::Opaque(o) => Ok(CoreValue::Opaque(Arc::clone(o))),
|
|
33
35
|
_ => Err(format!(
|
|
@@ -63,13 +65,19 @@ pub fn core_to_eval(v: CoreValue) -> Value {
|
|
|
63
65
|
CoreValue::Promise(p) => Value::CorePromise(Arc::clone(&p)),
|
|
64
66
|
#[cfg(not(feature = "http"))]
|
|
65
67
|
CoreValue::Promise(_) => Value::Null,
|
|
66
|
-
|
|
68
|
+
// `CoreNativeFn` is feature-gated (Rc vs Arc), so use Clone::clone
|
|
69
|
+
// which works for either.
|
|
70
|
+
CoreValue::Function(f) => Value::CoreFn(f.clone()),
|
|
67
71
|
// tishlang_core gets regex from http or regex features; handle RegExp when it exists
|
|
68
72
|
#[cfg(any(feature = "http", feature = "regex"))]
|
|
69
73
|
CoreValue::RegExp(re) => {
|
|
70
74
|
#[cfg(feature = "regex")]
|
|
71
75
|
{
|
|
72
|
-
|
|
76
|
+
// Core uses `VmRef<TishRegExp>` (potentially `Arc<Mutex>`),
|
|
77
|
+
// interpreter uses `Rc<RefCell<TishRegExp>>`. Clone the
|
|
78
|
+
// inner state across so the two storage shapes can coexist.
|
|
79
|
+
let inner: TishRegExp = re.borrow().clone();
|
|
80
|
+
Value::RegExp(Rc::new(RefCell::new(inner)))
|
|
73
81
|
}
|
|
74
82
|
#[cfg(not(feature = "regex"))]
|
|
75
83
|
{
|
|
@@ -279,6 +279,51 @@ impl Printer {
|
|
|
279
279
|
self.buf.push_str(" from ");
|
|
280
280
|
self.string_lit(from.as_ref());
|
|
281
281
|
}
|
|
282
|
+
Statement::TypeAlias { name, ty, .. } => {
|
|
283
|
+
self.indent(level);
|
|
284
|
+
self.buf.push_str("type ");
|
|
285
|
+
self.buf.push_str(name);
|
|
286
|
+
self.buf.push_str(" = ");
|
|
287
|
+
self.type_ann(ty);
|
|
288
|
+
}
|
|
289
|
+
Statement::DeclareVar {
|
|
290
|
+
name,
|
|
291
|
+
type_ann,
|
|
292
|
+
const_,
|
|
293
|
+
..
|
|
294
|
+
} => {
|
|
295
|
+
self.indent(level);
|
|
296
|
+
self.buf.push_str("declare ");
|
|
297
|
+
self.buf.push_str(if *const_ { "const " } else { "let " });
|
|
298
|
+
self.buf.push_str(name);
|
|
299
|
+
if let Some(t) = type_ann {
|
|
300
|
+
self.buf.push_str(": ");
|
|
301
|
+
self.type_ann(t);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
Statement::DeclareFun {
|
|
305
|
+
async_,
|
|
306
|
+
name,
|
|
307
|
+
params,
|
|
308
|
+
rest_param,
|
|
309
|
+
return_type,
|
|
310
|
+
..
|
|
311
|
+
} => {
|
|
312
|
+
self.indent(level);
|
|
313
|
+
self.buf.push_str("declare ");
|
|
314
|
+
if *async_ {
|
|
315
|
+
self.buf.push_str("async ");
|
|
316
|
+
}
|
|
317
|
+
self.buf.push_str("fn ");
|
|
318
|
+
self.buf.push_str(name);
|
|
319
|
+
self.buf.push('(');
|
|
320
|
+
self.param_list(params, rest_param);
|
|
321
|
+
self.buf.push(')');
|
|
322
|
+
if let Some(rt) = return_type {
|
|
323
|
+
self.buf.push_str(": ");
|
|
324
|
+
self.type_ann(rt);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
282
327
|
Statement::Export { declaration, .. } => {
|
|
283
328
|
self.indent(level);
|
|
284
329
|
self.buf.push_str("export ");
|
|
@@ -361,12 +406,12 @@ impl Printer {
|
|
|
361
406
|
fn import_specs(&mut self, specs: &[ImportSpecifier]) {
|
|
362
407
|
if specs.len() == 1 {
|
|
363
408
|
match &specs[0] {
|
|
364
|
-
ImportSpecifier::Default
|
|
365
|
-
ImportSpecifier::Namespace
|
|
409
|
+
ImportSpecifier::Default { name, .. } => self.buf.push_str(name.as_ref()),
|
|
410
|
+
ImportSpecifier::Namespace { name, .. } => {
|
|
366
411
|
self.buf.push_str("* as ");
|
|
367
|
-
self.buf.push_str(
|
|
412
|
+
self.buf.push_str(name.as_ref());
|
|
368
413
|
}
|
|
369
|
-
ImportSpecifier::Named { name, alias } => {
|
|
414
|
+
ImportSpecifier::Named { name, alias, .. } => {
|
|
370
415
|
self.buf.push_str("{ ");
|
|
371
416
|
self.buf.push_str(name.as_ref());
|
|
372
417
|
if let Some(a) = alias {
|
|
@@ -384,7 +429,7 @@ impl Printer {
|
|
|
384
429
|
self.buf.push_str(", ");
|
|
385
430
|
}
|
|
386
431
|
match sp {
|
|
387
|
-
ImportSpecifier::Named { name, alias } => {
|
|
432
|
+
ImportSpecifier::Named { name, alias, .. } => {
|
|
388
433
|
self.buf.push_str(name.as_ref());
|
|
389
434
|
if let Some(a) = alias {
|
|
390
435
|
self.buf.push_str(" as ");
|
|
@@ -453,9 +498,9 @@ impl Printer {
|
|
|
453
498
|
self.buf.push_str(", ");
|
|
454
499
|
}
|
|
455
500
|
match e {
|
|
456
|
-
Some(DestructElement::Ident(n)) => self.buf.push_str(n.as_ref()),
|
|
501
|
+
Some(DestructElement::Ident(n, _)) => self.buf.push_str(n.as_ref()),
|
|
457
502
|
Some(DestructElement::Pattern(inner)) => self.destruct_pat(inner),
|
|
458
|
-
Some(DestructElement::Rest(n)) => {
|
|
503
|
+
Some(DestructElement::Rest(n, _)) => {
|
|
459
504
|
self.buf.push_str("...");
|
|
460
505
|
self.buf.push_str(n.as_ref());
|
|
461
506
|
}
|
|
@@ -472,16 +517,16 @@ impl Printer {
|
|
|
472
517
|
}
|
|
473
518
|
self.buf.push_str(pr.key.as_ref());
|
|
474
519
|
match &pr.value {
|
|
475
|
-
DestructElement::Ident(n) if n.as_ref() != pr.key.as_ref() => {
|
|
520
|
+
DestructElement::Ident(n, _) if n.as_ref() != pr.key.as_ref() => {
|
|
476
521
|
self.buf.push_str(": ");
|
|
477
522
|
self.buf.push_str(n.as_ref());
|
|
478
523
|
}
|
|
479
|
-
DestructElement::Ident(_) => {}
|
|
524
|
+
DestructElement::Ident(_, _) => {}
|
|
480
525
|
DestructElement::Pattern(inner) => {
|
|
481
526
|
self.buf.push_str(": ");
|
|
482
527
|
self.destruct_pat(inner);
|
|
483
528
|
}
|
|
484
|
-
DestructElement::Rest(n) => {
|
|
529
|
+
DestructElement::Rest(n, _) => {
|
|
485
530
|
self.buf.push_str(": ...");
|
|
486
531
|
self.buf.push_str(n.as_ref());
|
|
487
532
|
}
|
|
@@ -617,7 +662,7 @@ impl Printer {
|
|
|
617
662
|
self.buf.push('.');
|
|
618
663
|
}
|
|
619
664
|
match prop {
|
|
620
|
-
MemberProp::Name
|
|
665
|
+
MemberProp::Name { name, .. } => self.buf.push_str(name.as_ref()),
|
|
621
666
|
MemberProp::Expr(ex) => {
|
|
622
667
|
self.buf.push('[');
|
|
623
668
|
self.expr(ex);
|
|
@@ -817,35 +862,72 @@ impl Printer {
|
|
|
817
862
|
if children.is_empty() {
|
|
818
863
|
self.buf.push_str(" />");
|
|
819
864
|
} else {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
JsxChild::Text(t)
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
865
|
+
let compact = children.len() == 1
|
|
866
|
+
&& matches!(
|
|
867
|
+
&children[0],
|
|
868
|
+
JsxChild::Text(t) if !t.as_ref().contains('\n')
|
|
869
|
+
);
|
|
870
|
+
if compact {
|
|
871
|
+
self.buf.push('>');
|
|
872
|
+
if let JsxChild::Text(t) = &children[0] {
|
|
873
|
+
self.buf.push_str(t.as_ref());
|
|
874
|
+
}
|
|
875
|
+
self.buf.push_str("</");
|
|
876
|
+
self.buf.push_str(tag.as_ref());
|
|
877
|
+
self.buf.push('>');
|
|
878
|
+
} else {
|
|
879
|
+
self.buf.push('>');
|
|
880
|
+
self.buf.push('\n');
|
|
881
|
+
for ch in children {
|
|
882
|
+
self.buf.push_str(" ");
|
|
883
|
+
match ch {
|
|
884
|
+
JsxChild::Text(t) => self.buf.push_str(t.as_ref()),
|
|
885
|
+
JsxChild::Expr(e) => {
|
|
886
|
+
self.buf.push('{');
|
|
887
|
+
self.expr(e);
|
|
888
|
+
self.buf.push('}');
|
|
889
|
+
}
|
|
828
890
|
}
|
|
891
|
+
self.buf.push('\n');
|
|
829
892
|
}
|
|
893
|
+
self.buf.push_str(" </");
|
|
894
|
+
self.buf.push_str(tag.as_ref());
|
|
895
|
+
self.buf.push('>');
|
|
830
896
|
}
|
|
831
|
-
self.buf.push_str("</");
|
|
832
|
-
self.buf.push_str(tag.as_ref());
|
|
833
|
-
self.buf.push('>');
|
|
834
897
|
}
|
|
835
898
|
}
|
|
836
899
|
Expr::JsxFragment { children, .. } => {
|
|
837
900
|
self.buf.push_str("<>");
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
901
|
+
if children.is_empty() {
|
|
902
|
+
self.buf.push_str("</>");
|
|
903
|
+
} else {
|
|
904
|
+
let compact = children.len() == 1
|
|
905
|
+
&& matches!(
|
|
906
|
+
&children[0],
|
|
907
|
+
JsxChild::Text(t) if !t.as_ref().contains('\n')
|
|
908
|
+
);
|
|
909
|
+
if compact {
|
|
910
|
+
if let JsxChild::Text(t) = &children[0] {
|
|
911
|
+
self.buf.push_str(t.as_ref());
|
|
912
|
+
}
|
|
913
|
+
self.buf.push_str("</>");
|
|
914
|
+
} else {
|
|
915
|
+
self.buf.push('\n');
|
|
916
|
+
for ch in children {
|
|
917
|
+
self.buf.push_str(" ");
|
|
918
|
+
match ch {
|
|
919
|
+
JsxChild::Text(t) => self.buf.push_str(t.as_ref()),
|
|
920
|
+
JsxChild::Expr(e) => {
|
|
921
|
+
self.buf.push('{');
|
|
922
|
+
self.expr(e);
|
|
923
|
+
self.buf.push('}');
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
self.buf.push('\n');
|
|
845
927
|
}
|
|
928
|
+
self.buf.push_str("</>");
|
|
846
929
|
}
|
|
847
930
|
}
|
|
848
|
-
self.buf.push_str("</>");
|
|
849
931
|
}
|
|
850
932
|
Expr::NativeModuleLoad {
|
|
851
933
|
spec, export_name, ..
|
|
@@ -936,4 +1018,12 @@ mod tests {
|
|
|
936
1018
|
let out = format_source(src).unwrap();
|
|
937
1019
|
let _ = tishlang_parser::parse(&out).unwrap();
|
|
938
1020
|
}
|
|
1021
|
+
|
|
1022
|
+
#[test]
|
|
1023
|
+
fn jsx_multiline_when_mixed_children() {
|
|
1024
|
+
let src = "let x = <div>a{b}</div>\n";
|
|
1025
|
+
let out = format_source(src).unwrap();
|
|
1026
|
+
assert!(out.contains('\n'), "expected line breaks in formatted JSX: {out:?}");
|
|
1027
|
+
let _ = tishlang_parser::parse(&out).unwrap();
|
|
1028
|
+
}
|
|
939
1029
|
}
|
|
@@ -544,6 +544,13 @@ impl<'a> Lexer<'a> {
|
|
|
544
544
|
if self.peek() == Some('/') {
|
|
545
545
|
self.advance();
|
|
546
546
|
self.skip_line_comment();
|
|
547
|
+
// `skip_line_comment` consumes the newline via `advance()`, which sets
|
|
548
|
+
// `at_line_start` before we would normally run `skip_whitespace()`. Without
|
|
549
|
+
// stripping the next line's leading spaces here, `read_indent_level` would see
|
|
550
|
+
// physical indentation and emit a spurious `Indent` (breaks e.g. object
|
|
551
|
+
// literals with trailing `//` comments). Newlines handled in `skip_whitespace`
|
|
552
|
+
// eat those spaces before the indent pass; match that behavior.
|
|
553
|
+
self.skip_whitespace();
|
|
547
554
|
return self.next_token();
|
|
548
555
|
} else if self.peek() == Some('*') {
|
|
549
556
|
self.advance();
|
|
@@ -636,11 +643,8 @@ impl<'a> Lexer<'a> {
|
|
|
636
643
|
return Ok(Some(Token {
|
|
637
644
|
kind,
|
|
638
645
|
span: Span { start, end },
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
} else {
|
|
642
|
-
None
|
|
643
|
-
},
|
|
646
|
+
// Spelling is useful for keywords too (e.g. object keys, type names like `type`).
|
|
647
|
+
literal: Some(ident.into()),
|
|
644
648
|
}));
|
|
645
649
|
}
|
|
646
650
|
'\n' => {
|
|
@@ -693,4 +697,15 @@ mod tests {
|
|
|
693
697
|
let string_tok = tokens.iter().find(|t| t.kind == TokenKind::String).unwrap();
|
|
694
698
|
assert_eq!(string_tok.literal.as_deref(), Some("H"));
|
|
695
699
|
}
|
|
700
|
+
|
|
701
|
+
#[test]
|
|
702
|
+
fn line_comment_does_not_emit_spurious_indent_before_next_line() {
|
|
703
|
+
let with_comment = "fn f() {\n return {\n a: 1, // c\n b: 2\n }\n}\n";
|
|
704
|
+
let tokens: Vec<_> = Lexer::new(with_comment).collect::<Result<Vec<_>, _>>().unwrap();
|
|
705
|
+
assert!(
|
|
706
|
+
!tokens.iter().any(|t| t.kind == TokenKind::Indent),
|
|
707
|
+
"unexpected Indent after line comment: {:?}",
|
|
708
|
+
tokens.iter().map(|t| format!("{:?}", t.kind)).collect::<Vec<_>>()
|
|
709
|
+
);
|
|
710
|
+
}
|
|
696
711
|
}
|
|
@@ -57,6 +57,8 @@ pub enum TokenKind {
|
|
|
57
57
|
New,
|
|
58
58
|
Import,
|
|
59
59
|
Export,
|
|
60
|
+
Type,
|
|
61
|
+
Declare,
|
|
60
62
|
|
|
61
63
|
// Punctuation
|
|
62
64
|
LParen,
|
|
@@ -153,6 +155,8 @@ impl TokenKind {
|
|
|
153
155
|
"new" => TokenKind::New,
|
|
154
156
|
"import" => TokenKind::Import,
|
|
155
157
|
"export" => TokenKind::Export,
|
|
158
|
+
"type" => TokenKind::Type,
|
|
159
|
+
"declare" => TokenKind::Declare,
|
|
156
160
|
_ => TokenKind::Ident,
|
|
157
161
|
}
|
|
158
162
|
}
|
|
@@ -23,7 +23,9 @@ pub fn compile_to_native(
|
|
|
23
23
|
detect_cycles(&modules).map_err(|e| LlvmError {
|
|
24
24
|
message: e.to_string(),
|
|
25
25
|
})?;
|
|
26
|
-
let program = merge_modules(modules)
|
|
26
|
+
let program = merge_modules(modules)
|
|
27
|
+
.map(|m| m.program)
|
|
28
|
+
.map_err(|e| LlvmError {
|
|
27
29
|
message: e.to_string(),
|
|
28
30
|
})?;
|
|
29
31
|
let chunk = tishlang_bytecode::compile(&program).map_err(|e| LlvmError {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "tishlang_lsp"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.1"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
description = "Language Server Protocol implementation for Tish"
|
|
6
6
|
license-file = { workspace = true }
|
|
@@ -12,9 +12,12 @@ path = "src/main.rs"
|
|
|
12
12
|
|
|
13
13
|
[dependencies]
|
|
14
14
|
tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
|
|
15
|
+
tishlang_cargo_bindgen = { path = "../tishlang_cargo_bindgen", version = ">=0.1" }
|
|
16
|
+
tishlang_compile = { path = "../tish_compile", version = ">=0.1" }
|
|
15
17
|
tishlang_parser = { path = "../tish_parser", version = ">=0.1" }
|
|
16
18
|
tishlang_fmt = { path = "../tish_fmt", version = ">=0.1" }
|
|
17
19
|
tishlang_lint = { path = "../tish_lint", version = ">=0.1" }
|
|
20
|
+
tishlang_resolve = { path = "../tish_resolve", version = ">=0.1" }
|
|
18
21
|
tower-lsp = "0.20"
|
|
19
22
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-std"] }
|
|
20
23
|
serde_json = "1"
|
|
@@ -14,7 +14,7 @@ Binary: `target/release/tish-lsp` (stdio LSP).
|
|
|
14
14
|
|
|
15
15
|
- Parse diagnostics + lint warnings (via `tish_lint` **library** — use **`tish-lint`** CLI separately in CI)
|
|
16
16
|
- Document symbols, completion, formatting (via `tish_fmt` **library** — use **`tish-fmt`** CLI separately in CI)
|
|
17
|
-
- Go to definition (same file +
|
|
17
|
+
- Go to definition (same file, relative `./` / `../`, bare `node_modules` packages like Node, and native `tish:` / `@scope/pkg` / `cargo:` → Rust `pub fn` via `syn` + `cargo metadata` where configured)
|
|
18
18
|
- Workspace symbol search (`**/*.tish`)
|
|
19
19
|
|
|
20
20
|
## Client configuration
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
//! Built-in and JSX-intrinsic "go to definition" for the Tish LSP.
|
|
2
|
+
//!
|
|
3
|
+
//! Global / namespace member anchors are loaded from `stdlib/builtins.d.tish` in the `tish`
|
|
4
|
+
//! repository via `// @tish-source <symbol> <rel-path> <1-based-line>` pragmas. The type
|
|
5
|
+
//! surface in that file is the canonical declaration for ambient builtins.
|
|
6
|
+
//!
|
|
7
|
+
//! HTML / SVG intrinsic tag names are still listed here (sorted) for fast lookup; each maps
|
|
8
|
+
//! to the same vnode factory as in the pragma table for `div`.
|
|
9
|
+
//!
|
|
10
|
+
//! Definitions resolve to `file://` URIs only when `TISHLANG_SOURCE_ROOT` is set or the client
|
|
11
|
+
//! passes `tishlangSourceRoot` in LSP `initializationOptions` (VS Code: `tish.tishlangSourceRoot`).
|
|
12
|
+
|
|
13
|
+
use std::collections::HashMap;
|
|
14
|
+
use std::path::Path;
|
|
15
|
+
use std::sync::OnceLock;
|
|
16
|
+
|
|
17
|
+
use regex::Regex;
|
|
18
|
+
use tower_lsp::lsp_types::{Location, Position, Range, Url};
|
|
19
|
+
|
|
20
|
+
/// Stable path relative to the `tish` repository root (where the workspace `Cargo.toml` lives).
|
|
21
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
22
|
+
pub struct BuiltinDef {
|
|
23
|
+
pub rel_path: String,
|
|
24
|
+
/// 0-based LSP line in the target file.
|
|
25
|
+
pub line: u32,
|
|
26
|
+
/// 0-based UTF-16 code unit offset on that line (ASCII-only targets use byte column).
|
|
27
|
+
pub character: u32,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fn is_ident_char(c: char) -> bool {
|
|
31
|
+
c.is_alphanumeric() || c == '_'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// HTML / SVG intrinsic tag names (lowercase) that JSX lowers to `h("tag", …)`; must stay sorted for `binary_search`.
|
|
35
|
+
const HTML_INTRINSIC_TAGS: &[&str] = &[
|
|
36
|
+
"a", "abbr", "address", "area", "article", "aside", "audio", "b", "base", "bdi", "bdo",
|
|
37
|
+
"blockquote", "body", "br", "button", "canvas", "caption", "cite", "code", "col", "colgroup",
|
|
38
|
+
"data", "datalist", "dd", "del", "details", "dfn", "dialog", "div", "dl", "dt", "em", "embed",
|
|
39
|
+
"fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6",
|
|
40
|
+
"head", "header", "hr", "html", "i", "iframe", "img", "input", "ins", "kbd", "label", "legend",
|
|
41
|
+
"li", "link", "main", "map", "mark", "meta", "meter", "nav", "noscript", "object", "ol",
|
|
42
|
+
"optgroup", "option", "output", "p", "param", "picture", "pre", "progress", "q", "rp", "rt",
|
|
43
|
+
"ruby", "s", "samp", "script", "section", "select", "slot", "small", "source", "span", "strong",
|
|
44
|
+
"style", "sub", "summary", "sup", "svg", "table", "tbody", "td", "template", "textarea",
|
|
45
|
+
"tfoot", "th", "thead", "time", "title", "tr", "track", "u", "ul", "var", "video", "wbr",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
static SOURCE_MAP: OnceLock<HashMap<String, BuiltinDef>> = OnceLock::new();
|
|
49
|
+
|
|
50
|
+
/// Parse `// @tish-source <symbol> <rel-path> <1-based-line>` lines (same format as `stdlib/builtins.d.tish`).
|
|
51
|
+
pub fn parse_tish_source_pragmas(src: &str) -> HashMap<String, BuiltinDef> {
|
|
52
|
+
let re = Regex::new(r"(?m)^\s*//\s*@tish-source\s+(\S+)\s+(\S+)\s+(\d+)\s*$")
|
|
53
|
+
.expect("builtin pragma regex");
|
|
54
|
+
let mut m = HashMap::new();
|
|
55
|
+
for cap in re.captures_iter(src) {
|
|
56
|
+
let sym = cap[1].to_string();
|
|
57
|
+
let rel = cap[2].to_string();
|
|
58
|
+
let line_1: u32 = cap[3].parse().unwrap_or(1);
|
|
59
|
+
m.insert(
|
|
60
|
+
sym,
|
|
61
|
+
BuiltinDef {
|
|
62
|
+
rel_path: rel,
|
|
63
|
+
line: line_1.saturating_sub(1),
|
|
64
|
+
character: 0,
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
m
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fn source_map() -> &'static HashMap<String, BuiltinDef> {
|
|
72
|
+
SOURCE_MAP.get_or_init(|| {
|
|
73
|
+
let src = include_str!(concat!(
|
|
74
|
+
env!("CARGO_MANIFEST_DIR"),
|
|
75
|
+
"/../../stdlib/builtins.d.tish"
|
|
76
|
+
));
|
|
77
|
+
parse_tish_source_pragmas(src)
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// If `col` lies on the name of a JSX opening (or closing) tag on this line, returns the span of that name in **character indices** (same convention as [`tower_lsp::lsp_types::Position::character`] for ASCII lines).
|
|
82
|
+
fn jsx_tag_name_char_span(line: &str, col: usize) -> Option<(usize, usize)> {
|
|
83
|
+
let chars: Vec<char> = line.chars().collect();
|
|
84
|
+
if chars.is_empty() {
|
|
85
|
+
return None;
|
|
86
|
+
}
|
|
87
|
+
let col = col.min(chars.len().saturating_sub(1));
|
|
88
|
+
let mut j = col;
|
|
89
|
+
while j > 0 {
|
|
90
|
+
j -= 1;
|
|
91
|
+
if chars[j] == '>' {
|
|
92
|
+
return None;
|
|
93
|
+
}
|
|
94
|
+
if chars[j] == '<' {
|
|
95
|
+
let mut k = j + 1;
|
|
96
|
+
while k < chars.len() && chars[k].is_whitespace() {
|
|
97
|
+
k += 1;
|
|
98
|
+
}
|
|
99
|
+
if k < chars.len() && chars[k] == '/' {
|
|
100
|
+
k += 1;
|
|
101
|
+
while k < chars.len() && chars[k].is_whitespace() {
|
|
102
|
+
k += 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
let name_start = k;
|
|
106
|
+
while k < chars.len() {
|
|
107
|
+
let c = chars[k];
|
|
108
|
+
if c.is_whitespace() || c == '>' || c == '/' || c == '{' {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
if !(c.is_alphanumeric() || c == '_' || c == '-') {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
k += 1;
|
|
115
|
+
}
|
|
116
|
+
let name_end = k;
|
|
117
|
+
if name_start < name_end && col >= name_start && col < name_end {
|
|
118
|
+
return Some((name_start, name_end));
|
|
119
|
+
}
|
|
120
|
+
return None;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
None
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn tag_name_at_span(line: &str, start: usize, end: usize) -> String {
|
|
127
|
+
line.chars()
|
|
128
|
+
.enumerate()
|
|
129
|
+
.filter(|(i, _)| *i >= start && *i < end)
|
|
130
|
+
.map(|(_, c)| c)
|
|
131
|
+
.collect()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// `base.member` when the cursor is on `member` (same line, character-index column as `word_at_position`).
|
|
135
|
+
fn split_property_access(line: &str, col: usize) -> Option<(String, String)> {
|
|
136
|
+
let chars: Vec<char> = line.chars().collect();
|
|
137
|
+
if chars.is_empty() {
|
|
138
|
+
return None;
|
|
139
|
+
}
|
|
140
|
+
let col = col.min(chars.len().saturating_sub(1));
|
|
141
|
+
if !is_ident_char(chars[col]) {
|
|
142
|
+
return None;
|
|
143
|
+
}
|
|
144
|
+
let mut end = col;
|
|
145
|
+
while end + 1 < chars.len() && is_ident_char(chars[end + 1]) {
|
|
146
|
+
end += 1;
|
|
147
|
+
}
|
|
148
|
+
let mut start = col;
|
|
149
|
+
while start > 0 && is_ident_char(chars[start - 1]) {
|
|
150
|
+
start -= 1;
|
|
151
|
+
}
|
|
152
|
+
if start == 0 {
|
|
153
|
+
return None;
|
|
154
|
+
}
|
|
155
|
+
if chars[start - 1] != '.' {
|
|
156
|
+
return None;
|
|
157
|
+
}
|
|
158
|
+
let member: String = chars[start..=end].iter().collect();
|
|
159
|
+
let mut k = start - 2;
|
|
160
|
+
while k > 0 && is_ident_char(chars[k - 1]) {
|
|
161
|
+
k -= 1;
|
|
162
|
+
}
|
|
163
|
+
let base: String = chars[k..start - 1].iter().collect();
|
|
164
|
+
if base.is_empty() {
|
|
165
|
+
return None;
|
|
166
|
+
}
|
|
167
|
+
Some((base, member))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
fn lookup_dotted(base: &str, member: &str) -> Option<BuiltinDef> {
|
|
171
|
+
let key = format!("{base}.{member}");
|
|
172
|
+
source_map().get(&key).cloned()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
fn lookup_global(word: &str) -> Option<BuiltinDef> {
|
|
176
|
+
source_map().get(word).cloned()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// Built-in or JSX-intrinsic definition for the identifier at `position`, if any.
|
|
180
|
+
pub fn definition_for_builtin(
|
|
181
|
+
text: &str,
|
|
182
|
+
line: u32,
|
|
183
|
+
character: u32,
|
|
184
|
+
word: &str,
|
|
185
|
+
) -> Option<BuiltinDef> {
|
|
186
|
+
let line_str = text.lines().nth(line as usize)?;
|
|
187
|
+
let col = character as usize;
|
|
188
|
+
|
|
189
|
+
if let Some((ns, ne)) = jsx_tag_name_char_span(line_str, col) {
|
|
190
|
+
let tag = tag_name_at_span(line_str, ns, ne);
|
|
191
|
+
if tag == word {
|
|
192
|
+
if word == "Fragment" {
|
|
193
|
+
return lookup_global("Fragment");
|
|
194
|
+
}
|
|
195
|
+
if HTML_INTRINSIC_TAGS.binary_search(&word).is_ok() {
|
|
196
|
+
// Intrinsic tags share the same `ui_h` entry point as `div`.
|
|
197
|
+
return lookup_global("div");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if let Some((base, member)) = split_property_access(line_str, col) {
|
|
203
|
+
if let Some(d) = lookup_dotted(base.as_str(), member.as_str()) {
|
|
204
|
+
return Some(d);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
lookup_global(word)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
pub fn to_file_location(root: &Path, def: &BuiltinDef) -> Option<Location> {
|
|
212
|
+
let path = root.join(&def.rel_path);
|
|
213
|
+
if !path.is_file() {
|
|
214
|
+
return None;
|
|
215
|
+
}
|
|
216
|
+
let uri = Url::from_file_path(&path).ok()?;
|
|
217
|
+
let p = Position {
|
|
218
|
+
line: def.line,
|
|
219
|
+
character: def.character,
|
|
220
|
+
};
|
|
221
|
+
Some(Location {
|
|
222
|
+
uri,
|
|
223
|
+
range: Range { start: p, end: p },
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#[cfg(test)]
|
|
228
|
+
mod tests {
|
|
229
|
+
use super::*;
|
|
230
|
+
|
|
231
|
+
#[test]
|
|
232
|
+
fn pragma_map_loads_console_log() {
|
|
233
|
+
let d = lookup_global("console").expect("console");
|
|
234
|
+
assert!(d.rel_path.contains("eval.rs"));
|
|
235
|
+
let m = lookup_dotted("console", "log").expect("console.log");
|
|
236
|
+
assert!(m.rel_path.contains("natives.rs"));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
#[test]
|
|
240
|
+
fn jsx_div_maps_to_ui_h() {
|
|
241
|
+
let text = "let x = <div />";
|
|
242
|
+
let line = 0u32;
|
|
243
|
+
let defn = definition_for_builtin(text, line, 9, "div").expect("div builtin");
|
|
244
|
+
assert!(defn.rel_path.contains("runtime"));
|
|
245
|
+
assert_eq!(defn.line, 40);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#[test]
|
|
249
|
+
fn set_timeout_global() {
|
|
250
|
+
let defn =
|
|
251
|
+
definition_for_builtin("setTimeout(0, fn() {})\n", 0, 0, "setTimeout").expect("timer");
|
|
252
|
+
assert_eq!(defn.rel_path, "crates/tish_eval/src/timers.rs");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
#[test]
|
|
256
|
+
fn console_log_qualified() {
|
|
257
|
+
let text = "console.log(1)";
|
|
258
|
+
let defn = definition_for_builtin(text, 0, 10, "log").expect("console.log");
|
|
259
|
+
assert_eq!(defn.rel_path, "crates/tish_eval/src/natives.rs");
|
|
260
|
+
}
|
|
261
|
+
}
|