@tishlang/tish-format 1.0.12 → 1.0.13

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 (164) hide show
  1. package/Cargo.toml +49 -0
  2. package/LICENSE +13 -0
  3. package/README.md +138 -0
  4. package/bin/tish-format +0 -0
  5. package/crates/js_to_tish/Cargo.toml +11 -0
  6. package/crates/js_to_tish/README.md +18 -0
  7. package/crates/js_to_tish/src/error.rs +55 -0
  8. package/crates/js_to_tish/src/lib.rs +11 -0
  9. package/crates/js_to_tish/src/span_util.rs +35 -0
  10. package/crates/js_to_tish/src/transform/expr.rs +610 -0
  11. package/crates/js_to_tish/src/transform/stmt.rs +503 -0
  12. package/crates/js_to_tish/src/transform.rs +60 -0
  13. package/crates/tish/Cargo.toml +54 -0
  14. package/crates/tish/src/cargo_native_registry.rs +32 -0
  15. package/crates/tish/src/cli_help.rs +565 -0
  16. package/crates/tish/src/main.rs +781 -0
  17. package/crates/tish/src/repl_completion.rs +200 -0
  18. package/crates/tish/tests/cargo_example_compile.rs +67 -0
  19. package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
  20. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
  21. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
  22. package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
  23. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
  24. package/crates/tish/tests/integration_test.rs +1095 -0
  25. package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
  26. package/crates/tish/tests/shortcircuit.rs +65 -0
  27. package/crates/tish_ast/Cargo.toml +9 -0
  28. package/crates/tish_ast/src/ast.rs +620 -0
  29. package/crates/tish_ast/src/lib.rs +5 -0
  30. package/crates/tish_build_utils/Cargo.toml +11 -0
  31. package/crates/tish_build_utils/src/lib.rs +577 -0
  32. package/crates/tish_builtins/Cargo.toml +20 -0
  33. package/crates/tish_builtins/src/array.rs +441 -0
  34. package/crates/tish_builtins/src/construct.rs +159 -0
  35. package/crates/tish_builtins/src/globals.rs +213 -0
  36. package/crates/tish_builtins/src/helpers.rs +35 -0
  37. package/crates/tish_builtins/src/lib.rs +16 -0
  38. package/crates/tish_builtins/src/math.rs +89 -0
  39. package/crates/tish_builtins/src/object.rs +36 -0
  40. package/crates/tish_builtins/src/string.rs +647 -0
  41. package/crates/tish_builtins/src/symbol.rs +83 -0
  42. package/crates/tish_bytecode/Cargo.toml +17 -0
  43. package/crates/tish_bytecode/src/chunk.rs +96 -0
  44. package/crates/tish_bytecode/src/compiler.rs +1760 -0
  45. package/crates/tish_bytecode/src/encoding.rs +100 -0
  46. package/crates/tish_bytecode/src/lib.rs +19 -0
  47. package/crates/tish_bytecode/src/opcode.rs +142 -0
  48. package/crates/tish_bytecode/src/peephole.rs +189 -0
  49. package/crates/tish_bytecode/src/serialize.rs +163 -0
  50. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
  51. package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
  52. package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
  53. package/crates/tish_compile/Cargo.toml +26 -0
  54. package/crates/tish_compile/src/codegen.rs +5332 -0
  55. package/crates/tish_compile/src/infer.rs +292 -0
  56. package/crates/tish_compile/src/lib.rs +164 -0
  57. package/crates/tish_compile/src/resolve.rs +1388 -0
  58. package/crates/tish_compile/src/types.rs +501 -0
  59. package/crates/tish_compile_js/Cargo.toml +18 -0
  60. package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
  61. package/crates/tish_compile_js/src/codegen.rs +871 -0
  62. package/crates/tish_compile_js/src/error.rs +20 -0
  63. package/crates/tish_compile_js/src/lib.rs +26 -0
  64. package/crates/tish_compile_js/src/tests_jsx.rs +350 -0
  65. package/crates/tish_compiler_wasm/Cargo.toml +21 -0
  66. package/crates/tish_compiler_wasm/src/lib.rs +57 -0
  67. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
  68. package/crates/tish_core/Cargo.toml +26 -0
  69. package/crates/tish_core/src/console_style.rs +160 -0
  70. package/crates/tish_core/src/json.rs +387 -0
  71. package/crates/tish_core/src/lib.rs +17 -0
  72. package/crates/tish_core/src/macros.rs +36 -0
  73. package/crates/tish_core/src/uri.rs +118 -0
  74. package/crates/tish_core/src/value.rs +696 -0
  75. package/crates/tish_core/src/vmref.rs +178 -0
  76. package/crates/tish_cranelift/Cargo.toml +19 -0
  77. package/crates/tish_cranelift/src/lib.rs +43 -0
  78. package/crates/tish_cranelift/src/link.rs +117 -0
  79. package/crates/tish_cranelift/src/lower.rs +85 -0
  80. package/crates/tish_cranelift_runtime/Cargo.toml +25 -0
  81. package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
  82. package/crates/tish_eval/Cargo.toml +45 -0
  83. package/crates/tish_eval/src/eval.rs +3717 -0
  84. package/crates/tish_eval/src/http.rs +188 -0
  85. package/crates/tish_eval/src/lib.rs +99 -0
  86. package/crates/tish_eval/src/natives.rs +399 -0
  87. package/crates/tish_eval/src/promise.rs +179 -0
  88. package/crates/tish_eval/src/regex.rs +299 -0
  89. package/crates/tish_eval/src/timers.rs +120 -0
  90. package/crates/tish_eval/src/value.rs +318 -0
  91. package/crates/tish_eval/src/value_convert.rs +111 -0
  92. package/crates/tish_fmt/Cargo.toml +16 -0
  93. package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
  94. package/crates/tish_fmt/src/lib.rs +2101 -0
  95. package/crates/tish_jsx_web/Cargo.toml +9 -0
  96. package/crates/tish_jsx_web/README.md +5 -0
  97. package/crates/tish_jsx_web/src/lib.rs +2 -0
  98. package/crates/tish_lexer/Cargo.toml +9 -0
  99. package/crates/tish_lexer/src/lib.rs +716 -0
  100. package/crates/tish_lexer/src/token.rs +163 -0
  101. package/crates/tish_lint/Cargo.toml +18 -0
  102. package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
  103. package/crates/tish_lint/src/lib.rs +289 -0
  104. package/crates/tish_llvm/Cargo.toml +13 -0
  105. package/crates/tish_llvm/src/lib.rs +115 -0
  106. package/crates/tish_lsp/Cargo.toml +25 -0
  107. package/crates/tish_lsp/README.md +26 -0
  108. package/crates/tish_lsp/src/builtin_goto.rs +362 -0
  109. package/crates/tish_lsp/src/import_goto.rs +562 -0
  110. package/crates/tish_lsp/src/main.rs +1046 -0
  111. package/crates/tish_native/Cargo.toml +16 -0
  112. package/crates/tish_native/src/build.rs +427 -0
  113. package/crates/tish_native/src/config.rs +48 -0
  114. package/crates/tish_native/src/lib.rs +416 -0
  115. package/crates/tish_opt/Cargo.toml +13 -0
  116. package/crates/tish_opt/src/lib.rs +943 -0
  117. package/crates/tish_parser/Cargo.toml +11 -0
  118. package/crates/tish_parser/src/lib.rs +332 -0
  119. package/crates/tish_parser/src/parser.rs +2304 -0
  120. package/crates/tish_pg/Cargo.toml +34 -0
  121. package/crates/tish_pg/README.md +38 -0
  122. package/crates/tish_pg/src/error.rs +52 -0
  123. package/crates/tish_pg/src/lib.rs +955 -0
  124. package/crates/tish_resolve/Cargo.toml +13 -0
  125. package/crates/tish_resolve/src/lib.rs +3561 -0
  126. package/crates/tish_resolve/src/pos.rs +141 -0
  127. package/crates/tish_runtime/Cargo.toml +96 -0
  128. package/crates/tish_runtime/src/http.rs +1298 -0
  129. package/crates/tish_runtime/src/http_fetch.rs +471 -0
  130. package/crates/tish_runtime/src/http_hyper.rs +418 -0
  131. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  132. package/crates/tish_runtime/src/lib.rs +1192 -0
  133. package/crates/tish_runtime/src/native_promise.rs +15 -0
  134. package/crates/tish_runtime/src/promise.rs +248 -0
  135. package/crates/tish_runtime/src/promise_io.rs +38 -0
  136. package/crates/tish_runtime/src/timers.rs +166 -0
  137. package/crates/tish_runtime/src/ws.rs +761 -0
  138. package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
  139. package/crates/tish_ui/Cargo.toml +17 -0
  140. package/crates/tish_ui/src/jsx.rs +682 -0
  141. package/crates/tish_ui/src/lib.rs +20 -0
  142. package/crates/tish_ui/src/runtime/hooks.rs +569 -0
  143. package/crates/tish_ui/src/runtime/mod.rs +180 -0
  144. package/crates/tish_vm/Cargo.toml +47 -0
  145. package/crates/tish_vm/src/lib.rs +39 -0
  146. package/crates/tish_vm/src/vm.rs +2192 -0
  147. package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
  148. package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
  149. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
  150. package/crates/tish_wasm/Cargo.toml +15 -0
  151. package/crates/tish_wasm/src/lib.rs +424 -0
  152. package/crates/tish_wasm_runtime/Cargo.toml +37 -0
  153. package/crates/tish_wasm_runtime/src/gpu.rs +413 -0
  154. package/crates/tish_wasm_runtime/src/lib.rs +42 -0
  155. package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
  156. package/crates/tishlang_cargo_bindgen/src/classify.rs +263 -0
  157. package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
  158. package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
  159. package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
  160. package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
  161. package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
  162. package/justfile +268 -0
  163. package/package.json +1 -1
  164. package/platform/darwin-arm64/tish-fmt +0 -0
@@ -0,0 +1,349 @@
1
+ //! Generate Rust glue for Tish `cargo:` imports by **reading the dependency crate’s source**
2
+ //! (via `cargo metadata` + `syn`), classifying each `pub fn` by **signature shape**, then emitting
3
+ //! `pub fn …(args: &[Value]) -> Value` shims.
4
+ //!
5
+ //! This avoids a fixed catalog of crate names: behavior follows **discovered** `pub fn` signatures
6
+ //! (e.g. `Serialize` + `Result<String, _>`, or `&str` + `Deserialize` + `Result`).
7
+ //!
8
+ //! **Standalone use:** the CLI does not link `tishlang_runtime` (it only emits source). For generated
9
+ //! crates, pass **`--tishlang-runtime-version`** so `Cargo.toml` uses a crates.io semver requirement
10
+ //! instead of a `path` into the Tish repo (no workspace checkout required to **build** the glue crate).
11
+ //!
12
+ //! **Project mode (default):** with **`--project-root`** and **no** `--dependency`, the tool reads
13
+ //! **`package.json` → `tish.rustDependencies`**, picks the path-based glue crate, and reads the
14
+ //! **upstream** crate + semver from that glue crate’s **`Cargo.toml`** `[dependencies]` if it
15
+ //! exists; otherwise from the **project root** **`Cargo.toml`** (`[dependencies]` and
16
+ //! **`[dev-dependencies]`**), skipping **`tishlang_core`** / **`tishlang_runtime`** and path-only entries.
17
+
18
+ mod classify;
19
+ mod discover;
20
+ pub mod infer;
21
+ mod metadata;
22
+
23
+ pub use classify::SignatureClass;
24
+ pub use discover::rust_public_fn_location;
25
+ pub use metadata::{
26
+ resolve_dependency_from_manifest, resolve_registry_dependency, ResolvedDependency,
27
+ };
28
+
29
+ use std::fs;
30
+ use std::io;
31
+ use std::path::Path;
32
+
33
+ use classify::classify_public_fn;
34
+
35
+ /// How the generated crate depends on `tishlang_runtime`.
36
+ #[derive(Debug, Clone)]
37
+ pub enum TishlangRuntimeDep {
38
+ /// `tishlang_runtime = { path = "..." }` (relative to `--out-dir`).
39
+ Path(String),
40
+ /// `tishlang_runtime = "1.0"` style crates.io requirement (standalone builds; publish `tishlang_runtime` first).
41
+ Version(String),
42
+ }
43
+
44
+ /// Configuration for a generated wrapper crate on disk.
45
+ #[derive(Debug, Clone)]
46
+ pub struct BindgenConfig {
47
+ /// Cargo `[package].name` of the **generated** crate (must match `cargo:` / `rustDependencies`).
48
+ pub output_crate_name: String,
49
+ pub tishlang_runtime: TishlangRuntimeDep,
50
+ pub out_dir: std::path::PathBuf,
51
+ /// Registry dependency name (e.g. `serde_json`) and semver req for the probe + generated dep.
52
+ pub dependency_name: String,
53
+ pub dependency_version_req: String,
54
+ /// Tish export names to wrap (must match a `pub fn` in the dependency).
55
+ pub exports: Vec<String>,
56
+ }
57
+
58
+ /// Full generation: resolve dependency, scan sources, classify, write crate.
59
+ pub fn generate_from_registry_dependency(cfg: &BindgenConfig) -> Result<(), String> {
60
+ let resolved = resolve_registry_dependency(&cfg.dependency_name, &cfg.dependency_version_req)?;
61
+ generate_from_resolved(cfg, &resolved)
62
+ }
63
+
64
+ /// Same as [`generate_from_registry_dependency`] but dependency is already in `manifest_path`’s workspace.
65
+ pub fn generate_from_manifest(
66
+ cfg: &BindgenConfig,
67
+ manifest_path: &Path,
68
+ dependency_package_name: &str,
69
+ ) -> Result<(), String> {
70
+ let resolved = resolve_dependency_from_manifest(manifest_path, dependency_package_name)?;
71
+ generate_from_resolved(cfg, &resolved)
72
+ }
73
+
74
+ fn generate_from_resolved(
75
+ cfg: &BindgenConfig,
76
+ resolved: &ResolvedDependency,
77
+ ) -> Result<(), String> {
78
+ let root = resolved.source_root();
79
+ let fns = discover::discover_public_functions(&root)?;
80
+
81
+ let mut need_json_helpers = false;
82
+ let mut emitted = Vec::new();
83
+
84
+ for export in &cfg.exports {
85
+ let item = fns.get(export).ok_or_else(|| {
86
+ format!(
87
+ "no public fn `{}` found under {}/src (exports must match a `pub fn` in the dependency sources)",
88
+ export,
89
+ root.display()
90
+ )
91
+ })?;
92
+
93
+ let class = classify_public_fn(item).ok_or_else(|| {
94
+ format!(
95
+ "public fn `{}` has no supported signature for automatic binding (need `&[Value] -> Value`, or `&T where T: Serialize` with `Result<String, _>`, or `&str` with `Deserialize` and `Result`)",
96
+ export
97
+ )
98
+ })?;
99
+
100
+ match class {
101
+ SignatureClass::SerializeRefToResultString | SignatureClass::DeserializeStrToResult => {
102
+ need_json_helpers = true;
103
+ }
104
+ SignatureClass::TishValueAbi => {}
105
+ }
106
+
107
+ emitted.push((export.clone(), class));
108
+ }
109
+
110
+ let lib_rs = render_generated_lib(&cfg.dependency_name, &emitted, need_json_helpers)?;
111
+
112
+ let cargo_toml = render_output_cargo_toml(cfg, resolved.version(), need_json_helpers)?;
113
+
114
+ fs::create_dir_all(cfg.out_dir.join("src")).map_err(|e| e.to_string())?;
115
+ fs::write(cfg.out_dir.join("Cargo.toml"), cargo_toml).map_err(|e| e.to_string())?;
116
+ fs::write(cfg.out_dir.join("src").join("lib.rs"), lib_rs).map_err(|e| e.to_string())?;
117
+
118
+ Ok(())
119
+ }
120
+
121
+ impl BindgenConfig {
122
+ /// Write using [`generate_from_registry_dependency`].
123
+ pub fn write_files(&self) -> io::Result<()> {
124
+ generate_from_registry_dependency(self).map_err(|e| io::Error::new(io::ErrorKind::Other, e))
125
+ }
126
+ }
127
+
128
+ fn render_output_cargo_toml(
129
+ cfg: &BindgenConfig,
130
+ dep_exact_version: &str,
131
+ need_serde_json: bool,
132
+ ) -> Result<String, String> {
133
+ let rt_line = match &cfg.tishlang_runtime {
134
+ TishlangRuntimeDep::Path(p) => {
135
+ format!("tishlang_runtime = {{ path = {} }}\n", toml_string_value(p))
136
+ }
137
+ TishlangRuntimeDep::Version(req) => {
138
+ format!("tishlang_runtime = {}\n", toml_string_value(req))
139
+ }
140
+ };
141
+ let dep_line = format!(
142
+ "{} = {}\n",
143
+ cfg.dependency_name,
144
+ toml_string_value(dep_exact_version)
145
+ );
146
+ // JSON bridge helpers use `serde_json::Value`; only add a second dep when the upstream crate is not serde_json.
147
+ let extra_serde = if need_serde_json && cfg.dependency_name != "serde_json" {
148
+ "serde_json = \"1.0\"\n"
149
+ } else {
150
+ ""
151
+ };
152
+
153
+ Ok(format!(
154
+ r#"[package]
155
+ name = "{name}"
156
+ version = "0.1.0"
157
+ edition = "2021"
158
+ publish = false
159
+
160
+ [lib]
161
+ path = "src/lib.rs"
162
+
163
+ [dependencies]
164
+ {rt_line}{dep_line}{extra_serde}"#,
165
+ name = cfg.output_crate_name,
166
+ rt_line = rt_line,
167
+ dep_line = dep_line,
168
+ extra_serde = extra_serde
169
+ ))
170
+ }
171
+
172
+ fn render_generated_lib(
173
+ dependency_cargo_name: &str,
174
+ exports: &[(String, SignatureClass)],
175
+ need_json_helpers: bool,
176
+ ) -> Result<String, String> {
177
+ let crate_ident = dependency_cargo_name.replace('-', "_");
178
+ let mut out = String::from(
179
+ "//! Generated by `tishlang-cargo-bindgen` — do not edit.\n\
180
+ //! Bindings inferred from the dependency crate’s `pub fn` signatures (syn + cargo metadata).\n\n\
181
+ use std::cell::RefCell;\n\
182
+ use std::rc::Rc;\n\
183
+ use std::sync::Arc;\n\
184
+ use tishlang_runtime::{ObjectMap, Value, VmRef};\n\n",
185
+ );
186
+
187
+ out.push_str(&format!("use {} as _tish_upstream;\n\n", crate_ident));
188
+
189
+ if need_json_helpers {
190
+ out.push_str(JSON_HELPERS);
191
+ }
192
+
193
+ for (name, class) in exports {
194
+ let rust_fn = syn::parse_str::<syn::Ident>(name)
195
+ .map_err(|e| format!("invalid export name {}: {}", name, e))?;
196
+ let block = match class {
197
+ SignatureClass::TishValueAbi => format!(
198
+ "pub fn {name}(args: &[Value]) -> Value {{\n _tish_upstream::{name}(args)\n}}\n\n",
199
+ name = rust_fn
200
+ ),
201
+ SignatureClass::SerializeRefToResultString => format!(
202
+ "pub fn {name}(args: &[Value]) -> Value {{\n let Some(v) = args.first() else {{ return Value::Null }};\n match _tish_upstream::{name}(&tish_to_json(v)) {{\n Ok(s) => Value::String(Arc::from(s)),\n Err(_) => Value::Null,\n }}\n}}\n\n",
203
+ name = rust_fn
204
+ ),
205
+ SignatureClass::DeserializeStrToResult => format!(
206
+ "pub fn {name}(args: &[Value]) -> Value {{\n let s = match args.first() {{\n Some(Value::String(x)) => x.as_ref(),\n _ => return Value::Null,\n }};\n match _tish_upstream::{name}::<serde_json::Value>(s) {{\n Ok(j) => json_to_tish(j),\n Err(_) => Value::Null,\n }}\n}}\n\n",
207
+ name = rust_fn
208
+ ),
209
+ };
210
+ out.push_str(&block);
211
+ }
212
+
213
+ Ok(out)
214
+ }
215
+
216
+ const JSON_HELPERS: &str = r#"fn tish_to_json(v: &Value) -> serde_json::Value {
217
+ match v {
218
+ Value::Null => serde_json::Value::Null,
219
+ Value::Bool(b) => serde_json::Value::Bool(*b),
220
+ Value::Number(n) => serde_json::Number::from_f64(*n)
221
+ .map(serde_json::Value::Number)
222
+ .unwrap_or(serde_json::Value::Null),
223
+ Value::String(s) => serde_json::Value::String(s.to_string()),
224
+ Value::Array(a) => {
225
+ serde_json::Value::Array(a.borrow().iter().map(|x| tish_to_json(x)).collect())
226
+ }
227
+ Value::Object(o) => {
228
+ let mut m = serde_json::Map::new();
229
+ for (k, v) in o.borrow().iter() {
230
+ m.insert(k.to_string(), tish_to_json(v));
231
+ }
232
+ serde_json::Value::Object(m)
233
+ }
234
+ _ => serde_json::Value::Null,
235
+ }
236
+ }
237
+
238
+ fn json_to_tish(v: serde_json::Value) -> Value {
239
+ match v {
240
+ serde_json::Value::Null => Value::Null,
241
+ serde_json::Value::Bool(b) => Value::Bool(b),
242
+ serde_json::Value::Number(n) => Value::Number(n.as_f64().unwrap_or(0.0)),
243
+ serde_json::Value::String(s) => Value::String(s.into()),
244
+ serde_json::Value::Array(a) => Value::Array(VmRef::new(
245
+ a.into_iter().map(json_to_tish).collect(),
246
+ )),
247
+ serde_json::Value::Object(m) => {
248
+ let mut om = ObjectMap::default();
249
+ for (k, v) in m {
250
+ om.insert(Arc::from(k), json_to_tish(v));
251
+ }
252
+ Value::Object(VmRef::new(om))
253
+ }
254
+ }
255
+ }
256
+
257
+ "#;
258
+
259
+ fn toml_string_value(s: &str) -> String {
260
+ let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
261
+ format!("\"{}\"", escaped)
262
+ }
263
+
264
+ /// Resolve a path to `tishlang_runtime` for writing into generated `Cargo.toml`.
265
+ pub fn resolve_runtime_path_for_output(start: &Path) -> Result<String, String> {
266
+ if let Ok(p) = std::env::var("TISHLANG_RUNTIME_PATH") {
267
+ let pb = Path::new(&p);
268
+ if pb.join("Cargo.toml").is_file() {
269
+ return canonical_path_string(pb);
270
+ }
271
+ return Err(format!(
272
+ "TISHLANG_RUNTIME_PATH does not point to tishlang_runtime: {}",
273
+ p
274
+ ));
275
+ }
276
+
277
+ let mut dir = if start.is_file() {
278
+ start.parent().unwrap_or(start).to_path_buf()
279
+ } else {
280
+ start.to_path_buf()
281
+ };
282
+
283
+ for _ in 0..64 {
284
+ let npm_rt = dir
285
+ .join("node_modules")
286
+ .join("@tishlang")
287
+ .join("tish")
288
+ .join("crates")
289
+ .join("tish_runtime");
290
+ if npm_rt.join("Cargo.toml").is_file() {
291
+ return canonical_path_string(&npm_rt);
292
+ }
293
+
294
+ let ws_rt = dir.join("crates").join("tish_runtime");
295
+ if ws_rt.join("Cargo.toml").is_file() {
296
+ return canonical_path_string(&ws_rt);
297
+ }
298
+
299
+ if !dir.pop() {
300
+ break;
301
+ }
302
+ }
303
+
304
+ Err(
305
+ "Could not find tishlang_runtime (set TISHLANG_RUNTIME_PATH or run from a project with node_modules/@tishlang/tish or a Tish repo checkout)"
306
+ .into(),
307
+ )
308
+ }
309
+
310
+ fn canonical_path_string(p: &Path) -> Result<String, String> {
311
+ p.canonicalize()
312
+ .map_err(|e| format!("Cannot canonicalize {}: {}", p.display(), e))
313
+ .map(|p| p.display().to_string().replace('\\', "/"))
314
+ }
315
+
316
+ /// Relative path from `out_dir` to `runtime` for Cargo.toml `path =`.
317
+ pub fn runtime_path_relative_to_out_dir(
318
+ out_dir: &Path,
319
+ runtime: impl AsRef<Path>,
320
+ ) -> Result<String, String> {
321
+ let abs_rt = runtime
322
+ .as_ref()
323
+ .canonicalize()
324
+ .map_err(|e| format!("Cannot canonicalize runtime path: {}", e))?;
325
+ fs::create_dir_all(out_dir).map_err(|e| e.to_string())?;
326
+ let abs_out = out_dir
327
+ .canonicalize()
328
+ .map_err(|e| format!("Cannot canonicalize out dir: {}", e))?;
329
+
330
+ pathdiff::diff_paths(&abs_rt, &abs_out)
331
+ .ok_or_else(|| "Could not compute relative path from out_dir to runtime".to_string())
332
+ .map(|p| p.display().to_string().replace('\\', "/"))
333
+ }
334
+
335
+ #[cfg(test)]
336
+ mod tests {
337
+ use super::*;
338
+
339
+ #[test]
340
+ fn generated_lib_contains_tish_abi_forward() {
341
+ let s = render_generated_lib(
342
+ "demo",
343
+ &[("greet".into(), SignatureClass::TishValueAbi)],
344
+ false,
345
+ )
346
+ .unwrap();
347
+ assert!(s.contains("_tish_upstream::greet"));
348
+ }
349
+ }
@@ -0,0 +1,167 @@
1
+ //! CLI for pre-generating `cargo:` binding crates (bindgen-style).
2
+ //!
3
+ //! Resolves the dependency with `cargo metadata`, scans its `src/**/*.rs` with `syn`, classifies
4
+ //! each requested `pub fn` by signature, and emits a wrapper crate.
5
+
6
+ use std::path::PathBuf;
7
+
8
+ use clap::Parser;
9
+ use tishlang_cargo_bindgen::{
10
+ generate_from_manifest, generate_from_registry_dependency, infer,
11
+ resolve_runtime_path_for_output, runtime_path_relative_to_out_dir, BindgenConfig,
12
+ TishlangRuntimeDep,
13
+ };
14
+
15
+ #[derive(Parser, Debug)]
16
+ #[command(name = "tishlang-cargo-bindgen")]
17
+ #[command(about = "Generate Rust glue for Tish `cargo:` imports from dependency source + metadata")]
18
+ struct Args {
19
+ /// Upstream Cargo package to wrap (e.g. serde_json). Omit to read `tish.rustDependencies` + glue `Cargo.toml`.
20
+ #[arg(long)]
21
+ dependency: Option<String>,
22
+
23
+ /// Semver for the upstream crate when using `--dependency` (ignored in full project auto mode).
24
+ #[arg(long, default_value = "1.0")]
25
+ dependency_version: String,
26
+
27
+ /// Generated crate `[package].name` when using `--dependency` with `--out-dir` (default: tish_serde_json).
28
+ #[arg(long)]
29
+ crate_name: Option<String>,
30
+
31
+ /// Output directory. Required with `--dependency` unless `--project-root` selects a path from `package.json`.
32
+ #[arg(long)]
33
+ out_dir: Option<PathBuf>,
34
+
35
+ /// Comma-separated export names to bind (must match `pub fn` names in the dependency).
36
+ #[arg(long, default_value = "to_string,from_str")]
37
+ exports: String,
38
+
39
+ /// If set, resolve `dependency` from this workspace instead of a temporary probe crate.
40
+ #[arg(long)]
41
+ manifest_path: Option<PathBuf>,
42
+
43
+ /// Directory containing `package.json` (default: current directory). Drives automatic glue discovery.
44
+ #[arg(long)]
45
+ project_root: Option<PathBuf>,
46
+
47
+ /// Crates.io semver for `tishlang_runtime` in the generated `Cargo.toml` (no path into the Tish repo).
48
+ /// Mutually exclusive with `--tishlang-runtime-path`.
49
+ #[arg(long, conflicts_with = "tishlang_runtime_path")]
50
+ tishlang_runtime_version: Option<String>,
51
+
52
+ /// Absolute path to the `tishlang_runtime` crate root for generated `path = ...` (overrides search).
53
+ /// Mutually exclusive with `--tishlang-runtime-version`.
54
+ #[arg(long, conflicts_with = "tishlang_runtime_version")]
55
+ tishlang_runtime_path: Option<PathBuf>,
56
+ }
57
+
58
+ fn main() {
59
+ if let Err(e) = run() {
60
+ eprintln!("tishlang-cargo-bindgen: error: {}", e);
61
+ std::process::exit(1);
62
+ }
63
+ }
64
+
65
+ fn run() -> Result<(), String> {
66
+ let args = Args::parse();
67
+
68
+ let exports: Vec<String> = args
69
+ .exports
70
+ .split(',')
71
+ .map(|s| s.trim().to_string())
72
+ .filter(|s| !s.is_empty())
73
+ .collect();
74
+ if exports.is_empty() {
75
+ return Err("no exports given".into());
76
+ }
77
+
78
+ if args.dependency.is_none() && args.out_dir.is_some() {
79
+ return Err(
80
+ "--out-dir is only used with --dependency (without --dependency, output path comes from package.json tish.rustDependencies)"
81
+ .into(),
82
+ );
83
+ }
84
+
85
+ let current_dir = std::env::current_dir().map_err(|e| e.to_string())?;
86
+
87
+ let (output_crate_name, out_dir, dependency_name, dependency_version_req, search_root) =
88
+ if let Some(dep) = args.dependency.as_ref() {
89
+ if let Some(out) = args.out_dir.as_ref() {
90
+ let root = args
91
+ .project_root
92
+ .clone()
93
+ .unwrap_or_else(|| out.parent().unwrap_or(out).to_path_buf());
94
+ (
95
+ args.crate_name
96
+ .clone()
97
+ .unwrap_or_else(|| "tish_serde_json".into()),
98
+ out.clone(),
99
+ dep.clone(),
100
+ args.dependency_version.clone(),
101
+ root,
102
+ )
103
+ } else {
104
+ let root = args
105
+ .project_root
106
+ .clone()
107
+ .unwrap_or_else(|| current_dir.clone());
108
+ let (crate_key, od) =
109
+ infer::infer_glue_paths_only(&root, args.crate_name.as_deref())?;
110
+ (
111
+ crate_key,
112
+ od,
113
+ dep.clone(),
114
+ args.dependency_version.clone(),
115
+ root,
116
+ )
117
+ }
118
+ } else {
119
+ let root = args
120
+ .project_root
121
+ .clone()
122
+ .unwrap_or_else(|| current_dir.clone());
123
+ let inf = infer::infer_from_project_root(&root, args.crate_name.as_deref())?;
124
+ (
125
+ inf.output_crate_name,
126
+ inf.out_dir,
127
+ inf.dependency_name,
128
+ inf.dependency_version_req,
129
+ root,
130
+ )
131
+ };
132
+
133
+ let tishlang_runtime = if let Some(req) = args.tishlang_runtime_version.clone() {
134
+ TishlangRuntimeDep::Version(req)
135
+ } else if let Some(p) = args.tishlang_runtime_path.as_ref() {
136
+ let abs = p
137
+ .canonicalize()
138
+ .map_err(|e| format!("tishlang_runtime path {}: {}", p.display(), e))?;
139
+ TishlangRuntimeDep::Path(runtime_path_relative_to_out_dir(&out_dir, &abs)?)
140
+ } else {
141
+ let runtime_abs = resolve_runtime_path_for_output(&search_root)?;
142
+ let rt_rel = runtime_path_relative_to_out_dir(&out_dir, &runtime_abs)?;
143
+ TishlangRuntimeDep::Path(rt_rel)
144
+ };
145
+
146
+ let cfg = BindgenConfig {
147
+ output_crate_name,
148
+ tishlang_runtime,
149
+ out_dir: out_dir.clone(),
150
+ dependency_name,
151
+ dependency_version_req,
152
+ exports,
153
+ };
154
+
155
+ if let Some(manifest) = args.manifest_path {
156
+ generate_from_manifest(&cfg, &manifest, &cfg.dependency_name)?;
157
+ } else {
158
+ generate_from_registry_dependency(&cfg)?;
159
+ }
160
+
161
+ println!(
162
+ "Wrote {} (upstream `{}`, metadata + syn)",
163
+ out_dir.display(),
164
+ cfg.dependency_name
165
+ );
166
+ Ok(())
167
+ }
@@ -0,0 +1,117 @@
1
+ //! Resolve a crates.io (or registry) dependency to its on-disk source tree via `cargo metadata`.
2
+
3
+ use std::fs;
4
+ use std::path::{Path, PathBuf};
5
+
6
+ use cargo_metadata::{MetadataCommand, Package, TargetKind};
7
+
8
+ /// Root directory of the resolved dependency (contains `Cargo.toml` and usually `src/`).
9
+ #[derive(Debug, Clone)]
10
+ pub struct ResolvedDependency {
11
+ pub name: String,
12
+ pub version: String,
13
+ pub manifest_path: PathBuf,
14
+ }
15
+
16
+ impl ResolvedDependency {
17
+ pub fn source_root(&self) -> PathBuf {
18
+ self.manifest_path
19
+ .parent()
20
+ .expect("manifest has parent")
21
+ .to_path_buf()
22
+ }
23
+
24
+ pub fn version(&self) -> &str {
25
+ &self.version
26
+ }
27
+ }
28
+
29
+ fn from_package(pkg: &Package) -> ResolvedDependency {
30
+ ResolvedDependency {
31
+ name: pkg.name.clone(),
32
+ version: pkg.version.to_string(),
33
+ manifest_path: pkg.manifest_path.as_std_path().to_path_buf(),
34
+ }
35
+ }
36
+
37
+ /// Build a tiny probe crate, run `cargo metadata`, and return the named package.
38
+ pub fn resolve_registry_dependency(
39
+ name: &str,
40
+ version_req: &str,
41
+ ) -> Result<ResolvedDependency, String> {
42
+ let probe_dir = tempfile::tempdir().map_err(|e| format!("tempdir: {}", e))?;
43
+ let probe_toml = format!(
44
+ r#"[package]
45
+ name = "_tishlang_bindgen_probe"
46
+ version = "0.0.0"
47
+ edition = "2021"
48
+ [dependencies]
49
+ {} = "{}"
50
+ "#,
51
+ name, version_req
52
+ );
53
+ let manifest = probe_dir.path().join("Cargo.toml");
54
+ fs::write(&manifest, probe_toml).map_err(|e| format!("write probe Cargo.toml: {}", e))?;
55
+ // `cargo metadata` requires at least one target in the root package.
56
+ let src_dir = probe_dir.path().join("src");
57
+ fs::create_dir_all(&src_dir).map_err(|e| format!("probe src dir: {}", e))?;
58
+ fs::write(src_dir.join("lib.rs"), "// probe only\n")
59
+ .map_err(|e| format!("write probe lib.rs: {}", e))?;
60
+
61
+ let meta = MetadataCommand::new()
62
+ .manifest_path(&manifest)
63
+ .exec()
64
+ .map_err(|e| format!("cargo metadata failed: {} (is `cargo` on PATH?)", e))?;
65
+
66
+ let pkg = meta
67
+ .packages
68
+ .iter()
69
+ .find(|p| p.name == name)
70
+ .ok_or_else(|| {
71
+ format!(
72
+ "dependency `{}` not found in metadata (check name and version req `{}`)",
73
+ name, version_req
74
+ )
75
+ })?;
76
+
77
+ let root = pkg.manifest_path.as_std_path().parent().unwrap();
78
+ if !root.join("src").is_dir()
79
+ && !pkg
80
+ .targets
81
+ .iter()
82
+ .any(|t| t.kind.iter().any(|k| *k == TargetKind::Lib))
83
+ {
84
+ return Err(format!(
85
+ "package `{}` has no src/ or lib target at {}",
86
+ name,
87
+ root.display()
88
+ ));
89
+ }
90
+
91
+ Ok(from_package(pkg))
92
+ }
93
+
94
+ /// Resolve using an existing workspace manifest (no temporary probe).
95
+ pub fn resolve_dependency_from_manifest(
96
+ manifest_path: &Path,
97
+ package_name: &str,
98
+ ) -> Result<ResolvedDependency, String> {
99
+ let meta = MetadataCommand::new()
100
+ .manifest_path(manifest_path)
101
+ .exec()
102
+ .map_err(|e| format!("cargo metadata: {}", e))?;
103
+
104
+ let pkg = meta
105
+ .packages
106
+ .iter()
107
+ .find(|p| p.name == package_name)
108
+ .ok_or_else(|| {
109
+ format!(
110
+ "package `{}` not found in metadata for {}",
111
+ package_name,
112
+ manifest_path.display()
113
+ )
114
+ })?;
115
+
116
+ Ok(from_package(pkg))
117
+ }