@tishlang/tish-lsp 2.16.13 → 2.36.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 (52) hide show
  1. package/bin/tish-lsp +0 -0
  2. package/crates/js_to_tish/src/transform/expr.rs +12 -2
  3. package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
  4. package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
  5. package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
  6. package/crates/tish/tests/fixtures/process_capture.tish +16 -0
  7. package/crates/tish/tests/fs_parity.rs +103 -0
  8. package/crates/tish/tests/process_capture.rs +40 -0
  9. package/crates/tish/tests/shortcircuit.rs +3 -3
  10. package/crates/tish_ast/src/ast.rs +77 -3
  11. package/crates/tish_builtins/Cargo.toml +1 -0
  12. package/crates/tish_builtins/src/array.rs +195 -16
  13. package/crates/tish_builtins/src/collections.rs +37 -2
  14. package/crates/tish_builtins/src/globals.rs +215 -25
  15. package/crates/tish_builtins/src/math.rs +65 -7
  16. package/crates/tish_builtins/src/number.rs +173 -0
  17. package/crates/tish_builtins/src/string.rs +168 -15
  18. package/crates/tish_bytecode/src/compiler.rs +56 -4
  19. package/crates/tish_bytecode/src/opcode.rs +4 -2
  20. package/crates/tish_compile/src/codegen.rs +519 -46
  21. package/crates/tish_compile/src/infer.rs +26 -11
  22. package/crates/tish_compile/src/resolve.rs +87 -10
  23. package/crates/tish_compile_js/src/codegen.rs +82 -24
  24. package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
  25. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +17 -2
  26. package/crates/tish_core/src/json.rs +5 -1
  27. package/crates/tish_core/src/lib.rs +48 -2
  28. package/crates/tish_core/src/uri.rs +45 -0
  29. package/crates/tish_core/src/value.rs +6 -0
  30. package/crates/tish_eval/Cargo.toml +2 -1
  31. package/crates/tish_eval/src/eval.rs +661 -27
  32. package/crates/tish_eval/src/natives.rs +318 -18
  33. package/crates/tish_eval/src/regex.rs +47 -1
  34. package/crates/tish_fmt/src/lib.rs +23 -4
  35. package/crates/tish_lsp/Cargo.toml +1 -1
  36. package/crates/tish_opt/src/lib.rs +20 -1
  37. package/crates/tish_parser/src/lib.rs +41 -0
  38. package/crates/tish_parser/src/parser.rs +180 -11
  39. package/crates/tish_resolve/src/lib.rs +66 -5
  40. package/crates/tish_runtime/Cargo.toml +5 -1
  41. package/crates/tish_runtime/src/fs_ext.rs +359 -0
  42. package/crates/tish_runtime/src/http_hyper.rs +41 -18
  43. package/crates/tish_runtime/src/lib.rs +236 -10
  44. package/crates/tish_ui/src/jsx.rs +18 -3
  45. package/crates/tish_vm/src/jit.rs +1 -1
  46. package/crates/tish_vm/src/vm.rs +294 -60
  47. package/package.json +1 -1
  48. package/platform/darwin-arm64/tish-lsp +0 -0
  49. package/platform/darwin-x64/tish-lsp +0 -0
  50. package/platform/linux-arm64/tish-lsp +0 -0
  51. package/platform/linux-x64/tish-lsp +0 -0
  52. package/platform/win32-x64/tish-lsp.exe +0 -0
package/bin/tish-lsp CHANGED
Binary file
@@ -176,7 +176,12 @@ pub fn convert_expr(expr: &OxcExpr<'_>, ctx: &Ctx<'_>) -> Result<Expr, ConvertEr
176
176
  span: span_util::oxc_span_to_tish(ctx.1, &*arrow.body),
177
177
  }))
178
178
  };
179
- Ok(Expr::ArrowFunction { params, body, span })
179
+ Ok(Expr::ArrowFunction {
180
+ async_: arrow.r#async,
181
+ params,
182
+ body,
183
+ span,
184
+ })
180
185
  }
181
186
  OxcExpr::AwaitExpression(a) => Ok(Expr::Await {
182
187
  operand: Box::new(convert_expr(&a.argument, ctx)?),
@@ -229,7 +234,12 @@ pub fn convert_expr(expr: &OxcExpr<'_>, ctx: &Ctx<'_>) -> Result<Expr, ConvertEr
229
234
  }))
230
235
  }
231
236
  };
232
- Ok(Expr::ArrowFunction { params, body, span })
237
+ Ok(Expr::ArrowFunction {
238
+ async_: f.r#async,
239
+ params,
240
+ body,
241
+ span,
242
+ })
233
243
  }
234
244
  _ => Err(ConvertError::new(ConvertErrorKind::Unsupported {
235
245
  what: format!("expression: {:?}", std::mem::discriminant(expr)),
@@ -0,0 +1,22 @@
1
+ // Issue #122: Node callback form on the VM — `fs.writeFile(path, data, (err) => …)` and
2
+ // `fs.readFile(path, (err, data) => …)`, with (err, data) semantics and a non-null err on a
3
+ // missing file. Operates under $FS_TMP.
4
+ import { writeFile, readFile, stat, unlink } from "node:fs"
5
+ import { process } from "tish:process"
6
+
7
+ let d = process.env.FS_TMP
8
+ writeFile(d + "/cb.txt", "callback data", (err) => {
9
+ console.log("write-err-null=" + String(err === null))
10
+ readFile(d + "/cb.txt", (err2, data) => {
11
+ console.log("read=" + data)
12
+ stat(d + "/cb.txt", (err3, st) => {
13
+ console.log("size=" + String(st.size) + ",file=" + String(st.isFile()))
14
+ unlink(d + "/cb.txt", (err4) => {
15
+ console.log("unlink-err-null=" + String(err4 === null))
16
+ })
17
+ })
18
+ })
19
+ })
20
+ readFile(d + "/missing.txt", (err, data) => {
21
+ console.log("missing-err-set=" + String(!(err === null)) + ",data-null=" + String(data === null))
22
+ })
@@ -0,0 +1,21 @@
1
+ // Issue #122: the async `node:fs/promises` surface — await on the same methods, and `access`
2
+ // rejects on a missing path. Operates under $FS_TMP.
3
+ import { writeFile, readFile, stat, mkdir, readdir, rm, access } from "node:fs/promises"
4
+ import { process } from "tish:process"
5
+
6
+ async fn main() {
7
+ let d = process.env.FS_TMP
8
+ await writeFile(d + "/p.txt", "async hello")
9
+ console.log("read=" + (await readFile(d + "/p.txt")))
10
+ let st = await stat(d + "/p.txt")
11
+ console.log("size=" + String(st.size))
12
+ await mkdir(d + "/pd")
13
+ await writeFile(d + "/pd/x", "x")
14
+ console.log("dir=" + (await readdir(d + "/pd")).join(","))
15
+ let rejected = false
16
+ try { await access(d + "/missing") } catch (e) { rejected = true }
17
+ console.log("access-missing-rejected=" + String(rejected))
18
+ await rm(d + "/pd", { recursive: true })
19
+ console.log("done")
20
+ }
21
+ main()
@@ -0,0 +1,20 @@
1
+ // Issue #122: Node-compatible fs sync surface via `node:fs`. Operates under $FS_TMP.
2
+ import { writeFileSync, readFileSync, existsSync, statSync, mkdirSync, readdirSync, renameSync, rmSync, appendFileSync, copyFileSync, constants } from "node:fs"
3
+ import { process } from "tish:process"
4
+
5
+ let d = process.env.FS_TMP
6
+ writeFileSync(d + "/a.txt", "hi")
7
+ appendFileSync(d + "/a.txt", "!")
8
+ console.log("read=" + readFileSync(d + "/a.txt"))
9
+ console.log("exists=" + String(existsSync(d + "/a.txt")) + "," + String(existsSync(d + "/no")))
10
+ let st = statSync(d + "/a.txt")
11
+ console.log("size=" + String(st.size) + ",file=" + String(st.isFile()) + ",dir=" + String(st.isDirectory()))
12
+ mkdirSync(d + "/sub")
13
+ copyFileSync(d + "/a.txt", d + "/sub/x.txt")
14
+ console.log("dir=" + readdirSync(d + "/sub").join(","))
15
+ renameSync(d + "/a.txt", d + "/b.txt")
16
+ console.log("renamed=" + String(existsSync(d + "/a.txt")) + "," + String(existsSync(d + "/b.txt")))
17
+ rmSync(d + "/sub", { recursive: true })
18
+ console.log("rm=" + String(existsSync(d + "/sub")))
19
+ console.log("const=" + String(constants.R_OK))
20
+ console.log("done")
@@ -0,0 +1,16 @@
1
+ // Issue #475: process.execCapture / execFileCapture return { code, stdout, stderr }.
2
+ import { execFileCapture, execCapture } from "tish:process"
3
+
4
+ let ok = execFileCapture("echo", ["hi"])
5
+ console.log("ok=" + ok.code + "," + ok.stdout.trim() + "," + (ok.stderr === ""))
6
+
7
+ let bad = execFileCapture("false", [])
8
+ console.log("bad=" + bad.code)
9
+
10
+ let miss = execFileCapture("/no/such/prog/xyz", [])
11
+ console.log("miss=" + miss.code + "," + (miss.stderr !== ""))
12
+
13
+ let shell = execCapture("echo a; echo b 1>&2")
14
+ console.log("shell=" + shell.code + "," + shell.stdout.trim() + "," + shell.stderr.trim())
15
+
16
+ console.log("done")
@@ -0,0 +1,103 @@
1
+ //! Issue #122: Node `fs` parity on the bytecode VM (`tish run` default backend) — the sync
2
+ //! surface via `node:fs` and the async `node:fs/promises` surface (await + rejection).
3
+
4
+ use std::path::PathBuf;
5
+ use std::process::Command;
6
+
7
+ fn run_on(backend: &str, fixture: &str, tmp: &str) -> String {
8
+ let tish = PathBuf::from(env!("CARGO_BIN_EXE_tish"));
9
+ let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
10
+ .join("tests")
11
+ .join("fixtures")
12
+ .join(fixture);
13
+ let out = Command::new(&tish)
14
+ .args([
15
+ "run",
16
+ "--backend",
17
+ backend,
18
+ "--feature",
19
+ "fs,process",
20
+ path.to_str().unwrap(),
21
+ ])
22
+ .env("FS_TMP", tmp)
23
+ .output()
24
+ .expect("spawn tish run");
25
+ assert!(
26
+ out.status.success(),
27
+ "tish run --backend {backend} {fixture} failed: {}",
28
+ String::from_utf8_lossy(&out.stderr)
29
+ );
30
+ String::from_utf8_lossy(&out.stdout).into_owned()
31
+ }
32
+
33
+ fn tmp_dir(tag: &str) -> String {
34
+ let nanos = std::time::SystemTime::now()
35
+ .duration_since(std::time::UNIX_EPOCH)
36
+ .unwrap()
37
+ .as_nanos();
38
+ let d = std::env::temp_dir().join(format!("tish_fs_parity_{tag}_{nanos:x}"));
39
+ std::fs::create_dir_all(&d).unwrap();
40
+ d.to_str().unwrap().to_string()
41
+ }
42
+
43
+ const SYNC_EXPECTED: &str = "read=hi!\n\
44
+ exists=true,false\n\
45
+ size=3,file=true,dir=false\n\
46
+ dir=x.txt\n\
47
+ renamed=false,true\n\
48
+ rm=false\n\
49
+ const=4\n\
50
+ done\n";
51
+
52
+ const PROMISES_EXPECTED: &str = "read=async hello\n\
53
+ size=11\n\
54
+ dir=x\n\
55
+ access-missing-rejected=true\n\
56
+ done\n";
57
+
58
+ #[test]
59
+ fn node_fs_sync_surface_on_every_backend() {
60
+ for backend in ["vm", "interp"] {
61
+ let tmp = tmp_dir(&format!("sync_{backend}"));
62
+ assert_eq!(
63
+ run_on(backend, "fs_parity_sync.tish", &tmp),
64
+ SYNC_EXPECTED,
65
+ "sync surface mismatch on {backend}"
66
+ );
67
+ let _ = std::fs::remove_dir_all(&tmp);
68
+ }
69
+ }
70
+
71
+ #[test]
72
+ fn node_fs_callback_form_on_every_backend() {
73
+ // Node callback form `readFile(path, (err, data) => …)` on every backend (#122): native's
74
+ // multi-arg-closure-to-native lowering was fixed upstream, and the interpreter runs the callback
75
+ // at the eval level (call_func's Native arm) instead of the self-less core bridge.
76
+ let expected = "write-err-null=true\n\
77
+ read=callback data\n\
78
+ size=13,file=true\n\
79
+ unlink-err-null=true\n\
80
+ missing-err-set=true,data-null=true\n";
81
+ for backend in ["vm", "interp", "native"] {
82
+ let tmp = tmp_dir(&format!("cb_{backend}"));
83
+ assert_eq!(
84
+ run_on(backend, "fs_parity_callbacks.tish", &tmp),
85
+ expected,
86
+ "callback form mismatch on {backend}"
87
+ );
88
+ let _ = std::fs::remove_dir_all(&tmp);
89
+ }
90
+ }
91
+
92
+ #[test]
93
+ fn node_fs_promises_surface_on_every_backend() {
94
+ for backend in ["vm", "interp"] {
95
+ let tmp = tmp_dir(&format!("prom_{backend}"));
96
+ assert_eq!(
97
+ run_on(backend, "fs_parity_promises.tish", &tmp),
98
+ PROMISES_EXPECTED,
99
+ "promises surface mismatch on {backend}"
100
+ );
101
+ let _ = std::fs::remove_dir_all(&tmp);
102
+ }
103
+ }
@@ -0,0 +1,40 @@
1
+ //! Issue #475: `process.execCapture` / `execFileCapture` return `{ code, stdout, stderr }` identically
2
+ //! on every backend (interpreter, bytecode VM, native AOT).
3
+
4
+ use std::path::PathBuf;
5
+ use std::process::Command;
6
+
7
+ fn run_on(backend: &str) -> String {
8
+ let tish = PathBuf::from(env!("CARGO_BIN_EXE_tish"));
9
+ let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
10
+ .join("tests")
11
+ .join("fixtures")
12
+ .join("process_capture.tish");
13
+ let out = Command::new(&tish)
14
+ .args([
15
+ "run",
16
+ "--backend",
17
+ backend,
18
+ "--feature",
19
+ "process",
20
+ path.to_str().unwrap(),
21
+ ])
22
+ .output()
23
+ .expect("spawn tish run");
24
+ assert!(
25
+ out.status.success(),
26
+ "tish run --backend {backend} process_capture.tish failed: {}",
27
+ String::from_utf8_lossy(&out.stderr)
28
+ );
29
+ String::from_utf8_lossy(&out.stdout).into_owned()
30
+ }
31
+
32
+ const EXPECTED: &str = "ok=0,hi,true\nbad=1\nmiss=-1,true\nshell=0,a,b\ndone\n";
33
+
34
+ #[test]
35
+ #[cfg(unix)]
36
+ fn exec_capture_on_every_backend() {
37
+ for backend in ["vm", "interp", "native"] {
38
+ assert_eq!(run_on(backend), EXPECTED, "exec-capture mismatch on {backend}");
39
+ }
40
+ }
@@ -9,7 +9,7 @@ use tishlang_vm;
9
9
 
10
10
  #[test]
11
11
  fn test_and_shortcircuit_emits_jump() {
12
- let source = "let x = null; let y = x != null && x.foo;";
12
+ let source = "let x = null; let y = x !== null && x.foo;";
13
13
  let program = parse(source).expect("parse");
14
14
  let chunk = compile_unoptimized(&program).expect("compile");
15
15
  let code = &chunk.code;
@@ -22,7 +22,7 @@ fn test_and_shortcircuit_emits_jump() {
22
22
 
23
23
  #[test]
24
24
  fn test_and_shortcircuit_runs_unoptimized() {
25
- let source = "let x = null; let y = x != null && x.foo;";
25
+ let source = "let x = null; let y = x !== null && x.foo;";
26
26
  let program = parse(source).expect("parse");
27
27
  let chunk = compile_unoptimized(&program).expect("compile");
28
28
  let result = tishlang_vm::run(&chunk);
@@ -35,7 +35,7 @@ fn test_and_shortcircuit_runs_unoptimized() {
35
35
 
36
36
  #[test]
37
37
  fn test_and_shortcircuit_runs_optimized() {
38
- let source = "let x = null; let y = x != null && x.foo;";
38
+ let source = "let x = null; let y = x !== null && x.foo;";
39
39
  let program = parse(source).expect("parse");
40
40
  let program = tishlang_opt::optimize(&program);
41
41
  let chunk = tishlang_bytecode::compile(&program).expect("compile");
@@ -157,13 +157,15 @@ pub enum ExportDeclaration {
157
157
  /// export default expr
158
158
  Default(Expr),
159
159
  /// Re-export from another module (#305):
160
- /// `export { foo, bar as inc } from "./m"` -> specifiers, all=false
161
- /// `export * from "./m"` -> specifiers=[], all=true
160
+ /// `export { foo, bar as inc } from "./m"` -> specifiers, all=false, from=Some
161
+ /// `export * from "./m"` -> specifiers=[], all=true, from=Some
162
+ /// `export { a, b as c }` -> specifiers, all=false, from=None (#415 —
163
+ /// a LOCAL named export of already-declared bindings)
162
164
  /// Specifiers reuse `ImportSpecifier::Named` (`{ a as b }`).
163
165
  ReExport {
164
166
  specifiers: Vec<ImportSpecifier>,
165
167
  all: bool,
166
- from: Arc<str>,
168
+ from: Option<Arc<str>>,
167
169
  span: Span,
168
170
  },
169
171
  }
@@ -173,6 +175,64 @@ pub struct Program {
173
175
  pub statements: Vec<Statement>,
174
176
  }
175
177
 
178
+ /// #469: adopt a Go/Rust/Swift-style `main` entry point.
179
+ ///
180
+ /// tish runs top-level statements (JS module semantics), so `fn main() { … }` on its own defines a
181
+ /// function that is never called — a footgun for devs coming from compiled languages (the native
182
+ /// server case: `fn main() { serve(...) }` silently exits). This appends a synthetic `main()`
183
+ /// invocation to the ENTRY program's top-level statements so a defined `main` actually runs: top-level
184
+ /// statements still execute first (module init), then `main`.
185
+ ///
186
+ /// Deliberately conservative to avoid a double-invocation: it fires ONLY when a top-level `main`
187
+ /// function is declared AND the program does not already invoke `main()` itself at top level (the
188
+ /// JS-habit `fn main(){…}; main()` shape). This must be applied to the entry module only (not imported
189
+ /// modules) and identically for every backend — call it from `merge_modules` so interp/vm/native/js
190
+ /// stay consistent by construction.
191
+ ///
192
+ /// The synthetic call is a plain `main()` (never `await main()`), even for `async fn main`: a blocking
193
+ /// server main never returns anyway, and the interpreter cannot `await` a user async function's result
194
+ /// (a separate pre-existing gap), so an unwrapped call is the one form that behaves identically on
195
+ /// every backend.
196
+ ///
197
+ /// Note: this is a tish-specific divergence from JS/node (node does not auto-call `main`); test it via
198
+ /// interp==vm==native cross-checks, and pair any `.tish` fixture with a `.js` that calls `main()`
199
+ /// explicitly so the node reference matches.
200
+ pub fn append_main_entry(statements: &mut Vec<Statement>) {
201
+ // A top-level `fn main` / `async fn main` declaration.
202
+ let has_main = statements.iter().any(|s| {
203
+ matches!(s, Statement::FunDecl { name, .. } if &**name == "main")
204
+ });
205
+ if !has_main {
206
+ return;
207
+ }
208
+ // Already invoked at top level? Then the user is driving it — don't double-call.
209
+ fn is_main_call(expr: &Expr) -> bool {
210
+ match expr {
211
+ Expr::Call { callee, .. } => {
212
+ matches!(callee.as_ref(), Expr::Ident { name, .. } if &**name == "main")
213
+ }
214
+ Expr::Await { operand, .. } => is_main_call(operand),
215
+ _ => false,
216
+ }
217
+ }
218
+ let already_called = statements
219
+ .iter()
220
+ .any(|s| matches!(s, Statement::ExprStmt { expr, .. } if is_main_call(expr)));
221
+ if already_called {
222
+ return;
223
+ }
224
+ let span = Span::default();
225
+ let call = Expr::Call {
226
+ callee: Box::new(Expr::Ident {
227
+ name: Arc::from("main"),
228
+ span,
229
+ }),
230
+ args: Vec::new(),
231
+ span,
232
+ };
233
+ statements.push(Statement::ExprStmt { expr: call, span });
234
+ }
235
+
176
236
  #[derive(Debug, Clone, PartialEq)]
177
237
  pub enum Statement {
178
238
  Block {
@@ -231,6 +291,16 @@ pub enum Statement {
231
291
  body: Box<Statement>,
232
292
  span: Span,
233
293
  },
294
+ /// `for (let k in object)` — iterates the object's own enumerable keys (JS `for-in` semantics).
295
+ /// Distinct from `ForOf` (which iterates values). Kept as a first-class node so every backend
296
+ /// enumerates keys consistently and `tish fmt` round-trips `for-in`.
297
+ ForIn {
298
+ name: Arc<str>,
299
+ name_span: Span,
300
+ object: Expr,
301
+ body: Box<Statement>,
302
+ span: Span,
303
+ },
234
304
  Return {
235
305
  value: Option<Expr>,
236
306
  span: Span,
@@ -431,6 +501,9 @@ pub enum Expr {
431
501
  },
432
502
  /// Arrow function: (params) => body
433
503
  ArrowFunction {
504
+ /// `true` for `async () => …` (#428) — the body runs in async context (`await` allowed) and
505
+ /// the JS target emits an `async` prefix. Non-async arrows have `async_: false`.
506
+ async_: bool,
434
507
  params: Vec<FunParam>,
435
508
  body: ArrowBody,
436
509
  span: Span,
@@ -653,6 +726,7 @@ impl Statement {
653
726
  | Statement::While { span, .. }
654
727
  | Statement::For { span, .. }
655
728
  | Statement::ForOf { span, .. }
729
+ | Statement::ForIn { span, .. }
656
730
  | Statement::Return { span, .. }
657
731
  | Statement::Break { span, .. }
658
732
  | Statement::Continue { span, .. }
@@ -11,6 +11,7 @@ repository = { workspace = true }
11
11
  ahash = "0.8.11"
12
12
  indexmap = "2"
13
13
  rand = "0.10.1"
14
+ unicode-normalization = "0.1.25"
14
15
  tishlang_core = { path = "../tish_core", version = ">=0.1" }
15
16
 
16
17
  [features]