@tishlang/tish 1.6.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 +2 -0
- package/README.md +2 -0
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/error.rs +2 -8
- package/crates/js_to_tish/src/transform/expr.rs +128 -137
- package/crates/js_to_tish/src/transform/stmt.rs +62 -32
- package/crates/tish/Cargo.toml +15 -5
- package/crates/tish/src/cargo_native_registry.rs +29 -0
- package/crates/tish/src/cli_help.rs +92 -39
- package/crates/tish/src/main.rs +172 -86
- package/crates/tish/src/repl_completion.rs +3 -3
- package/crates/tish/tests/cargo_example_compile.rs +4 -2
- package/crates/tish/tests/integration_test.rs +216 -54
- package/crates/tish/tests/run_optimize_stdout_parity.rs +3 -7
- package/crates/tish/tests/shortcircuit.rs +20 -5
- package/crates/tish_ast/src/ast.rs +92 -23
- package/crates/tish_build_utils/Cargo.toml +4 -0
- package/crates/tish_build_utils/src/lib.rs +136 -8
- package/crates/tish_builtins/Cargo.toml +5 -1
- package/crates/tish_builtins/src/array.rs +65 -33
- package/crates/tish_builtins/src/construct.rs +34 -39
- package/crates/tish_builtins/src/globals.rs +42 -26
- package/crates/tish_builtins/src/helpers.rs +2 -1
- package/crates/tish_builtins/src/lib.rs +5 -5
- package/crates/tish_builtins/src/math.rs +5 -3
- package/crates/tish_builtins/src/object.rs +3 -2
- package/crates/tish_builtins/src/string.rs +144 -22
- package/crates/tish_bytecode/src/chunk.rs +0 -1
- package/crates/tish_bytecode/src/compiler.rs +173 -71
- package/crates/tish_bytecode/src/opcode.rs +24 -6
- package/crates/tish_bytecode/src/peephole.rs +2 -2
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/codegen.rs +1621 -453
- package/crates/tish_compile/src/infer.rs +75 -19
- package/crates/tish_compile/src/lib.rs +19 -8
- package/crates/tish_compile/src/resolve.rs +278 -137
- package/crates/tish_compile/src/types.rs +184 -24
- package/crates/tish_compile_js/Cargo.toml +1 -0
- package/crates/tish_compile_js/src/codegen.rs +181 -37
- package/crates/tish_compile_js/src/lib.rs +3 -1
- package/crates/tish_compile_js/src/tests_jsx.rs +30 -6
- package/crates/tish_compiler_wasm/src/lib.rs +16 -13
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +69 -59
- package/crates/tish_core/Cargo.toml +8 -0
- package/crates/tish_core/src/json.rs +107 -56
- package/crates/tish_core/src/lib.rs +4 -2
- package/crates/tish_core/src/macros.rs +5 -5
- package/crates/tish_core/src/uri.rs +9 -6
- package/crates/tish_core/src/value.rs +145 -43
- package/crates/tish_core/src/vmref.rs +178 -0
- package/crates/tish_cranelift/src/link.rs +6 -9
- package/crates/tish_cranelift/src/lower.rs +14 -8
- package/crates/tish_eval/Cargo.toml +17 -2
- package/crates/tish_eval/src/eval.rs +474 -165
- package/crates/tish_eval/src/http.rs +61 -0
- package/crates/tish_eval/src/lib.rs +12 -8
- package/crates/tish_eval/src/natives.rs +136 -38
- package/crates/tish_eval/src/promise.rs +14 -8
- package/crates/tish_eval/src/timers.rs +28 -19
- package/crates/tish_eval/src/value.rs +17 -6
- package/crates/tish_eval/src/value_convert.rs +13 -5
- package/crates/tish_fmt/src/lib.rs +149 -43
- package/crates/tish_lexer/src/lib.rs +232 -63
- package/crates/tish_lexer/src/token.rs +10 -6
- package/crates/tish_llvm/src/lib.rs +17 -8
- 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 +504 -106
- package/crates/tish_native/src/build.rs +4 -8
- package/crates/tish_native/src/lib.rs +54 -21
- package/crates/tish_opt/src/lib.rs +84 -52
- package/crates/tish_parser/src/lib.rs +45 -13
- package/crates/tish_parser/src/parser.rs +505 -130
- 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 +1136 -145
- package/crates/tish_runtime/src/http_fetch.rs +38 -27
- 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 +375 -189
- package/crates/tish_runtime/src/promise.rs +199 -40
- 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 +65 -42
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +5 -4
- package/crates/tish_ui/src/jsx.rs +317 -27
- 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 +725 -281
- package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +11 -4
- package/crates/tish_wasm/src/lib.rs +55 -42
- 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 +26 -0
- package/crates/tishlang_cargo_bindgen/src/classify.rs +265 -0
- package/crates/tishlang_cargo_bindgen/src/discover.rs +120 -0
- package/crates/tishlang_cargo_bindgen/src/infer.rs +372 -0
- package/crates/tishlang_cargo_bindgen/src/lib.rs +350 -0
- package/crates/tishlang_cargo_bindgen/src/main.rs +164 -0
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +114 -0
- 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
|
@@ -41,7 +41,10 @@ fn string_strict_eq_logical_or_inside_ternary_repl_last_expr() {
|
|
|
41
41
|
let opt = tishlang_opt::optimize(&tishlang_parser::parse(src).expect("parse"));
|
|
42
42
|
let v_peep = run_chunk(&compile_for_repl(&opt).expect("compile repl"));
|
|
43
43
|
let v_unopt = run_chunk(&compile_for_repl_unoptimized(&opt).expect("compile repl unopt"));
|
|
44
|
-
assert!(
|
|
44
|
+
assert!(
|
|
45
|
+
v_peep.strict_eq(&v_unopt),
|
|
46
|
+
"peep={v_peep:?} unopt={v_unopt:?}"
|
|
47
|
+
);
|
|
45
48
|
assert!(
|
|
46
49
|
matches!(&v_peep, Value::Number(n) if *n == 1.0),
|
|
47
50
|
"expected 1, got {v_peep:?}"
|
|
@@ -62,7 +65,8 @@ fn logical_or_strict_eq_peephole_matches_unoptimized() {
|
|
|
62
65
|
);
|
|
63
66
|
|
|
64
67
|
let v_peep_repl = run_chunk(&compile_for_repl(&program).expect("compile repl"));
|
|
65
|
-
let v_raw_repl =
|
|
68
|
+
let v_raw_repl =
|
|
69
|
+
run_chunk(&compile_for_repl_unoptimized(&program).expect("compile repl unopt"));
|
|
66
70
|
assert!(
|
|
67
71
|
v_peep_repl.strict_eq(&v_raw_repl),
|
|
68
72
|
"repl: peep={v_peep_repl:?} raw={v_raw_repl:?}"
|
|
@@ -111,11 +115,14 @@ fn string_strict_eq_logical_or_peephole_matches_unoptimized() {
|
|
|
111
115
|
/// `tish run path/to/file.tish` uses merge_modules; ensure that matches plain parse for the fixture.
|
|
112
116
|
#[test]
|
|
113
117
|
fn merged_module_program_bytecode_matches_parse_for_string_or_fixture() {
|
|
114
|
-
let fixture =
|
|
118
|
+
let fixture =
|
|
119
|
+
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/or_string_cmd.tish");
|
|
115
120
|
let src = std::fs::read_to_string(&fixture).expect("read fixture");
|
|
116
121
|
let modules = tishlang_compile::resolve_project(&fixture, Some(fixture.parent().unwrap()))
|
|
117
122
|
.expect("resolve");
|
|
118
|
-
let merged = tishlang_compile::merge_modules(modules)
|
|
123
|
+
let merged = tishlang_compile::merge_modules(modules)
|
|
124
|
+
.expect("merge")
|
|
125
|
+
.program;
|
|
119
126
|
let flat = tishlang_parser::parse(&src).expect("parse");
|
|
120
127
|
let m_opt = tishlang_opt::optimize(&merged);
|
|
121
128
|
let f_opt = tishlang_opt::optimize(&flat);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
//! Compiles Tish to bytecode, then produces a .wasm VM binary + loader.
|
|
4
4
|
//! The VM runs in the browser; your program runs as serialized bytecode.
|
|
5
5
|
|
|
6
|
+
use std::collections::BTreeSet;
|
|
6
7
|
use std::path::Path;
|
|
7
8
|
use std::process::Command;
|
|
8
9
|
|
|
@@ -43,7 +44,9 @@ fn resolve_and_compile_to_chunk(
|
|
|
43
44
|
message: e.to_string(),
|
|
44
45
|
})?;
|
|
45
46
|
let program = {
|
|
46
|
-
let prog = merge_modules(modules)
|
|
47
|
+
let prog = merge_modules(modules)
|
|
48
|
+
.map(|m| m.program)
|
|
49
|
+
.map_err(|e| WasmError {
|
|
47
50
|
message: e.to_string(),
|
|
48
51
|
})?;
|
|
49
52
|
if optimize {
|
|
@@ -107,27 +110,33 @@ fn emit_wasm_from_chunk(chunk: &Chunk, output_path: &Path) -> Result<(), WasmErr
|
|
|
107
110
|
std::fs::create_dir_all(&out_dir_abs).map_err(|e| WasmError {
|
|
108
111
|
message: format!("Cannot create output directory: {}", e),
|
|
109
112
|
})?;
|
|
110
|
-
let workspace_root =
|
|
111
|
-
message: e
|
|
112
|
-
})?;
|
|
113
|
+
let workspace_root =
|
|
114
|
+
tishlang_build_utils::find_workspace_root().map_err(|e| WasmError { message: e })?;
|
|
113
115
|
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
|
|
114
116
|
let build_status = Command::new(&cargo)
|
|
115
117
|
.current_dir(&workspace_root)
|
|
116
118
|
.args([
|
|
117
|
-
"build",
|
|
118
|
-
"
|
|
119
|
-
"
|
|
119
|
+
"build",
|
|
120
|
+
"-p",
|
|
121
|
+
"tishlang_wasm_runtime",
|
|
122
|
+
"--target",
|
|
123
|
+
"wasm32-unknown-unknown",
|
|
124
|
+
"--release",
|
|
125
|
+
"--features",
|
|
126
|
+
"browser",
|
|
120
127
|
])
|
|
121
128
|
.status()
|
|
122
|
-
.map_err(|e| WasmError {
|
|
129
|
+
.map_err(|e| WasmError {
|
|
130
|
+
message: format!("Failed to run cargo: {}", e),
|
|
131
|
+
})?;
|
|
123
132
|
if !build_status.success() {
|
|
124
133
|
return Err(WasmError {
|
|
125
134
|
message: "Failed to build wasm runtime. Run: rustup target add wasm32-unknown-unknown"
|
|
126
135
|
.to_string(),
|
|
127
136
|
});
|
|
128
137
|
}
|
|
129
|
-
let wasm_artifact =
|
|
130
|
-
.join("target/wasm32-unknown-unknown/release/tishlang_wasm_runtime.wasm");
|
|
138
|
+
let wasm_artifact =
|
|
139
|
+
workspace_root.join("target/wasm32-unknown-unknown/release/tishlang_wasm_runtime.wasm");
|
|
131
140
|
if !wasm_artifact.exists() {
|
|
132
141
|
return Err(WasmError {
|
|
133
142
|
message: format!("Wasm artifact not found: {}", wasm_artifact.display()),
|
|
@@ -137,17 +146,25 @@ fn emit_wasm_from_chunk(chunk: &Chunk, output_path: &Path) -> Result<(), WasmErr
|
|
|
137
146
|
let out_name = stem.to_string();
|
|
138
147
|
let bindgen_status = Command::new(&wasm_bindgen)
|
|
139
148
|
.args([
|
|
140
|
-
"--target",
|
|
141
|
-
"
|
|
142
|
-
"--out-
|
|
149
|
+
"--target",
|
|
150
|
+
"web",
|
|
151
|
+
"--out-dir",
|
|
152
|
+
out_dir_abs.to_str().unwrap(),
|
|
153
|
+
"--out-name",
|
|
154
|
+
&out_name,
|
|
143
155
|
wasm_artifact.to_str().unwrap(),
|
|
144
156
|
])
|
|
145
157
|
.status()
|
|
146
158
|
.map_err(|e| WasmError {
|
|
147
|
-
message: format!(
|
|
159
|
+
message: format!(
|
|
160
|
+
"Failed to run wasm-bindgen: {}. Install with: cargo install wasm-bindgen-cli",
|
|
161
|
+
e
|
|
162
|
+
),
|
|
148
163
|
})?;
|
|
149
164
|
if !bindgen_status.success() {
|
|
150
|
-
return Err(WasmError {
|
|
165
|
+
return Err(WasmError {
|
|
166
|
+
message: "wasm-bindgen failed".to_string(),
|
|
167
|
+
});
|
|
151
168
|
}
|
|
152
169
|
let js_name = format!("{}.js", stem);
|
|
153
170
|
let html = format!(
|
|
@@ -171,7 +188,12 @@ run(chunk);
|
|
|
171
188
|
std::fs::write(&html_path, html).map_err(|e| WasmError {
|
|
172
189
|
message: format!("Cannot write {}: {}", html_path.display(), e),
|
|
173
190
|
})?;
|
|
174
|
-
println!(
|
|
191
|
+
println!(
|
|
192
|
+
"Built: {}_bg.wasm, {}.js, {}",
|
|
193
|
+
stem,
|
|
194
|
+
stem,
|
|
195
|
+
html_path.display()
|
|
196
|
+
);
|
|
175
197
|
Ok(())
|
|
176
198
|
}
|
|
177
199
|
|
|
@@ -208,7 +230,7 @@ pub fn compile_to_wasi(
|
|
|
208
230
|
let (chunk, program) = resolve_and_compile_to_chunk(entry_path, project_root, optimize)?;
|
|
209
231
|
if has_external_native_imports(&program) {
|
|
210
232
|
return Err(WasmError {
|
|
211
|
-
message: "WASI backend does not support external native imports (tish:egui, @scope/pkg). Built-in tish:fs, tish:http, tish:process are supported.".to_string(),
|
|
233
|
+
message: "WASI backend does not support external native imports (tish:egui, @scope/pkg). Built-in tish:fs, tish:http, tish:process, tish:timers are supported.".to_string(),
|
|
212
234
|
});
|
|
213
235
|
}
|
|
214
236
|
let wasi_features = extract_native_import_features(&program);
|
|
@@ -233,9 +255,8 @@ pub fn compile_to_wasi(
|
|
|
233
255
|
message: format!("Cannot create output directory: {}", e),
|
|
234
256
|
})?;
|
|
235
257
|
|
|
236
|
-
let workspace_root =
|
|
237
|
-
message: e
|
|
238
|
-
})?;
|
|
258
|
+
let workspace_root =
|
|
259
|
+
tishlang_build_utils::find_workspace_root().map_err(|e| WasmError { message: e })?;
|
|
239
260
|
|
|
240
261
|
// Create generated project: wasi_build/{stem}/
|
|
241
262
|
let build_dir = out_dir_abs.join("wasi_build").join(stem);
|
|
@@ -249,27 +270,25 @@ pub fn compile_to_wasi(
|
|
|
249
270
|
})?;
|
|
250
271
|
|
|
251
272
|
// Cargo.toml - path to tishlang_wasm_runtime (crate in crates/tish_wasm_runtime)
|
|
252
|
-
let runtime_path = workspace_root
|
|
253
|
-
.join("crates")
|
|
254
|
-
.join("tish_wasm_runtime");
|
|
273
|
+
let runtime_path = workspace_root.join("crates").join("tish_wasm_runtime");
|
|
255
274
|
let runtime_path_str = runtime_path
|
|
256
275
|
.canonicalize()
|
|
257
276
|
.unwrap_or(runtime_path)
|
|
258
277
|
.to_string_lossy()
|
|
259
278
|
.replace('\\', "/");
|
|
260
279
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
280
|
+
// Bundled perf (`tests/main.tish`) and many scripts use global setTimeout without a top-level
|
|
281
|
+
// `import` (so `extract_native_import_features` is empty). Always link timers for WASI VM.
|
|
282
|
+
let mut wasi_feature_set: BTreeSet<String> = wasi_features.into_iter().collect();
|
|
283
|
+
wasi_feature_set.insert("timers".to_string());
|
|
284
|
+
let features_str = format!(
|
|
285
|
+
", features = [{}]",
|
|
286
|
+
wasi_feature_set
|
|
287
|
+
.iter()
|
|
288
|
+
.map(|f| format!("{:?}", f))
|
|
289
|
+
.collect::<Vec<_>>()
|
|
290
|
+
.join(", ")
|
|
291
|
+
);
|
|
273
292
|
let cargo_toml = format!(
|
|
274
293
|
r#"[package]
|
|
275
294
|
name = "tish_wasi_{stem}"
|
|
@@ -314,12 +333,7 @@ fn main() {
|
|
|
314
333
|
let build_status = Command::new(&cargo)
|
|
315
334
|
.current_dir(&build_dir)
|
|
316
335
|
.env("CARGO_TARGET_DIR", &target_dir)
|
|
317
|
-
.args([
|
|
318
|
-
"build",
|
|
319
|
-
"--target",
|
|
320
|
-
"wasm32-wasip1",
|
|
321
|
-
"--release",
|
|
322
|
-
])
|
|
336
|
+
.args(["build", "--target", "wasm32-wasip1", "--release"])
|
|
323
337
|
.status()
|
|
324
338
|
.map_err(|e| WasmError {
|
|
325
339
|
message: format!("Failed to run cargo: {}", e),
|
|
@@ -355,4 +369,3 @@ fn main() {
|
|
|
355
369
|
);
|
|
356
370
|
Ok(())
|
|
357
371
|
}
|
|
358
|
-
|
|
@@ -12,10 +12,11 @@ crate-type = ["cdylib", "rlib"]
|
|
|
12
12
|
[features]
|
|
13
13
|
# For wasm32-unknown-unknown (browser): wasm-bindgen, console output
|
|
14
14
|
browser = ["dep:wasm-bindgen", "tishlang_vm/wasm"]
|
|
15
|
-
# Built-in modules for WASI (wasm32-wasip1): file I/O, process, http
|
|
15
|
+
# Built-in modules for WASI (wasm32-wasip1): file I/O, process, http, timers
|
|
16
16
|
fs = ["tishlang_vm/fs"]
|
|
17
17
|
process = ["tishlang_vm/process"]
|
|
18
18
|
http = ["tishlang_vm/http"]
|
|
19
|
+
timers = ["tishlang_vm/timers"]
|
|
19
20
|
|
|
20
21
|
[dependencies]
|
|
21
22
|
tishlang_bytecode = { path = "../tish_bytecode", version = ">=0.1" }
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//!
|
|
3
3
|
//! Two targets:
|
|
4
4
|
//! - **Browser** (wasm32-unknown-unknown): use `--features browser`, wasm-bindgen, console output
|
|
5
|
-
//! - **WASI/Wasmtime** (wasm32-
|
|
5
|
+
//! - **WASI/Wasmtime** (wasm32-wasip1): optional `timers` / `http` / … via Cargo features; `compile_to_wasi` enables `timers` by default.
|
|
6
6
|
|
|
7
7
|
use tishlang_bytecode::deserialize;
|
|
8
8
|
use tishlang_vm::Vm;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "tishlang_cargo_bindgen"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
description = "Generate Rust glue for Tish cargo: imports (bindgen-style, pre-commit friendly)"
|
|
6
|
+
license-file = { workspace = true }
|
|
7
|
+
repository = { workspace = true }
|
|
8
|
+
|
|
9
|
+
[[bin]]
|
|
10
|
+
name = "tishlang-cargo-bindgen"
|
|
11
|
+
path = "src/main.rs"
|
|
12
|
+
|
|
13
|
+
[lib]
|
|
14
|
+
name = "tishlang_cargo_bindgen"
|
|
15
|
+
path = "src/lib.rs"
|
|
16
|
+
|
|
17
|
+
[dependencies]
|
|
18
|
+
cargo_metadata = "0.19"
|
|
19
|
+
clap = { version = "4.6", features = ["derive", "color"] }
|
|
20
|
+
pathdiff = "0.2"
|
|
21
|
+
proc-macro2 = { version = "1.0", features = ["span-locations"] }
|
|
22
|
+
serde_json = "1"
|
|
23
|
+
syn = { version = "2.0", features = ["full", "extra-traits"] }
|
|
24
|
+
tempfile = "3"
|
|
25
|
+
toml = "0.8"
|
|
26
|
+
walkdir = "2"
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
//! Classify a `pub fn` from syn for glue emission (driven by signature shape, not crate name).
|
|
2
|
+
|
|
3
|
+
use syn::{
|
|
4
|
+
FnArg, GenericArgument, ItemFn, PathArguments, ReturnType, Type, TypeReference,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
8
|
+
pub enum SignatureClass {
|
|
9
|
+
/// `fn foo(args: &[Value]) -> Value` (or `tishlang_runtime::Value`).
|
|
10
|
+
TishValueAbi,
|
|
11
|
+
/// First parameter `&T` (or `&mut T`), `T: Serialize` (or `?Sized + Serialize`), returns `Result<String, _>`-like.
|
|
12
|
+
SerializeRefToResultString,
|
|
13
|
+
/// First parameter `&str` (or `& 'a str`), returns `Result<_, _>`, and has `Deserialize` bound on a type param.
|
|
14
|
+
DeserializeStrToResult,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
pub fn classify_public_fn(item: &ItemFn) -> Option<SignatureClass> {
|
|
18
|
+
if matches!(classify_tish_abi(item), Some(SignatureClass::TishValueAbi)) {
|
|
19
|
+
return Some(SignatureClass::TishValueAbi);
|
|
20
|
+
}
|
|
21
|
+
if is_deserialize_str_result(item) {
|
|
22
|
+
return Some(SignatureClass::DeserializeStrToResult);
|
|
23
|
+
}
|
|
24
|
+
if is_serialize_ref_to_result_string(item) {
|
|
25
|
+
return Some(SignatureClass::SerializeRefToResultString);
|
|
26
|
+
}
|
|
27
|
+
None
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fn classify_tish_abi(item: &ItemFn) -> Option<SignatureClass> {
|
|
31
|
+
let sig = &item.sig;
|
|
32
|
+
let mut value_args = 0;
|
|
33
|
+
for arg in &sig.inputs {
|
|
34
|
+
let FnArg::Typed(t) = arg else {
|
|
35
|
+
continue;
|
|
36
|
+
};
|
|
37
|
+
if is_slice_value(&t.ty) {
|
|
38
|
+
value_args += 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if value_args != 1 || sig.inputs.len() != 1 {
|
|
42
|
+
return None;
|
|
43
|
+
}
|
|
44
|
+
let Some(ret_ty) = return_type_inner(&sig.output) else {
|
|
45
|
+
return None;
|
|
46
|
+
};
|
|
47
|
+
if !is_value_type(ret_ty) {
|
|
48
|
+
return None;
|
|
49
|
+
}
|
|
50
|
+
Some(SignatureClass::TishValueAbi)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn return_type_inner(ret: &ReturnType) -> Option<&Type> {
|
|
54
|
+
match ret {
|
|
55
|
+
ReturnType::Default => None,
|
|
56
|
+
ReturnType::Type(_, ty) => Some(ty),
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fn is_slice_value(ty: &Type) -> bool {
|
|
61
|
+
let Some(inner) = strip_reference(ty) else {
|
|
62
|
+
return false;
|
|
63
|
+
};
|
|
64
|
+
let Type::Slice(s) = inner else {
|
|
65
|
+
return false;
|
|
66
|
+
};
|
|
67
|
+
is_value_type(&s.elem)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn strip_reference(ty: &Type) -> Option<&Type> {
|
|
71
|
+
match ty {
|
|
72
|
+
Type::Reference(TypeReference { elem, .. }) => Some(elem.as_ref()),
|
|
73
|
+
_ => None,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn is_value_type(ty: &Type) -> bool {
|
|
78
|
+
let Type::Path(p) = ty else {
|
|
79
|
+
return false;
|
|
80
|
+
};
|
|
81
|
+
let seg = p.path.segments.last();
|
|
82
|
+
let Some(seg) = seg else {
|
|
83
|
+
return false;
|
|
84
|
+
};
|
|
85
|
+
if seg.ident != "Value" {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
// Accept `Value`, `tishlang_runtime::Value`, `tishlang_core::Value`
|
|
89
|
+
if p.path.segments.len() == 1 {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
let prev = &p.path.segments[p.path.segments.len() - 2];
|
|
93
|
+
prev.ident == "tishlang_runtime" || prev.ident == "tishlang_core"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fn is_str_ref(ty: &Type) -> bool {
|
|
97
|
+
match ty {
|
|
98
|
+
Type::Reference(TypeReference { elem, .. }) => matches!(
|
|
99
|
+
elem.as_ref(),
|
|
100
|
+
Type::Path(p) if p.path.is_ident("str")
|
|
101
|
+
),
|
|
102
|
+
_ => false,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fn is_deserialize_str_result(item: &ItemFn) -> bool {
|
|
107
|
+
let sig = &item.sig;
|
|
108
|
+
if sig.inputs.len() != 1 {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
let FnArg::Typed(arg) = sig.inputs.first().unwrap() else {
|
|
112
|
+
return false;
|
|
113
|
+
};
|
|
114
|
+
if !is_str_ref(&arg.ty) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
let Some(ret) = return_type_inner(&sig.output) else {
|
|
118
|
+
return false;
|
|
119
|
+
};
|
|
120
|
+
if result_ok_type(ret).is_none() {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
has_deserialize_bound(item)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn has_deserialize_bound(item: &ItemFn) -> bool {
|
|
127
|
+
for p in &item.sig.generics.params {
|
|
128
|
+
if let syn::GenericParam::Type(t) = p {
|
|
129
|
+
for b in &t.bounds {
|
|
130
|
+
if bound_name_is(b, "Deserialize") {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if let Some(wc) = &item.sig.generics.where_clause {
|
|
137
|
+
for pred in &wc.predicates {
|
|
138
|
+
if let syn::WherePredicate::Type(t) = pred {
|
|
139
|
+
for b in &t.bounds {
|
|
140
|
+
if bound_name_is(b, "Deserialize") {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
false
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fn bound_name_is(b: &syn::TypeParamBound, want: &str) -> bool {
|
|
151
|
+
let syn::TypeParamBound::Trait(t) = b else {
|
|
152
|
+
return false;
|
|
153
|
+
};
|
|
154
|
+
let path = &t.path;
|
|
155
|
+
path.segments.last().is_some_and(|s| s.ident == want)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn is_serialize_ref_to_result_string(item: &ItemFn) -> bool {
|
|
159
|
+
let sig = &item.sig;
|
|
160
|
+
if sig.inputs.len() != 1 {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
let FnArg::Typed(arg) = sig.inputs.first().unwrap() else {
|
|
164
|
+
return false;
|
|
165
|
+
};
|
|
166
|
+
if strip_reference(&arg.ty).is_none() {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
let Some(ret) = return_type_inner(&sig.output) else {
|
|
170
|
+
return false;
|
|
171
|
+
};
|
|
172
|
+
let Some(ok_ty) = result_ok_type(ret) else {
|
|
173
|
+
return false;
|
|
174
|
+
};
|
|
175
|
+
if !type_is_string_or_str(ok_ty) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
has_serialize_bound(item)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
fn has_serialize_bound(item: &ItemFn) -> bool {
|
|
182
|
+
for p in &item.sig.generics.params {
|
|
183
|
+
if let syn::GenericParam::Type(t) = p {
|
|
184
|
+
for b in &t.bounds {
|
|
185
|
+
if bound_name_is(b, "Serialize") {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if let Some(wc) = &item.sig.generics.where_clause {
|
|
192
|
+
for pred in &wc.predicates {
|
|
193
|
+
if let syn::WherePredicate::Type(t) = pred {
|
|
194
|
+
for b in &t.bounds {
|
|
195
|
+
if bound_name_is(b, "Serialize") {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
false
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fn result_ok_type(ty: &Type) -> Option<&Type> {
|
|
206
|
+
let Type::Path(p) = ty else {
|
|
207
|
+
return None;
|
|
208
|
+
};
|
|
209
|
+
let seg = p.path.segments.last()?;
|
|
210
|
+
if seg.ident != "Result" {
|
|
211
|
+
return None;
|
|
212
|
+
}
|
|
213
|
+
let PathArguments::AngleBracketed(ab) = &seg.arguments else {
|
|
214
|
+
return None;
|
|
215
|
+
};
|
|
216
|
+
let first = ab.args.first()?;
|
|
217
|
+
let GenericArgument::Type(t) = first else {
|
|
218
|
+
return None;
|
|
219
|
+
};
|
|
220
|
+
Some(t)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
fn type_is_string_or_str(ty: &Type) -> bool {
|
|
224
|
+
match ty {
|
|
225
|
+
Type::Path(p) => {
|
|
226
|
+
if p.path.is_ident("String") {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
p.path.segments.len() == 1 && p.path.segments[0].ident == "str"
|
|
230
|
+
}
|
|
231
|
+
_ => false,
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
#[cfg(test)]
|
|
236
|
+
mod tests {
|
|
237
|
+
use super::*;
|
|
238
|
+
use syn::parse_quote;
|
|
239
|
+
|
|
240
|
+
#[test]
|
|
241
|
+
fn classify_serde_to_string_shape() {
|
|
242
|
+
let item: ItemFn = parse_quote! {
|
|
243
|
+
pub fn to_string<T: ?Sized + Serialize>(value: &T) -> Result<String, ()> {
|
|
244
|
+
unimplemented!()
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
assert_eq!(
|
|
248
|
+
classify_public_fn(&item),
|
|
249
|
+
Some(SignatureClass::SerializeRefToResultString)
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#[test]
|
|
254
|
+
fn classify_from_str_shape() {
|
|
255
|
+
let item: ItemFn = parse_quote! {
|
|
256
|
+
pub fn from_str<'a, T: Deserialize<'a>>(s: &'a str) -> Result<T, ()> {
|
|
257
|
+
unimplemented!()
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
assert_eq!(
|
|
261
|
+
classify_public_fn(&item),
|
|
262
|
+
Some(SignatureClass::DeserializeStrToResult)
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
//! Walk dependency `src/**/*.rs` and collect `pub fn` items by name.
|
|
2
|
+
|
|
3
|
+
use std::collections::HashMap;
|
|
4
|
+
use std::fs;
|
|
5
|
+
use std::path::{Path, PathBuf};
|
|
6
|
+
|
|
7
|
+
use syn::{Item, ItemFn, Visibility};
|
|
8
|
+
use walkdir::WalkDir;
|
|
9
|
+
|
|
10
|
+
fn is_pub(vis: &Visibility) -> bool {
|
|
11
|
+
matches!(vis, Visibility::Public(_))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/// Map export name (Rust ident) to the function AST (must be unique).
|
|
15
|
+
pub fn discover_public_functions(crate_root: &Path) -> Result<HashMap<String, ItemFn>, String> {
|
|
16
|
+
let src = crate_root.join("src");
|
|
17
|
+
if !src.is_dir() {
|
|
18
|
+
return Err(format!("no src/ under {}", crate_root.display()));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let mut map: HashMap<String, (PathBuf, ItemFn)> = HashMap::new();
|
|
22
|
+
|
|
23
|
+
for entry in WalkDir::new(&src)
|
|
24
|
+
.into_iter()
|
|
25
|
+
.filter_map(|e| e.ok())
|
|
26
|
+
.filter(|e| e.path().extension().map(|x| x == "rs").unwrap_or(false))
|
|
27
|
+
{
|
|
28
|
+
let path = entry.path();
|
|
29
|
+
let text = fs::read_to_string(path).map_err(|e| format!("read {}: {}", path.display(), e))?;
|
|
30
|
+
let file = syn::parse_file(&text).map_err(|e| format!("parse {}: {}", path.display(), e))?;
|
|
31
|
+
|
|
32
|
+
for item in file.items {
|
|
33
|
+
if let Item::Fn(f) = item {
|
|
34
|
+
if !is_pub(&f.vis) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
let name = f.sig.ident.to_string();
|
|
38
|
+
if let Some((prev_path, _)) = map.get(&name) {
|
|
39
|
+
return Err(format!(
|
|
40
|
+
"ambiguous public fn `{}`: found in {} and {}",
|
|
41
|
+
name,
|
|
42
|
+
prev_path.display(),
|
|
43
|
+
path.display()
|
|
44
|
+
));
|
|
45
|
+
}
|
|
46
|
+
map.insert(name, (path.to_path_buf(), f));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Ok(map.into_iter().map(|(k, (_, v))| (k, v)).collect())
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// On-disk location of a top-level `pub fn {fn_name}` under `crate_root/src` (LSP line/column, 0-based).
|
|
55
|
+
///
|
|
56
|
+
/// Requires `proc-macro2` built with `span-locations` (this crate enables it) so spans from
|
|
57
|
+
/// `syn::parse_file` carry line/column.
|
|
58
|
+
pub fn rust_public_fn_location(
|
|
59
|
+
crate_root: &Path,
|
|
60
|
+
fn_name: &str,
|
|
61
|
+
) -> Result<(PathBuf, u32, u32), String> {
|
|
62
|
+
let src = crate_root.join("src");
|
|
63
|
+
if !src.is_dir() {
|
|
64
|
+
return Err(format!("no src/ under {}", crate_root.display()));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for entry in WalkDir::new(&src)
|
|
68
|
+
.into_iter()
|
|
69
|
+
.filter_map(|e| e.ok())
|
|
70
|
+
.filter(|e| e.path().extension().map(|x| x == "rs").unwrap_or(false))
|
|
71
|
+
{
|
|
72
|
+
let path = entry.path();
|
|
73
|
+
let text = fs::read_to_string(path).map_err(|e| format!("read {}: {}", path.display(), e))?;
|
|
74
|
+
let file = syn::parse_file(&text).map_err(|e| format!("parse {}: {}", path.display(), e))?;
|
|
75
|
+
|
|
76
|
+
for item in file.items {
|
|
77
|
+
if let Item::Fn(f) = item {
|
|
78
|
+
if !is_pub(&f.vis) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if f.sig.ident != fn_name {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
let lc = f.sig.ident.span().start();
|
|
85
|
+
let line = u32::try_from(lc.line)
|
|
86
|
+
.map_err(|_| "span line out of range".to_string())?
|
|
87
|
+
.saturating_sub(1);
|
|
88
|
+
let col = u32::try_from(lc.column).map_err(|_| "span column out of range".to_string())?;
|
|
89
|
+
return Ok((path.to_path_buf(), line, col));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
Err(format!(
|
|
95
|
+
"no public fn `{}` found under {}/src",
|
|
96
|
+
fn_name,
|
|
97
|
+
crate_root.display()
|
|
98
|
+
))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#[cfg(test)]
|
|
102
|
+
mod tests {
|
|
103
|
+
use super::*;
|
|
104
|
+
use tempfile::tempdir;
|
|
105
|
+
|
|
106
|
+
#[test]
|
|
107
|
+
fn rust_public_fn_location_finds_fn() {
|
|
108
|
+
let tmp = tempdir().unwrap();
|
|
109
|
+
let src = tmp.path().join("src");
|
|
110
|
+
fs::create_dir_all(&src).unwrap();
|
|
111
|
+
fs::write(
|
|
112
|
+
src.join("lib.rs"),
|
|
113
|
+
"// comment\npub fn hello_tish_export() -> i32 { 0 }\n",
|
|
114
|
+
)
|
|
115
|
+
.unwrap();
|
|
116
|
+
let (path, line, _col) = rust_public_fn_location(tmp.path(), "hello_tish_export").unwrap();
|
|
117
|
+
assert!(path.ends_with("lib.rs"));
|
|
118
|
+
assert_eq!(line, 1);
|
|
119
|
+
}
|
|
120
|
+
}
|