@tishlang/tish 1.6.0 → 1.7.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 (79) hide show
  1. package/Cargo.toml +1 -0
  2. package/bin/tish +0 -0
  3. package/crates/js_to_tish/src/error.rs +2 -8
  4. package/crates/js_to_tish/src/transform/expr.rs +101 -130
  5. package/crates/js_to_tish/src/transform/stmt.rs +25 -22
  6. package/crates/tish/Cargo.toml +1 -1
  7. package/crates/tish/src/cli_help.rs +76 -29
  8. package/crates/tish/src/main.rs +85 -54
  9. package/crates/tish/tests/cargo_example_compile.rs +3 -1
  10. package/crates/tish/tests/integration_test.rs +197 -47
  11. package/crates/tish/tests/run_optimize_stdout_parity.rs +3 -7
  12. package/crates/tish/tests/shortcircuit.rs +19 -4
  13. package/crates/tish_ast/src/ast.rs +12 -14
  14. package/crates/tish_build_utils/src/lib.rs +31 -6
  15. package/crates/tish_builtins/src/array.rs +52 -21
  16. package/crates/tish_builtins/src/construct.rs +2 -8
  17. package/crates/tish_builtins/src/globals.rs +30 -15
  18. package/crates/tish_builtins/src/lib.rs +5 -5
  19. package/crates/tish_builtins/src/math.rs +5 -3
  20. package/crates/tish_builtins/src/string.rs +71 -19
  21. package/crates/tish_bytecode/src/chunk.rs +0 -1
  22. package/crates/tish_bytecode/src/compiler.rs +164 -60
  23. package/crates/tish_bytecode/src/opcode.rs +13 -4
  24. package/crates/tish_bytecode/src/peephole.rs +2 -2
  25. package/crates/tish_compile/src/codegen.rs +921 -299
  26. package/crates/tish_compile/src/infer.rs +69 -19
  27. package/crates/tish_compile/src/lib.rs +15 -5
  28. package/crates/tish_compile/src/resolve.rs +112 -69
  29. package/crates/tish_compile/src/types.rs +10 -14
  30. package/crates/tish_compile_js/src/codegen.rs +34 -13
  31. package/crates/tish_compile_js/src/tests_jsx.rs +30 -6
  32. package/crates/tish_compiler_wasm/src/lib.rs +16 -13
  33. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +39 -48
  34. package/crates/tish_core/src/json.rs +5 -3
  35. package/crates/tish_core/src/lib.rs +1 -1
  36. package/crates/tish_core/src/uri.rs +9 -6
  37. package/crates/tish_core/src/value.rs +92 -28
  38. package/crates/tish_cranelift/src/link.rs +6 -9
  39. package/crates/tish_cranelift/src/lower.rs +14 -8
  40. package/crates/tish_eval/src/eval.rs +389 -142
  41. package/crates/tish_eval/src/lib.rs +10 -6
  42. package/crates/tish_eval/src/natives.rs +95 -38
  43. package/crates/tish_eval/src/promise.rs +14 -8
  44. package/crates/tish_eval/src/timers.rs +28 -19
  45. package/crates/tish_eval/src/value.rs +10 -3
  46. package/crates/tish_fmt/src/lib.rs +29 -13
  47. package/crates/tish_lexer/src/lib.rs +217 -63
  48. package/crates/tish_lexer/src/token.rs +6 -6
  49. package/crates/tish_llvm/src/lib.rs +15 -8
  50. package/crates/tish_lsp/src/main.rs +41 -43
  51. package/crates/tish_native/src/build.rs +1 -6
  52. package/crates/tish_native/src/lib.rs +48 -19
  53. package/crates/tish_opt/src/lib.rs +67 -50
  54. package/crates/tish_parser/src/lib.rs +36 -11
  55. package/crates/tish_parser/src/parser.rs +172 -87
  56. package/crates/tish_runtime/src/http.rs +15 -6
  57. package/crates/tish_runtime/src/http_fetch.rs +24 -14
  58. package/crates/tish_runtime/src/lib.rs +224 -168
  59. package/crates/tish_runtime/src/promise.rs +1 -5
  60. package/crates/tish_runtime/src/ws.rs +45 -20
  61. package/crates/tish_runtime/tests/fetch_readable_stream.rs +5 -4
  62. package/crates/tish_ui/src/jsx.rs +41 -22
  63. package/crates/tish_ui/src/lib.rs +2 -2
  64. package/crates/tish_vm/src/vm.rs +309 -112
  65. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +8 -3
  66. package/crates/tish_wasm/src/lib.rs +38 -28
  67. package/crates/tishlang_cargo_bindgen/Cargo.toml +25 -0
  68. package/crates/tishlang_cargo_bindgen/src/classify.rs +265 -0
  69. package/crates/tishlang_cargo_bindgen/src/discover.rs +52 -0
  70. package/crates/tishlang_cargo_bindgen/src/infer.rs +372 -0
  71. package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
  72. package/crates/tishlang_cargo_bindgen/src/main.rs +164 -0
  73. package/crates/tishlang_cargo_bindgen/src/metadata.rs +114 -0
  74. package/package.json +1 -1
  75. package/platform/darwin-arm64/tish +0 -0
  76. package/platform/darwin-x64/tish +0 -0
  77. package/platform/linux-arm64/tish +0 -0
  78. package/platform/linux-x64/tish +0 -0
  79. 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!(v_peep.strict_eq(&v_unopt), "peep={v_peep:?} unopt={v_unopt:?}");
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 = run_chunk(&compile_for_repl_unoptimized(&program).expect("compile repl unopt"));
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,7 +115,8 @@ 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 = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/or_string_cmd.tish");
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");
@@ -107,27 +107,33 @@ fn emit_wasm_from_chunk(chunk: &Chunk, output_path: &Path) -> Result<(), WasmErr
107
107
  std::fs::create_dir_all(&out_dir_abs).map_err(|e| WasmError {
108
108
  message: format!("Cannot create output directory: {}", e),
109
109
  })?;
110
- let workspace_root = tishlang_build_utils::find_workspace_root().map_err(|e| WasmError {
111
- message: e,
112
- })?;
110
+ let workspace_root =
111
+ tishlang_build_utils::find_workspace_root().map_err(|e| WasmError { message: e })?;
113
112
  let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
114
113
  let build_status = Command::new(&cargo)
115
114
  .current_dir(&workspace_root)
116
115
  .args([
117
- "build", "-p", "tishlang_wasm_runtime",
118
- "--target", "wasm32-unknown-unknown",
119
- "--release", "--features", "browser",
116
+ "build",
117
+ "-p",
118
+ "tishlang_wasm_runtime",
119
+ "--target",
120
+ "wasm32-unknown-unknown",
121
+ "--release",
122
+ "--features",
123
+ "browser",
120
124
  ])
121
125
  .status()
122
- .map_err(|e| WasmError { message: format!("Failed to run cargo: {}", e) })?;
126
+ .map_err(|e| WasmError {
127
+ message: format!("Failed to run cargo: {}", e),
128
+ })?;
123
129
  if !build_status.success() {
124
130
  return Err(WasmError {
125
131
  message: "Failed to build wasm runtime. Run: rustup target add wasm32-unknown-unknown"
126
132
  .to_string(),
127
133
  });
128
134
  }
129
- let wasm_artifact = workspace_root
130
- .join("target/wasm32-unknown-unknown/release/tishlang_wasm_runtime.wasm");
135
+ let wasm_artifact =
136
+ workspace_root.join("target/wasm32-unknown-unknown/release/tishlang_wasm_runtime.wasm");
131
137
  if !wasm_artifact.exists() {
132
138
  return Err(WasmError {
133
139
  message: format!("Wasm artifact not found: {}", wasm_artifact.display()),
@@ -137,17 +143,25 @@ fn emit_wasm_from_chunk(chunk: &Chunk, output_path: &Path) -> Result<(), WasmErr
137
143
  let out_name = stem.to_string();
138
144
  let bindgen_status = Command::new(&wasm_bindgen)
139
145
  .args([
140
- "--target", "web",
141
- "--out-dir", out_dir_abs.to_str().unwrap(),
142
- "--out-name", &out_name,
146
+ "--target",
147
+ "web",
148
+ "--out-dir",
149
+ out_dir_abs.to_str().unwrap(),
150
+ "--out-name",
151
+ &out_name,
143
152
  wasm_artifact.to_str().unwrap(),
144
153
  ])
145
154
  .status()
146
155
  .map_err(|e| WasmError {
147
- message: format!("Failed to run wasm-bindgen: {}. Install with: cargo install wasm-bindgen-cli", e),
156
+ message: format!(
157
+ "Failed to run wasm-bindgen: {}. Install with: cargo install wasm-bindgen-cli",
158
+ e
159
+ ),
148
160
  })?;
149
161
  if !bindgen_status.success() {
150
- return Err(WasmError { message: "wasm-bindgen failed".to_string() });
162
+ return Err(WasmError {
163
+ message: "wasm-bindgen failed".to_string(),
164
+ });
151
165
  }
152
166
  let js_name = format!("{}.js", stem);
153
167
  let html = format!(
@@ -171,7 +185,12 @@ run(chunk);
171
185
  std::fs::write(&html_path, html).map_err(|e| WasmError {
172
186
  message: format!("Cannot write {}: {}", html_path.display(), e),
173
187
  })?;
174
- println!("Built: {}_bg.wasm, {}.js, {}", stem, stem, html_path.display());
188
+ println!(
189
+ "Built: {}_bg.wasm, {}.js, {}",
190
+ stem,
191
+ stem,
192
+ html_path.display()
193
+ );
175
194
  Ok(())
176
195
  }
177
196
 
@@ -233,9 +252,8 @@ pub fn compile_to_wasi(
233
252
  message: format!("Cannot create output directory: {}", e),
234
253
  })?;
235
254
 
236
- let workspace_root = tishlang_build_utils::find_workspace_root().map_err(|e| WasmError {
237
- message: e,
238
- })?;
255
+ let workspace_root =
256
+ tishlang_build_utils::find_workspace_root().map_err(|e| WasmError { message: e })?;
239
257
 
240
258
  // Create generated project: wasi_build/{stem}/
241
259
  let build_dir = out_dir_abs.join("wasi_build").join(stem);
@@ -249,9 +267,7 @@ pub fn compile_to_wasi(
249
267
  })?;
250
268
 
251
269
  // 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");
270
+ let runtime_path = workspace_root.join("crates").join("tish_wasm_runtime");
255
271
  let runtime_path_str = runtime_path
256
272
  .canonicalize()
257
273
  .unwrap_or(runtime_path)
@@ -314,12 +330,7 @@ fn main() {
314
330
  let build_status = Command::new(&cargo)
315
331
  .current_dir(&build_dir)
316
332
  .env("CARGO_TARGET_DIR", &target_dir)
317
- .args([
318
- "build",
319
- "--target",
320
- "wasm32-wasip1",
321
- "--release",
322
- ])
333
+ .args(["build", "--target", "wasm32-wasip1", "--release"])
323
334
  .status()
324
335
  .map_err(|e| WasmError {
325
336
  message: format!("Failed to run cargo: {}", e),
@@ -355,4 +366,3 @@ fn main() {
355
366
  );
356
367
  Ok(())
357
368
  }
358
-
@@ -0,0 +1,25 @@
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
+ serde_json = "1"
22
+ syn = { version = "2.0", features = ["full", "extra-traits"] }
23
+ tempfile = "3"
24
+ toml = "0.8"
25
+ 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,52 @@
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
+ }