@tishlang/tish 1.1.3 → 1.3.1
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/README.md +5 -5
- package/bin/.gitkeep +0 -0
- package/bin/tish +0 -0
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/src/main.rs +156 -41
- package/crates/tish/tests/integration_test.rs +12 -12
- package/crates/tish_builtins/src/construct.rs +1 -1
- package/crates/tish_compile/src/lib.rs +1 -1
- package/crates/tish_compile/src/resolve.rs +53 -0
- package/crates/tish_cranelift_runtime/src/lib.rs +1 -1
- package/crates/tish_eval/src/eval.rs +1 -1
- package/justfile +9 -9
- package/package.json +7 -3
- 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
- package/scripts/install-bin.js +36 -0
- package/bin/tish.js +0 -32
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @tishlang/tish
|
|
2
2
|
|
|
3
|
-
[Tish](https://github.com/tishlang/tish) is a minimal TypeScript/JavaScript–compatible language: run with an interpreter, use a REPL, or
|
|
3
|
+
[Tish](https://github.com/tishlang/tish) is a minimal TypeScript/JavaScript–compatible language: run with an interpreter, use a REPL, or build native binaries and other targets.
|
|
4
4
|
|
|
5
|
-
This npm package ships the **Tish CLI
|
|
5
|
+
This npm package ships the **Tish CLI**. It includes platform-specific native binaries under `platform/`; **`npm install`** runs `postinstall`, which copies the correct binary to `bin/tish`. That file is what runs when you invoke `tish` — the CLI itself is native, not Node. Node **22+** is required for install scripts and tooling (e.g. semantic-release in this repo); the `tish` binary has no Node runtime dependency.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -25,14 +25,14 @@ npx @tishlang/tish hello.tish
|
|
|
25
25
|
npx @tishlang/tish run src/main.tish
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Build a native executable:
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
npx @tishlang/tish
|
|
31
|
+
npx @tishlang/tish build app.tish -o app
|
|
32
32
|
./app
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
Native
|
|
35
|
+
Native builds use the Rust backend by default (requires [Rust](https://rustup.rs) and `cargo` on your PATH). The package includes the Tish workspace source (`Cargo.toml`, `crates/`, `justfile`) so `tish build` can run `cargo build` for your program. For pure Tish without native imports, use `--native-backend cranelift` (no Rust toolchain needed).
|
|
36
36
|
|
|
37
37
|
Start the REPL:
|
|
38
38
|
|
package/bin/.gitkeep
ADDED
|
File without changes
|
package/bin/tish
ADDED
|
Binary file
|
package/crates/tish/Cargo.toml
CHANGED
package/crates/tish/src/main.rs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
//! Tish CLI - run, REPL,
|
|
1
|
+
//! Tish CLI - run, REPL, build to native or other targets.
|
|
2
2
|
|
|
3
3
|
mod repl_completion;
|
|
4
4
|
|
|
5
5
|
use std::cell::RefCell;
|
|
6
6
|
use std::fs;
|
|
7
|
-
use std::io::{self, IsTerminal, Write};
|
|
7
|
+
use std::io::{self, IsTerminal, Read, Write};
|
|
8
8
|
use std::path::{Path, PathBuf};
|
|
9
9
|
use std::rc::Rc;
|
|
10
10
|
|
|
@@ -23,7 +23,8 @@ pub(crate) struct Cli {
|
|
|
23
23
|
|
|
24
24
|
#[derive(Parser)]
|
|
25
25
|
struct RunArgs {
|
|
26
|
-
|
|
26
|
+
/// Path to a `.tish` file, or `-` to read the program from stdin (like `node -`).
|
|
27
|
+
#[arg(required = true, allow_hyphen_values = true)]
|
|
27
28
|
file: String,
|
|
28
29
|
#[arg(long, default_value = "vm")]
|
|
29
30
|
backend: String,
|
|
@@ -45,7 +46,7 @@ struct ReplArgs {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
#[derive(Parser)]
|
|
48
|
-
struct
|
|
49
|
+
struct BuildArgs {
|
|
49
50
|
#[arg(short, long, default_value = "tish_out")]
|
|
50
51
|
output: String,
|
|
51
52
|
#[arg(long, default_value = "native")]
|
|
@@ -66,8 +67,8 @@ pub(crate) enum Commands {
|
|
|
66
67
|
Run(RunArgs),
|
|
67
68
|
/// Interactive REPL
|
|
68
69
|
Repl(ReplArgs),
|
|
69
|
-
///
|
|
70
|
-
|
|
70
|
+
/// Build native binary, wasm, wasi, or JavaScript output
|
|
71
|
+
Build(BuildArgs),
|
|
71
72
|
/// Parse and dump AST
|
|
72
73
|
#[command(name = "dump-ast")]
|
|
73
74
|
DumpAst {
|
|
@@ -76,15 +77,42 @@ pub(crate) enum Commands {
|
|
|
76
77
|
},
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
/// `tish script.tish` → insert `run` so it matches `tish run script.tish` (npx / npm UX).
|
|
81
|
+
fn argv_with_implicit_run(mut argv: Vec<String>) -> Vec<String> {
|
|
82
|
+
if argv.len() >= 2 {
|
|
83
|
+
let first = argv[1].as_str();
|
|
84
|
+
const SUBCOMMANDS: &[&str] = &["run", "repl", "build", "dump-ast"];
|
|
85
|
+
let looks_like_file =
|
|
86
|
+
!first.starts_with('-') && !SUBCOMMANDS.iter().any(|&s| s == first);
|
|
87
|
+
if looks_like_file {
|
|
88
|
+
argv.insert(1, "run".to_string());
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
argv
|
|
92
|
+
}
|
|
93
|
+
|
|
79
94
|
fn main() {
|
|
80
|
-
let cli = Cli::parse();
|
|
81
95
|
let no_opt_env = std::env::var_os("TISH_NO_OPTIMIZE")
|
|
82
96
|
.map(|v| v == "1" || v == "true" || v == "yes")
|
|
83
97
|
.unwrap_or(false);
|
|
98
|
+
|
|
99
|
+
// `tish -` (like `node -` / `bun -`); clap would treat `-` as an invalid subcommand.
|
|
100
|
+
let argv: Vec<String> = std::env::args().collect();
|
|
101
|
+
if argv.len() == 2 && argv[1] == "-" {
|
|
102
|
+
let result = run_stdin_pipe("vm", &[], no_opt_env, true);
|
|
103
|
+
if let Err(e) = result {
|
|
104
|
+
eprintln!("Error: {}", e);
|
|
105
|
+
std::process::exit(1);
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let argv = argv_with_implicit_run(argv);
|
|
111
|
+
let cli = Cli::parse_from(argv);
|
|
84
112
|
let result = match cli.command {
|
|
85
113
|
Some(Commands::Run(a)) => run_file(&a.file, &a.backend, &a.features, a.no_optimize || no_opt_env),
|
|
86
114
|
Some(Commands::Repl(a)) => run_repl(&a.backend, a.no_optimize || no_opt_env),
|
|
87
|
-
Some(Commands::
|
|
115
|
+
Some(Commands::Build(a)) => build_file(
|
|
88
116
|
&a.file,
|
|
89
117
|
&a.output,
|
|
90
118
|
&a.target,
|
|
@@ -93,7 +121,14 @@ fn main() {
|
|
|
93
121
|
a.no_optimize || no_opt_env,
|
|
94
122
|
),
|
|
95
123
|
Some(Commands::DumpAst { file }) => dump_ast(&file),
|
|
96
|
-
None =>
|
|
124
|
+
None => {
|
|
125
|
+
if io::stdin().is_terminal() {
|
|
126
|
+
run_repl("vm", no_opt_env)
|
|
127
|
+
} else {
|
|
128
|
+
// `echo '...' | tish` — run script from stdin (Bun-style)
|
|
129
|
+
run_stdin_pipe("vm", &[], no_opt_env, false)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
97
132
|
};
|
|
98
133
|
|
|
99
134
|
if let Err(e) = result {
|
|
@@ -102,49 +137,97 @@ fn main() {
|
|
|
102
137
|
}
|
|
103
138
|
}
|
|
104
139
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
140
|
+
/// Read stdin and run as Tish. If `fail_on_empty`, `tish run -` / `tish -` get an error; if false, empty stdin exits 0.
|
|
141
|
+
fn run_stdin_pipe(
|
|
142
|
+
backend: &str,
|
|
143
|
+
features: &[String],
|
|
144
|
+
no_optimize: bool,
|
|
145
|
+
fail_on_empty: bool,
|
|
146
|
+
) -> Result<(), String> {
|
|
147
|
+
let mut source = String::new();
|
|
148
|
+
io::stdin()
|
|
149
|
+
.read_to_string(&mut source)
|
|
150
|
+
.map_err(|e| format!("Cannot read stdin: {}", e))?;
|
|
151
|
+
if source.trim().is_empty() {
|
|
152
|
+
if fail_on_empty {
|
|
153
|
+
return Err(
|
|
154
|
+
"No source on stdin. Example: echo 'console.log(1)' | tish or tish run -".into(),
|
|
155
|
+
);
|
|
112
156
|
}
|
|
113
|
-
|
|
157
|
+
return Ok(());
|
|
158
|
+
}
|
|
159
|
+
run_stdin_source(&source, backend, features, no_optimize)
|
|
160
|
+
}
|
|
114
161
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
162
|
+
fn run_stdin_source(
|
|
163
|
+
source: &str,
|
|
164
|
+
backend: &str,
|
|
165
|
+
_features: &[String],
|
|
166
|
+
no_optimize: bool,
|
|
167
|
+
) -> Result<(), String> {
|
|
168
|
+
let cwd = std::env::current_dir().map_err(|e| e.to_string())?;
|
|
169
|
+
let modules = tishlang_compile::resolve_project_from_stdin(source, &cwd)?;
|
|
170
|
+
tishlang_compile::detect_cycles(&modules)?;
|
|
171
|
+
let prog = tishlang_compile::merge_modules(modules)?;
|
|
172
|
+
let program = if no_optimize {
|
|
173
|
+
prog
|
|
174
|
+
} else {
|
|
175
|
+
tishlang_opt::optimize(&prog)
|
|
176
|
+
};
|
|
177
|
+
run_program(&program, backend, no_optimize)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fn run_file(path: &str, backend: &str, features: &[String], no_optimize: bool) -> Result<(), String> {
|
|
181
|
+
let program = if path == "-" {
|
|
182
|
+
return run_stdin_pipe(backend, features, no_optimize, true);
|
|
123
183
|
} else {
|
|
124
|
-
let
|
|
125
|
-
|
|
126
|
-
let
|
|
127
|
-
|
|
128
|
-
|
|
184
|
+
let path =
|
|
185
|
+
Path::new(path).canonicalize().map_err(|e| format!("Cannot resolve {}: {}", path, e))?;
|
|
186
|
+
let project_root = path.parent().and_then(|p| {
|
|
187
|
+
if p.file_name().and_then(|n| n.to_str()) == Some("src") {
|
|
188
|
+
p.parent()
|
|
189
|
+
} else {
|
|
190
|
+
Some(p)
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if path.extension().map(|e| e == "js") == Some(true) {
|
|
195
|
+
let prog = tishlang_js_to_tish::convert(&fs::read_to_string(&path).map_err(|e| format!("{}", e))?)
|
|
196
|
+
.map_err(|e| format!("{}", e))?;
|
|
197
|
+
if no_optimize {
|
|
198
|
+
prog
|
|
199
|
+
} else {
|
|
200
|
+
tishlang_opt::optimize(&prog)
|
|
201
|
+
}
|
|
129
202
|
} else {
|
|
130
|
-
|
|
203
|
+
let modules = tishlang_compile::resolve_project(&path, project_root)?;
|
|
204
|
+
tishlang_compile::detect_cycles(&modules)?;
|
|
205
|
+
let prog = tishlang_compile::merge_modules(modules)?;
|
|
206
|
+
if no_optimize {
|
|
207
|
+
prog
|
|
208
|
+
} else {
|
|
209
|
+
tishlang_opt::optimize(&prog)
|
|
210
|
+
}
|
|
131
211
|
}
|
|
132
212
|
};
|
|
133
213
|
|
|
214
|
+
run_program(&program, backend, no_optimize)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
fn run_program(program: &tishlang_ast::Program, backend: &str, no_optimize: bool) -> Result<(), String> {
|
|
134
218
|
if backend == "interp" {
|
|
135
219
|
let mut eval = tishlang_eval::Evaluator::new();
|
|
136
|
-
let value = eval.eval_program(
|
|
220
|
+
let value = eval.eval_program(program)?;
|
|
137
221
|
if !matches!(value, tishlang_eval::Value::Null) {
|
|
138
222
|
println!("{}", tishlang_eval::format_value_for_console(&value, tishlang_core::use_console_colors()));
|
|
139
223
|
}
|
|
140
224
|
return Ok(());
|
|
141
225
|
}
|
|
142
226
|
|
|
143
|
-
// VM backend (bytecode) - supports native imports when built with fs/http/process features
|
|
144
227
|
let chunk = if no_optimize {
|
|
145
|
-
tishlang_bytecode::compile_unoptimized(
|
|
228
|
+
tishlang_bytecode::compile_unoptimized(program).map_err(|e| e.to_string())?
|
|
146
229
|
} else {
|
|
147
|
-
tishlang_bytecode::compile(
|
|
230
|
+
tishlang_bytecode::compile(program).map_err(|e| e.to_string())?
|
|
148
231
|
};
|
|
149
232
|
let value = tishlang_vm::run(&chunk)?;
|
|
150
233
|
if !matches!(value, tishlang_core::Value::Null) {
|
|
@@ -385,7 +468,7 @@ fn compile_to_js(input_path: &Path, output_path: &str, optimize: bool) -> Result
|
|
|
385
468
|
}
|
|
386
469
|
|
|
387
470
|
#[allow(clippy::vec_init_then_push)]
|
|
388
|
-
fn
|
|
471
|
+
fn build_file(
|
|
389
472
|
input_path: &str,
|
|
390
473
|
output_path: &str,
|
|
391
474
|
target: &str,
|
|
@@ -509,13 +592,36 @@ fn compile_file(
|
|
|
509
592
|
mod cli_tests {
|
|
510
593
|
use clap::Parser;
|
|
511
594
|
|
|
512
|
-
use super::{Cli, Commands};
|
|
595
|
+
use super::{argv_with_implicit_run, Cli, Commands};
|
|
596
|
+
|
|
597
|
+
#[test]
|
|
598
|
+
fn implicit_run_inserts_run_before_file() {
|
|
599
|
+
let argv = argv_with_implicit_run(vec![
|
|
600
|
+
"tish".to_string(),
|
|
601
|
+
"hello.tish".to_string(),
|
|
602
|
+
]);
|
|
603
|
+
let cli = Cli::try_parse_from(argv).unwrap();
|
|
604
|
+
match cli.command {
|
|
605
|
+
Some(Commands::Run(a)) => assert_eq!(a.file, "hello.tish"),
|
|
606
|
+
_ => panic!("expected Run"),
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
#[test]
|
|
611
|
+
fn explicit_subcommand_not_treated_as_file() {
|
|
612
|
+
let argv = argv_with_implicit_run(vec![
|
|
613
|
+
"tish".to_string(),
|
|
614
|
+
"repl".to_string(),
|
|
615
|
+
]);
|
|
616
|
+
let cli = Cli::try_parse_from(argv).unwrap();
|
|
617
|
+
assert!(matches!(cli.command, Some(Commands::Repl(_))));
|
|
618
|
+
}
|
|
513
619
|
|
|
514
620
|
#[test]
|
|
515
|
-
fn
|
|
621
|
+
fn build_js_target_parses() {
|
|
516
622
|
let cli = Cli::try_parse_from([
|
|
517
623
|
"tish",
|
|
518
|
-
"
|
|
624
|
+
"build",
|
|
519
625
|
"m.tish",
|
|
520
626
|
"--target",
|
|
521
627
|
"js",
|
|
@@ -524,8 +630,17 @@ mod cli_tests {
|
|
|
524
630
|
])
|
|
525
631
|
.unwrap();
|
|
526
632
|
match cli.command {
|
|
527
|
-
Some(Commands::
|
|
528
|
-
_ => panic!("expected
|
|
633
|
+
Some(Commands::Build(a)) => assert_eq!(a.file, "m.tish"),
|
|
634
|
+
_ => panic!("expected Build"),
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
#[test]
|
|
639
|
+
fn run_stdin_marker_parses_as_file() {
|
|
640
|
+
let cli = Cli::try_parse_from(["tish", "run", "-"]).unwrap();
|
|
641
|
+
match cli.command {
|
|
642
|
+
Some(Commands::Run(a)) => assert_eq!(a.file, "-"),
|
|
643
|
+
_ => panic!("expected Run"),
|
|
529
644
|
}
|
|
530
645
|
}
|
|
531
646
|
}
|
|
@@ -40,7 +40,7 @@ fn target_dir() -> PathBuf {
|
|
|
40
40
|
.unwrap_or_else(|_| workspace_root().join("target"))
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/// Cache dir for tish
|
|
43
|
+
/// Cache dir for tish build outputs (under target/ so CI rust-cache restores it).
|
|
44
44
|
fn integration_compile_cache_dir() -> PathBuf {
|
|
45
45
|
target_dir().join("integration_compile_cache")
|
|
46
46
|
}
|
|
@@ -56,7 +56,7 @@ fn file_content_hash(path: &Path) -> u64 {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/// Compile a .tish file with the given backend, using a persistent cache so we only run
|
|
59
|
-
/// `tish
|
|
59
|
+
/// `tish build` when the file or backend changed. Returns path to the compiled artifact
|
|
60
60
|
/// (binary, .js, or .wasm) in a temp dir; caller may run it and then delete it.
|
|
61
61
|
///
|
|
62
62
|
/// Cache is keyed by backend (native, cranelift, js, wasi) so e.g. cranelift and wasi
|
|
@@ -73,7 +73,7 @@ fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
|
|
|
73
73
|
let ext = if cfg!(target_os = "windows") { ".exe" } else { "" };
|
|
74
74
|
let cached = cache_base.join(format!("{}_{}{}", stem, hash8, ext));
|
|
75
75
|
let args = vec![
|
|
76
|
-
OsString::from("
|
|
76
|
+
OsString::from("build"),
|
|
77
77
|
OsString::from(path),
|
|
78
78
|
OsString::from("-o"),
|
|
79
79
|
OsString::from(&cached),
|
|
@@ -84,7 +84,7 @@ fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
|
|
|
84
84
|
let ext = if cfg!(target_os = "windows") { ".exe" } else { "" };
|
|
85
85
|
let cached = cache_base.join(format!("{}_{}{}", stem, hash8, ext));
|
|
86
86
|
let args = vec![
|
|
87
|
-
OsString::from("
|
|
87
|
+
OsString::from("build"),
|
|
88
88
|
OsString::from(path),
|
|
89
89
|
OsString::from("-o"),
|
|
90
90
|
OsString::from(&cached),
|
|
@@ -96,7 +96,7 @@ fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
|
|
|
96
96
|
"js" => {
|
|
97
97
|
let cached = cache_base.join(format!("{}_{}.js", stem, hash8));
|
|
98
98
|
let args = vec![
|
|
99
|
-
OsString::from("
|
|
99
|
+
OsString::from("build"),
|
|
100
100
|
OsString::from(path),
|
|
101
101
|
OsString::from("--target"),
|
|
102
102
|
OsString::from("js"),
|
|
@@ -109,7 +109,7 @@ fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
|
|
|
109
109
|
let out_base = cache_base.join(format!("{}_{}", stem, hash8));
|
|
110
110
|
let artifact = out_base.with_extension("wasm");
|
|
111
111
|
let args = vec![
|
|
112
|
-
OsString::from("
|
|
112
|
+
OsString::from("build"),
|
|
113
113
|
OsString::from(path),
|
|
114
114
|
OsString::from("-o"),
|
|
115
115
|
OsString::from(&out_base),
|
|
@@ -126,7 +126,7 @@ fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
|
|
|
126
126
|
.args(compile_args)
|
|
127
127
|
.current_dir(workspace_root())
|
|
128
128
|
.output()
|
|
129
|
-
.expect("run tish
|
|
129
|
+
.expect("run tish build");
|
|
130
130
|
assert!(
|
|
131
131
|
out.status.success(),
|
|
132
132
|
"Compile failed for {} ({}): {}",
|
|
@@ -203,13 +203,13 @@ fn test_async_await_compile_via_binary() {
|
|
|
203
203
|
if path.exists() && bin.exists() {
|
|
204
204
|
let out = std::env::temp_dir().join("tish_async_test_out");
|
|
205
205
|
let compile_result = Command::new(&bin)
|
|
206
|
-
.args(["
|
|
206
|
+
.args(["build", path.to_string_lossy().as_ref(), "-o", out.to_string_lossy().as_ref()])
|
|
207
207
|
.current_dir(workspace_root())
|
|
208
208
|
.output();
|
|
209
|
-
let compile_out = compile_result.expect("run tish
|
|
209
|
+
let compile_out = compile_result.expect("run tish build");
|
|
210
210
|
assert!(
|
|
211
211
|
compile_out.status.success(),
|
|
212
|
-
"tish
|
|
212
|
+
"tish build failed: {}",
|
|
213
213
|
String::from_utf8_lossy(&compile_out.stderr)
|
|
214
214
|
);
|
|
215
215
|
// Run compiled binary to validate non-blocking fetchAll executes correctly
|
|
@@ -245,13 +245,13 @@ fn test_async_parallel_vs_sequential_timing() {
|
|
|
245
245
|
|
|
246
246
|
// Compile both
|
|
247
247
|
let compile_par = Command::new(&bin)
|
|
248
|
-
.args(["
|
|
248
|
+
.args(["build", parallel_src.to_string_lossy().as_ref(), "-o", out_parallel.to_string_lossy().as_ref()])
|
|
249
249
|
.current_dir(workspace_root())
|
|
250
250
|
.output();
|
|
251
251
|
assert!(compile_par.as_ref().unwrap().status.success(), "compile parallel: {}", String::from_utf8_lossy(&compile_par.as_ref().unwrap().stderr));
|
|
252
252
|
|
|
253
253
|
let compile_seq = Command::new(&bin)
|
|
254
|
-
.args(["
|
|
254
|
+
.args(["build", sequential_src.to_string_lossy().as_ref(), "-o", out_sequential.to_string_lossy().as_ref()])
|
|
255
255
|
.current_dir(workspace_root())
|
|
256
256
|
.output();
|
|
257
257
|
assert!(compile_seq.as_ref().unwrap().status.success(), "compile sequential: {}", String::from_utf8_lossy(&compile_seq.as_ref().unwrap().stderr));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//! `new` lowering for non-JS targets: `construct(callee, args)` approximates JS `[[Construct]]`.
|
|
2
|
-
//! Browser-exact behavior remains on `tish
|
|
2
|
+
//! Browser-exact behavior remains on `tish build --target js`.
|
|
3
3
|
|
|
4
4
|
use std::cell::RefCell;
|
|
5
5
|
use std::rc::Rc;
|
|
@@ -15,7 +15,7 @@ pub use codegen::CompileError;
|
|
|
15
15
|
pub use resolve::{
|
|
16
16
|
detect_cycles, extract_native_import_features, has_external_native_imports, has_native_imports,
|
|
17
17
|
is_builtin_native_spec, merge_modules, resolve_native_modules, resolve_project,
|
|
18
|
-
ResolvedNativeModule,
|
|
18
|
+
resolve_project_from_stdin, ResolvedNativeModule,
|
|
19
19
|
};
|
|
20
20
|
pub use types::{RustType, TypeContext};
|
|
21
21
|
|
|
@@ -242,6 +242,59 @@ pub fn resolve_project(
|
|
|
242
242
|
.collect())
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
/// Resolve modules when the entry program is read from stdin (`tish run -`).
|
|
246
|
+
/// Relative file imports resolve from `project_root` (typically [`std::env::current_dir()`]).
|
|
247
|
+
/// The synthetic entry path `<stdin>` is not a real file; dependencies load from disk as usual.
|
|
248
|
+
pub fn resolve_project_from_stdin(
|
|
249
|
+
source: &str,
|
|
250
|
+
project_root: &Path,
|
|
251
|
+
) -> Result<Vec<ResolvedModule>, String> {
|
|
252
|
+
let root_canon = project_root
|
|
253
|
+
.canonicalize()
|
|
254
|
+
.map_err(|e| format!("Cannot canonicalize project root {}: {}", project_root.display(), e))?;
|
|
255
|
+
|
|
256
|
+
let stdin_path = root_canon.join("<stdin>");
|
|
257
|
+
let program = tishlang_parser::parse(source)
|
|
258
|
+
.map_err(|e| format!("Parse error (stdin): {}", e))?;
|
|
259
|
+
|
|
260
|
+
let mut visited = HashSet::new();
|
|
261
|
+
let mut path_to_module: HashMap<PathBuf, Program> = HashMap::new();
|
|
262
|
+
let mut load_order: Vec<PathBuf> = Vec::new();
|
|
263
|
+
|
|
264
|
+
let from_dir = stdin_path
|
|
265
|
+
.parent()
|
|
266
|
+
.unwrap_or_else(|| Path::new("."));
|
|
267
|
+
|
|
268
|
+
for stmt in &program.statements {
|
|
269
|
+
if let Statement::Import { from, .. } = stmt {
|
|
270
|
+
if is_native_import(from) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
let dep_path = resolve_import_path(from, from_dir, &root_canon)?;
|
|
274
|
+
if !path_to_module.contains_key(&dep_path) {
|
|
275
|
+
load_module_recursive(
|
|
276
|
+
&dep_path,
|
|
277
|
+
&root_canon,
|
|
278
|
+
&mut visited,
|
|
279
|
+
&mut path_to_module,
|
|
280
|
+
&mut load_order,
|
|
281
|
+
)?;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
path_to_module.insert(stdin_path.clone(), program);
|
|
287
|
+
load_order.push(stdin_path);
|
|
288
|
+
|
|
289
|
+
Ok(load_order
|
|
290
|
+
.into_iter()
|
|
291
|
+
.map(|p| {
|
|
292
|
+
let program = path_to_module.remove(&p).unwrap();
|
|
293
|
+
ResolvedModule { path: p, program }
|
|
294
|
+
})
|
|
295
|
+
.collect())
|
|
296
|
+
}
|
|
297
|
+
|
|
245
298
|
fn load_module_recursive(
|
|
246
299
|
module_path: &Path,
|
|
247
300
|
project_root: &Path,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//! Runtime linked into the `tish
|
|
1
|
+
//! Runtime linked into the `tish build --native-backend cranelift` executable.
|
|
2
2
|
//!
|
|
3
3
|
//! **`tish_run_chunk`** deserializes embedded bytecode and runs **`tishlang_vm`** — the same
|
|
4
4
|
//! execution engine as `tish run --backend vm`. The crate name is historical; this is not
|
|
@@ -1593,7 +1593,7 @@ impl Evaluator {
|
|
|
1593
1593
|
self.construct_value(&c, &arg_vals)
|
|
1594
1594
|
}
|
|
1595
1595
|
Expr::JsxElement { .. } | Expr::JsxFragment { .. } => Err(EvalError::Error(
|
|
1596
|
-
"JSX is not supported in the interpreter. Use 'tish
|
|
1596
|
+
"JSX is not supported in the interpreter. Use 'tish build --target js' to compile to JavaScript.".to_string(),
|
|
1597
1597
|
)),
|
|
1598
1598
|
Expr::NativeModuleLoad { spec, export_name, .. } => {
|
|
1599
1599
|
self.load_builtin_export(spec.as_ref(), export_name.as_ref())
|
package/justfile
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
# just run run hello.tish # runs via interpreter
|
|
10
10
|
# tish run hello.tish # if tish is in PATH
|
|
11
11
|
#
|
|
12
|
-
# 2. BUILD (
|
|
13
|
-
# just run
|
|
12
|
+
# 2. BUILD (native binary) - Create standalone executable:
|
|
13
|
+
# just run build hello.tish -o hello # builds native binary
|
|
14
14
|
# ./hello # run the standalone binary
|
|
15
15
|
#
|
|
16
16
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -72,17 +72,17 @@ run-regex *ARGS:
|
|
|
72
72
|
cargo run --release --no-default-features --features regex -- {{ARGS}}
|
|
73
73
|
|
|
74
74
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
75
|
-
#
|
|
75
|
+
# BUILD TISH PROGRAMS TO NATIVE BINARIES (just recipe name: compile → invokes `tish build`)
|
|
76
76
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
77
77
|
|
|
78
|
-
#
|
|
78
|
+
# Build a .tish file to native binary (with all features)
|
|
79
79
|
# Usage: just compile hello.tish hello
|
|
80
80
|
compile INPUT OUTPUT:
|
|
81
|
-
cargo run --release --features full --
|
|
81
|
+
cargo run --release --features full -- build {{INPUT}} -o {{OUTPUT}}
|
|
82
82
|
|
|
83
83
|
# Compile with secure mode (no dangerous features)
|
|
84
84
|
compile-secure INPUT OUTPUT:
|
|
85
|
-
cargo run --release --no-default-features --
|
|
85
|
+
cargo run --release --no-default-features -- build {{INPUT}} -o {{OUTPUT}}
|
|
86
86
|
|
|
87
87
|
# Build compiler WASM (for playground, REPL, try-it). Output: tish_compiler.js, tish_compiler_bg.wasm
|
|
88
88
|
# Requires: rustup target add wasm32-unknown-unknown, cargo install wasm-bindgen-cli
|
|
@@ -94,17 +94,17 @@ build-compiler-wasm OUT_DIR:
|
|
|
94
94
|
# Compile to WebAssembly (browser) - produces .wasm, .js, .html
|
|
95
95
|
# Requires: rustup target add wasm32-unknown-unknown, cargo install wasm-bindgen-cli
|
|
96
96
|
compile-wasm INPUT OUTPUT:
|
|
97
|
-
cargo run --release --
|
|
97
|
+
cargo run --release -- build {{INPUT}} -o {{OUTPUT}} --target wasm
|
|
98
98
|
|
|
99
99
|
# Compile to WebAssembly (Wasmtime/WASI) - single .wasm, run with: wasmtime OUTPUT.wasm
|
|
100
100
|
# Requires: rustup target add wasm32-wasip1, wasmtime (curl -sSf https://wasmtime.dev/install.sh | bash)
|
|
101
101
|
compile-wasi INPUT OUTPUT:
|
|
102
|
-
cargo run --release --
|
|
102
|
+
cargo run --release -- build {{INPUT}} -o {{OUTPUT}} --target wasi
|
|
103
103
|
|
|
104
104
|
# Compile with specific features
|
|
105
105
|
# Usage: just compile-with "http fs" hello.tish hello
|
|
106
106
|
compile-with FEATURES INPUT OUTPUT:
|
|
107
|
-
cargo run --release --no-default-features --features "{{FEATURES}}" --
|
|
107
|
+
cargo run --release --no-default-features --features "{{FEATURES}}" -- build {{INPUT}} -o {{OUTPUT}}
|
|
108
108
|
|
|
109
109
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
110
110
|
# TESTS
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tishlang/tish",
|
|
3
|
-
"version": "1.1
|
|
4
|
-
"description": "Tish - minimal TS/JS-compatible language. Run, REPL,
|
|
3
|
+
"version": "1.3.1",
|
|
4
|
+
"description": "Tish - minimal TS/JS-compatible language. Run, REPL, build to native or other targets.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -11,10 +11,14 @@
|
|
|
11
11
|
"access": "public"
|
|
12
12
|
},
|
|
13
13
|
"bin": {
|
|
14
|
-
"tish": "./bin/tish
|
|
14
|
+
"tish": "./bin/tish"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"postinstall": "node scripts/install-bin.js"
|
|
15
18
|
},
|
|
16
19
|
"files": [
|
|
17
20
|
"bin",
|
|
21
|
+
"scripts/install-bin.js",
|
|
18
22
|
"platform",
|
|
19
23
|
"README.md",
|
|
20
24
|
"LICENSE",
|
|
Binary file
|
package/platform/darwin-x64/tish
CHANGED
|
Binary file
|
|
Binary file
|
package/platform/linux-x64/tish
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Copy platform/<os>-<arch>/tish to bin/tish so the npm bin is the native binary.
|
|
6
|
+
* Runs on postinstall and before npm pack in CI.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const platformKey = `${process.platform}-${process.arch}`;
|
|
13
|
+
const binaryName = process.platform === 'win32' ? 'tish.exe' : 'tish';
|
|
14
|
+
|
|
15
|
+
const root = path.join(__dirname, '..');
|
|
16
|
+
const src = path.join(root, 'platform', platformKey, binaryName);
|
|
17
|
+
const dest = path.join(root, 'bin', 'tish');
|
|
18
|
+
|
|
19
|
+
if (!fs.existsSync(src)) {
|
|
20
|
+
if (process.env.TISH_NPM_PACK_REQUIRE === '1') {
|
|
21
|
+
console.error(`[tish] No prebuilt binary for this platform: ${platformKey}`);
|
|
22
|
+
console.error(`[tish] Expected: ${src}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
console.warn(`[tish] Skipping native bin (not found for ${platformKey}): ${src}`);
|
|
26
|
+
console.warn('[tish] From repo root run: ./npm/scripts/build-binaries.sh — then: node npm/tish/scripts/install-bin.js');
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
31
|
+
fs.copyFileSync(src, dest);
|
|
32
|
+
try {
|
|
33
|
+
fs.chmodSync(dest, 0o755);
|
|
34
|
+
} catch (_) {
|
|
35
|
+
/* Windows may ignore chmod */
|
|
36
|
+
}
|
package/bin/tish.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const { spawnSync } = require('child_process');
|
|
7
|
-
|
|
8
|
-
const platformKey = `${process.platform}-${process.arch}`;
|
|
9
|
-
const supported = ['darwin-arm64', 'darwin-x64', 'linux-x64', 'linux-arm64', 'win32-x64'];
|
|
10
|
-
const binaryName = process.platform === 'win32' ? 'tish.exe' : 'tish';
|
|
11
|
-
|
|
12
|
-
const binPath = path.join(__dirname, '..', 'platform', platformKey, binaryName);
|
|
13
|
-
if (!fs.existsSync(binPath)) {
|
|
14
|
-
console.error(`[tish] Unsupported or missing binary for platform: ${platformKey}`);
|
|
15
|
-
console.error('Supported:', supported.join(', '));
|
|
16
|
-
console.error('Build from source: https://github.com/tishlang/tish');
|
|
17
|
-
process.exit(1);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Convenience: "npx @tishlang/tish FILE [args]" → "tish run FILE [args]"
|
|
21
|
-
const args = process.argv.slice(2);
|
|
22
|
-
const subcommands = ['run', 'repl', 'compile', 'dump-ast'];
|
|
23
|
-
const first = args[0];
|
|
24
|
-
const looksLikeFile = first && !first.startsWith('-') && !subcommands.includes(first);
|
|
25
|
-
|
|
26
|
-
const finalArgs = looksLikeFile ? ['run', ...args] : args;
|
|
27
|
-
|
|
28
|
-
const result = spawnSync(binPath, finalArgs, {
|
|
29
|
-
stdio: 'inherit',
|
|
30
|
-
windowsHide: true,
|
|
31
|
-
});
|
|
32
|
-
process.exit(result.status !== null ? result.status : 1);
|