@tishlang/tish 1.7.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.
Files changed (95) hide show
  1. package/Cargo.toml +1 -0
  2. package/README.md +2 -0
  3. package/bin/tish +0 -0
  4. package/crates/js_to_tish/src/transform/expr.rs +28 -8
  5. package/crates/js_to_tish/src/transform/stmt.rs +49 -22
  6. package/crates/tish/Cargo.toml +15 -5
  7. package/crates/tish/src/cargo_native_registry.rs +29 -0
  8. package/crates/tish/src/cli_help.rs +16 -10
  9. package/crates/tish/src/main.rs +87 -32
  10. package/crates/tish/src/repl_completion.rs +3 -3
  11. package/crates/tish/tests/cargo_example_compile.rs +1 -1
  12. package/crates/tish/tests/integration_test.rs +19 -7
  13. package/crates/tish/tests/shortcircuit.rs +1 -1
  14. package/crates/tish_ast/src/ast.rs +80 -9
  15. package/crates/tish_build_utils/Cargo.toml +4 -0
  16. package/crates/tish_build_utils/src/lib.rs +105 -2
  17. package/crates/tish_builtins/Cargo.toml +5 -1
  18. package/crates/tish_builtins/src/array.rs +13 -12
  19. package/crates/tish_builtins/src/construct.rs +34 -33
  20. package/crates/tish_builtins/src/globals.rs +12 -11
  21. package/crates/tish_builtins/src/helpers.rs +2 -1
  22. package/crates/tish_builtins/src/object.rs +3 -2
  23. package/crates/tish_builtins/src/string.rs +73 -3
  24. package/crates/tish_bytecode/src/compiler.rs +12 -14
  25. package/crates/tish_bytecode/src/opcode.rs +12 -3
  26. package/crates/tish_compile/Cargo.toml +1 -0
  27. package/crates/tish_compile/src/codegen.rs +745 -199
  28. package/crates/tish_compile/src/infer.rs +6 -0
  29. package/crates/tish_compile/src/lib.rs +4 -3
  30. package/crates/tish_compile/src/resolve.rs +180 -82
  31. package/crates/tish_compile/src/types.rs +175 -11
  32. package/crates/tish_compile_js/Cargo.toml +1 -0
  33. package/crates/tish_compile_js/src/codegen.rs +152 -29
  34. package/crates/tish_compile_js/src/lib.rs +3 -1
  35. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +31 -12
  36. package/crates/tish_core/Cargo.toml +8 -0
  37. package/crates/tish_core/src/json.rs +102 -53
  38. package/crates/tish_core/src/lib.rs +3 -1
  39. package/crates/tish_core/src/macros.rs +5 -5
  40. package/crates/tish_core/src/value.rs +53 -15
  41. package/crates/tish_core/src/vmref.rs +178 -0
  42. package/crates/tish_eval/Cargo.toml +17 -2
  43. package/crates/tish_eval/src/eval.rs +90 -28
  44. package/crates/tish_eval/src/http.rs +61 -0
  45. package/crates/tish_eval/src/lib.rs +3 -3
  46. package/crates/tish_eval/src/natives.rs +41 -0
  47. package/crates/tish_eval/src/value.rs +7 -3
  48. package/crates/tish_eval/src/value_convert.rs +13 -5
  49. package/crates/tish_fmt/src/lib.rs +120 -30
  50. package/crates/tish_lexer/src/lib.rs +20 -5
  51. package/crates/tish_lexer/src/token.rs +4 -0
  52. package/crates/tish_llvm/src/lib.rs +3 -1
  53. package/crates/tish_lsp/Cargo.toml +4 -1
  54. package/crates/tish_lsp/README.md +1 -1
  55. package/crates/tish_lsp/src/builtin_goto.rs +261 -0
  56. package/crates/tish_lsp/src/import_goto.rs +549 -0
  57. package/crates/tish_lsp/src/main.rs +502 -102
  58. package/crates/tish_native/src/build.rs +3 -2
  59. package/crates/tish_native/src/lib.rs +6 -2
  60. package/crates/tish_opt/src/lib.rs +17 -2
  61. package/crates/tish_parser/src/lib.rs +10 -3
  62. package/crates/tish_parser/src/parser.rs +346 -56
  63. package/crates/tish_resolve/Cargo.toml +13 -0
  64. package/crates/tish_resolve/src/lib.rs +3436 -0
  65. package/crates/tish_resolve/src/pos.rs +133 -0
  66. package/crates/tish_runtime/Cargo.toml +68 -3
  67. package/crates/tish_runtime/src/http.rs +1123 -141
  68. package/crates/tish_runtime/src/http_fetch.rs +15 -14
  69. package/crates/tish_runtime/src/http_hyper.rs +418 -0
  70. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  71. package/crates/tish_runtime/src/lib.rs +159 -29
  72. package/crates/tish_runtime/src/promise.rs +199 -36
  73. package/crates/tish_runtime/src/promise_io.rs +2 -1
  74. package/crates/tish_runtime/src/timers.rs +37 -1
  75. package/crates/tish_runtime/src/ws.rs +26 -28
  76. package/crates/tish_ui/src/jsx.rs +279 -8
  77. package/crates/tish_ui/src/lib.rs +5 -2
  78. package/crates/tish_ui/src/runtime/hooks.rs +406 -45
  79. package/crates/tish_ui/src/runtime/mod.rs +36 -9
  80. package/crates/tish_vm/Cargo.toml +15 -5
  81. package/crates/tish_vm/src/vm.rs +506 -259
  82. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +3 -1
  83. package/crates/tish_wasm/src/lib.rs +17 -14
  84. package/crates/tish_wasm_runtime/Cargo.toml +2 -1
  85. package/crates/tish_wasm_runtime/src/lib.rs +1 -1
  86. package/crates/tishlang_cargo_bindgen/Cargo.toml +1 -0
  87. package/crates/tishlang_cargo_bindgen/src/discover.rs +68 -0
  88. package/crates/tishlang_cargo_bindgen/src/lib.rs +5 -4
  89. package/justfile +8 -0
  90. package/package.json +1 -1
  91. package/platform/darwin-arm64/tish +0 -0
  92. package/platform/darwin-x64/tish +0 -0
  93. package/platform/linux-arm64/tish +0 -0
  94. package/platform/linux-x64/tish +0 -0
  95. package/platform/win32-x64/tish.exe +0 -0
@@ -2,8 +2,8 @@
2
2
  //! Grey preview hint below the line (like Node) and Tab for full list.
3
3
 
4
4
  use std::borrow::Cow;
5
- use std::cell::RefCell;
6
- use std::rc::Rc;
5
+
6
+ use tishlang_core::VmRef;
7
7
 
8
8
  use rustyline::completion::{Completer, Pair};
9
9
  use rustyline::highlight::Highlighter;
@@ -29,7 +29,7 @@ const ANSI_RESET: &str = "\x1b[0m";
29
29
 
30
30
  /// Tab completer that evaluates the expression before the last `.` and suggests property/method names.
31
31
  pub struct ReplCompleter {
32
- pub vm: Rc<RefCell<Vm>>,
32
+ pub vm: VmRef<Vm>,
33
33
  pub no_optimize: bool,
34
34
  }
35
35
 
@@ -39,7 +39,7 @@ fn resolve_and_merge_cargo_example_fixture() {
39
39
  panic!("expected import, got {:?}", first);
40
40
  };
41
41
  assert_eq!(from.as_ref(), "cargo:demo_shim");
42
- merge_modules(modules).unwrap();
42
+ let _ = merge_modules(modules).unwrap();
43
43
  }
44
44
 
45
45
  #[test]
@@ -66,12 +66,20 @@ fn file_content_hash(path: &Path) -> u64 {
66
66
  ///
67
67
  /// Cache is keyed by backend (native, cranelift, js, wasi) so e.g. cranelift and wasi
68
68
  /// compiles of the same file do not overwrite each other: .../cranelift/<stem>_<hash> vs .../wasi/<stem>_<hash>.wasm.
69
+ ///
70
+ /// The artifact **basename** must be unique per `(stem, hash, backend)`: nested `tish build`
71
+ /// uses it as the Cargo binary name under the workspace `target/release/`. If native and
72
+ /// cranelift both used `strict_equality_<hash>`, parallel `cargo nextest` could run those
73
+ /// tests concurrently and corrupt the same `target/release/...` output (Linux: ETXTBSY when
74
+ /// executing a binary still being written).
69
75
  fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
70
76
  let stem = path.file_stem().unwrap().to_string_lossy();
71
77
  let hash = file_content_hash(path);
72
78
  let hash8 = &format!("{:016x}", hash)[..8];
73
79
  let cache_base = integration_compile_cache_dir().join(backend);
74
80
  let _ = std::fs::create_dir_all(&cache_base);
81
+ // Include `backend` in the leaf name so nested cargo bin names never collide across backends.
82
+ let leaf = format!("{}__{}__{}", stem, backend, hash8);
75
83
 
76
84
  let (artifact_path, compile_args): (PathBuf, Vec<OsString>) = match backend {
77
85
  "native" => {
@@ -80,7 +88,7 @@ fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
80
88
  } else {
81
89
  ""
82
90
  };
83
- let cached = cache_base.join(format!("{}_{}{}", stem, hash8, ext));
91
+ let cached = cache_base.join(format!("{}{}", leaf, ext));
84
92
  let args = vec![
85
93
  OsString::from("build"),
86
94
  OsString::from(path),
@@ -95,7 +103,7 @@ fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
95
103
  } else {
96
104
  ""
97
105
  };
98
- let cached = cache_base.join(format!("{}_{}{}", stem, hash8, ext));
106
+ let cached = cache_base.join(format!("{}{}", leaf, ext));
99
107
  let args = vec![
100
108
  OsString::from("build"),
101
109
  OsString::from(path),
@@ -107,7 +115,7 @@ fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
107
115
  (cached, args)
108
116
  }
109
117
  "js" => {
110
- let cached = cache_base.join(format!("{}_{}.js", stem, hash8));
118
+ let cached = cache_base.join(format!("{}.js", leaf));
111
119
  let args = vec![
112
120
  OsString::from("build"),
113
121
  OsString::from(path),
@@ -119,7 +127,7 @@ fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
119
127
  (cached, args)
120
128
  }
121
129
  "wasi" => {
122
- let out_base = cache_base.join(format!("{}_{}", stem, hash8));
130
+ let out_base = cache_base.join(&leaf);
123
131
  let artifact = out_base.with_extension("wasm");
124
132
  let args = vec![
125
133
  OsString::from("build"),
@@ -397,7 +405,7 @@ fn test_async_await_run() {
397
405
  }
398
406
  }
399
407
 
400
- /// Run Promise and setTimeout module tests (require http feature).
408
+ /// Run Promise and setTimeout module tests (`promise` needs `http`; `settimeout` needs `timers`, which `http` enables).
401
409
  /// Ignored: tishlang_eval::run() does not run the event loop.
402
410
  #[test]
403
411
  #[cfg(feature = "http")]
@@ -455,7 +463,9 @@ fn test_vm_date_now() {
455
463
  // Library path
456
464
  let modules = tishlang_compile::resolve_project(&path, path.parent()).expect("resolve");
457
465
  tishlang_compile::detect_cycles(&modules).expect("cycles");
458
- let program = tishlang_compile::merge_modules(modules).expect("merge");
466
+ let program = tishlang_compile::merge_modules(modules)
467
+ .expect("merge")
468
+ .program;
459
469
  let chunk = tishlang_bytecode::compile(&program).expect("compile");
460
470
  let result = tishlang_vm::run(&chunk);
461
471
  assert!(
@@ -499,7 +509,9 @@ fn test_vm_index_assign_via_resolve() {
499
509
  .join("array_sort_minimal.tish");
500
510
  let modules = tishlang_compile::resolve_project(&path, path.parent()).expect("resolve");
501
511
  tishlang_compile::detect_cycles(&modules).expect("cycles");
502
- let program = tishlang_compile::merge_modules(modules).expect("merge");
512
+ let program = tishlang_compile::merge_modules(modules)
513
+ .expect("merge")
514
+ .program;
503
515
  let chunk = tishlang_bytecode::compile(&program).expect("compile");
504
516
  let result = tishlang_vm::run(&chunk);
505
517
  assert!(
@@ -53,7 +53,7 @@ fn test_and_shortcircuit_via_resolve_project() {
53
53
  let path = path.canonicalize().expect("path");
54
54
  let project_root = path.parent().unwrap();
55
55
  let modules = resolve_project(&path, Some(project_root)).expect("resolve");
56
- let program = merge_modules(modules).expect("merge");
56
+ let program = merge_modules(modules).expect("merge").program;
57
57
  let program = tishlang_opt::optimize(&program); // Mirror CLI
58
58
  let chunk = compile(&program).expect("compile");
59
59
  let result = tishlang_vm::run(&chunk);
@@ -30,6 +30,7 @@ pub enum TypeAnnotation {
30
30
  #[derive(Debug, Clone, PartialEq)]
31
31
  pub struct TypedParam {
32
32
  pub name: Arc<str>,
33
+ pub name_span: Span,
33
34
  pub type_ann: Option<TypeAnnotation>,
34
35
  pub default: Option<Expr>,
35
36
  }
@@ -64,11 +65,11 @@ impl FunParam {
64
65
  for el in elements {
65
66
  if let Some(el) = el {
66
67
  match el {
67
- DestructElement::Ident(n) => out.push(Arc::clone(n)),
68
+ DestructElement::Ident(n, _) => out.push(Arc::clone(n)),
68
69
  DestructElement::Pattern(p) => {
69
70
  Self::collect_pattern_binding_names(p, out);
70
71
  }
71
- DestructElement::Rest(n) => out.push(Arc::clone(n)),
72
+ DestructElement::Rest(n, _) => out.push(Arc::clone(n)),
72
73
  }
73
74
  }
74
75
  }
@@ -76,11 +77,11 @@ impl FunParam {
76
77
  DestructPattern::Object(props) => {
77
78
  for prop in props {
78
79
  match &prop.value {
79
- DestructElement::Ident(n) => out.push(Arc::clone(n)),
80
+ DestructElement::Ident(n, _) => out.push(Arc::clone(n)),
80
81
  DestructElement::Pattern(p) => {
81
82
  Self::collect_pattern_binding_names(p, out);
82
83
  }
83
- DestructElement::Rest(n) => out.push(Arc::clone(n)),
84
+ DestructElement::Rest(n, _) => out.push(Arc::clone(n)),
84
85
  }
85
86
  }
86
87
  }
@@ -101,11 +102,11 @@ pub enum DestructPattern {
101
102
  #[derive(Debug, Clone, PartialEq)]
102
103
  pub enum DestructElement {
103
104
  /// Simple binding: a
104
- Ident(Arc<str>),
105
+ Ident(Arc<str>, Span),
105
106
  /// Nested pattern: [a, b] or { x, y }
106
107
  Pattern(Box<DestructPattern>),
107
108
  /// Rest element: ...rest
108
- Rest(Arc<str>),
109
+ Rest(Arc<str>, Span),
109
110
  }
110
111
 
111
112
  /// Property in object destructuring pattern
@@ -123,12 +124,20 @@ pub enum ImportSpecifier {
123
124
  /// Named: { foo } or { foo as bar }
124
125
  Named {
125
126
  name: Arc<str>,
127
+ name_span: Span,
126
128
  alias: Option<Arc<str>>,
129
+ alias_span: Option<Span>,
127
130
  },
128
131
  /// Namespace: * as M
129
- Namespace(Arc<str>),
132
+ Namespace {
133
+ name: Arc<str>,
134
+ name_span: Span,
135
+ },
130
136
  /// Default: import X from "..."
131
- Default(Arc<str>),
137
+ Default {
138
+ name: Arc<str>,
139
+ name_span: Span,
140
+ },
132
141
  }
133
142
 
134
143
  /// Export declaration: named (const/let/fn) or default
@@ -153,6 +162,7 @@ pub enum Statement {
153
162
  },
154
163
  VarDecl {
155
164
  name: Arc<str>,
165
+ name_span: Span,
156
166
  mutable: bool, // true for `let`, false for `const`
157
167
  type_ann: Option<TypeAnnotation>,
158
168
  init: Option<Expr>,
@@ -189,6 +199,7 @@ pub enum Statement {
189
199
  },
190
200
  ForOf {
191
201
  name: Arc<str>,
202
+ name_span: Span,
192
203
  iterable: Expr,
193
204
  body: Box<Statement>,
194
205
  span: Span,
@@ -206,6 +217,7 @@ pub enum Statement {
206
217
  FunDecl {
207
218
  async_: bool,
208
219
  name: Arc<str>,
220
+ name_span: Span,
209
221
  params: Vec<FunParam>,
210
222
  rest_param: Option<TypedParam>,
211
223
  return_type: Option<TypeAnnotation>,
@@ -230,6 +242,7 @@ pub enum Statement {
230
242
  Try {
231
243
  body: Box<Statement>,
232
244
  catch_param: Option<Arc<str>>,
245
+ catch_param_span: Option<Span>,
233
246
  catch_body: Option<Box<Statement>>,
234
247
  finally_body: Option<Box<Statement>>,
235
248
  span: Span,
@@ -243,6 +256,31 @@ pub enum Statement {
243
256
  declaration: Box<ExportDeclaration>,
244
257
  span: Span,
245
258
  },
259
+ /// `type Name = Type` (erased at runtime; for checker / declaration files).
260
+ TypeAlias {
261
+ name: Arc<str>,
262
+ name_span: Span,
263
+ ty: TypeAnnotation,
264
+ span: Span,
265
+ },
266
+ /// `declare let name: T` or `declare const name: T`
267
+ DeclareVar {
268
+ name: Arc<str>,
269
+ name_span: Span,
270
+ type_ann: Option<TypeAnnotation>,
271
+ const_: bool,
272
+ span: Span,
273
+ },
274
+ /// `declare [async] function name(...): R` (no body).
275
+ DeclareFun {
276
+ async_: bool,
277
+ name: Arc<str>,
278
+ name_span: Span,
279
+ params: Vec<FunParam>,
280
+ rest_param: Option<TypedParam>,
281
+ return_type: Option<TypeAnnotation>,
282
+ span: Span,
283
+ },
246
284
  }
247
285
 
248
286
  #[derive(Debug, Clone, PartialEq)]
@@ -550,6 +588,39 @@ pub enum UnaryOp {
550
588
 
551
589
  #[derive(Debug, Clone, PartialEq)]
552
590
  pub enum MemberProp {
553
- Name(Arc<str>),
591
+ /// Property name in `obj.prop` / `obj?.prop` (span covers **prop** only).
592
+ Name {
593
+ name: Arc<str>,
594
+ span: Span,
595
+ },
554
596
  Expr(Box<Expr>), // for computed property
555
597
  }
598
+
599
+ impl Statement {
600
+ /// Source span covering this statement (including nested bodies where applicable).
601
+ pub fn span(&self) -> Span {
602
+ match self {
603
+ Statement::Block { span, .. }
604
+ | Statement::VarDecl { span, .. }
605
+ | Statement::VarDeclDestructure { span, .. }
606
+ | Statement::ExprStmt { span, .. }
607
+ | Statement::If { span, .. }
608
+ | Statement::While { span, .. }
609
+ | Statement::For { span, .. }
610
+ | Statement::ForOf { span, .. }
611
+ | Statement::Return { span, .. }
612
+ | Statement::Break { span, .. }
613
+ | Statement::Continue { span, .. }
614
+ | Statement::FunDecl { span, .. }
615
+ | Statement::Switch { span, .. }
616
+ | Statement::DoWhile { span, .. }
617
+ | Statement::Throw { span, .. }
618
+ | Statement::Try { span, .. }
619
+ | Statement::Import { span, .. }
620
+ | Statement::Export { span, .. }
621
+ | Statement::TypeAlias { span, .. }
622
+ | Statement::DeclareVar { span, .. }
623
+ | Statement::DeclareFun { span, .. } => *span,
624
+ }
625
+ }
626
+ }
@@ -5,3 +5,7 @@ edition = "2021"
5
5
  description = "Shared build utilities for Tish (workspace discovery, path resolution)"
6
6
  license-file = { workspace = true }
7
7
  repository = { workspace = true }
8
+
9
+ [dependencies]
10
+ # Bundled `protoc` for nested `cargo build` (Lance / prost-build) when none is on PATH.
11
+ protoc-bin-vendored = "3"
@@ -137,6 +137,39 @@ fn tish_root_from_project_cargo_files(mut start: PathBuf) -> Option<PathBuf> {
137
137
  /// Returns the directory containing the workspace Cargo.toml (with [workspace]).
138
138
  /// Used when building native binaries, WASM, or locating runtime crates.
139
139
  pub fn find_workspace_root() -> Result<PathBuf, String> {
140
+ // Strategy 0: explicit checkout (e.g. tish-hub `npm run dev` / native build from a JS-only tree)
141
+ if let Ok(root) = std::env::var("TISHLANG_WORKSPACE") {
142
+ let trimmed = root.trim();
143
+ if !trimmed.is_empty() {
144
+ let p = PathBuf::from(trimmed);
145
+ if p.is_dir() && is_tish_workspace_root(&p) {
146
+ return p.canonicalize().map_err(|e| {
147
+ format!(
148
+ "TISHLANG_WORKSPACE is set but not a usable directory: {}",
149
+ e
150
+ )
151
+ });
152
+ }
153
+ // Non-empty but invalid: fall through (sibling `tish/` discovery may still apply).
154
+ }
155
+ }
156
+
157
+ // Strategy 0b: monorepo layout `…/<parent>/tish-hub` (cwd) next to `…/<parent>/tish` (language repo).
158
+ // Works without `TISHLANG_WORKSPACE` so older `tish` binaries still find the compiler tree.
159
+ if let Ok(mut dir) = std::env::current_dir() {
160
+ for _ in 0..24 {
161
+ let candidate = dir.join("tish");
162
+ if is_tish_workspace_root(&candidate) {
163
+ return candidate.canonicalize().map_err(|e| {
164
+ format!("Cannot canonicalize Tish workspace {}: {}", candidate.display(), e)
165
+ });
166
+ }
167
+ if !dir.pop() {
168
+ break;
169
+ }
170
+ }
171
+ }
172
+
140
173
  // Strategy 1: CARGO_MANIFEST_DIR (works during cargo build/run from workspace)
141
174
  if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
142
175
  let path = PathBuf::from(&manifest_dir);
@@ -301,18 +334,70 @@ pub fn create_build_dir(prefix: &str, out_name: &str) -> Result<PathBuf, String>
301
334
  Ok(build_dir)
302
335
  }
303
336
 
337
+ /// `PROTOC` for prost-build in transitive crates (e.g. lance-encoding) during nested `cargo build`.
338
+ /// Respects an existing `PROTOC`, then `protoc` on `PATH`, then `protoc-bin-vendored`.
339
+ fn protoc_for_nested_cargo() -> Option<PathBuf> {
340
+ // Empty or non-file PROTOC must not short-circuit: prost-build treats "" like a missing binary.
341
+ if let Some(v) = std::env::var_os("PROTOC") {
342
+ if !v.is_empty() {
343
+ let p = PathBuf::from(&v);
344
+ if p.is_file() {
345
+ return None;
346
+ }
347
+ }
348
+ }
349
+ // Prefer vendored protoc over PATH: a broken or non-executable `protoc` on PATH still passes `is_file()`.
350
+ if let Ok(p) = protoc_bin_vendored::protoc_bin_path() {
351
+ return Some(p);
352
+ }
353
+ let ext = if cfg!(windows) { ".exe" } else { "" };
354
+ let name = format!("protoc{}", ext);
355
+ if let Some(paths) = std::env::var_os("PATH") {
356
+ for dir in std::env::split_paths(&paths) {
357
+ let candidate = dir.join(&name);
358
+ if candidate.is_file() {
359
+ return Some(candidate);
360
+ }
361
+ }
362
+ }
363
+ None
364
+ }
365
+
304
366
  /// Run cargo build in the given directory.
305
367
  /// If target_dir is Some, use that for --target-dir (e.g. workspace target for caching).
306
368
  pub fn run_cargo_build(build_dir: &Path, target_dir: Option<&Path>) -> Result<(), String> {
307
369
  let target_dir = target_dir
308
370
  .map(|p| p.to_path_buf())
309
371
  .unwrap_or_else(|| build_dir.join("target"));
310
- let output = Command::new("cargo")
311
- .args(["build", "--release", "--target-dir"])
372
+ // Default to target-cpu=native so the emitted binary uses every SIMD / ISA
373
+ // extension the build host supports. Callers can override by pre-setting
374
+ // RUSTFLAGS in the environment.
375
+ let existing_rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
376
+ let merged_rustflags = if existing_rustflags.is_empty() {
377
+ "-C target-cpu=native".to_string()
378
+ } else if existing_rustflags.contains("target-cpu") {
379
+ existing_rustflags
380
+ } else {
381
+ format!("{} -C target-cpu=native", existing_rustflags)
382
+ };
383
+ // Nested `cargo build` (e.g. `tish build --native-backend rust`) inherits the parent
384
+ // environment. CI often sets `RUSTC_WRAPPER=sccache`; wrapping this inner compile too can
385
+ // cause flaky or failed builds (LTO / temp-crate paths). Use plain rustc here; the main
386
+ // workspace build still benefits from the wrapper.
387
+ let mut cmd = Command::new("cargo");
388
+ cmd.args(["build", "--release", "--target-dir"])
312
389
  .arg(&target_dir)
313
390
  .current_dir(build_dir)
314
391
  .env_remove("CARGO_TARGET_DIR")
392
+ .env_remove("RUSTC_WRAPPER")
393
+ .env_remove("RUSTC_WORKSPACE_WRAPPER")
394
+ .env_remove("CARGO_BUILD_RUSTC_WRAPPER")
315
395
  .env("CARGO_TERM_PROGRESS", "always")
396
+ .env("RUSTFLAGS", &merged_rustflags);
397
+ if let Some(protoc) = protoc_for_nested_cargo() {
398
+ cmd.env("PROTOC", protoc);
399
+ }
400
+ let output = cmd
316
401
  .output()
317
402
  .map_err(|e| format!("Failed to run cargo: {}", e))?;
318
403
 
@@ -327,6 +412,24 @@ pub fn run_cargo_build(build_dir: &Path, target_dir: Option<&Path>) -> Result<()
327
412
  Ok(())
328
413
  }
329
414
 
415
+ #[cfg(test)]
416
+ mod protoc_tests {
417
+ use super::*;
418
+
419
+ #[test]
420
+ fn protoc_for_nested_cargo_without_env_uses_vendored_or_path() {
421
+ let _lock = std::sync::Mutex::new(());
422
+ let _guard = _lock.lock().unwrap();
423
+ std::env::remove_var("PROTOC");
424
+ let p = protoc_for_nested_cargo().expect("expected vendored or PATH protoc");
425
+ assert!(
426
+ p.exists(),
427
+ "resolved protoc should exist: {}",
428
+ p.display()
429
+ );
430
+ }
431
+ }
432
+
330
433
  /// Find the built binary in target/release.
331
434
  pub fn find_release_binary(binary_dir: &Path, bin_name: &str) -> Result<PathBuf, String> {
332
435
  let binary_no_ext = binary_dir.join(bin_name);
@@ -8,9 +8,13 @@ description = "Shared builtin implementations for Tish (array, string, object, m
8
8
  license-file = { workspace = true }
9
9
  repository = { workspace = true }
10
10
  [dependencies]
11
- rand = "0.10.0"
11
+ rand = "0.10.1"
12
12
  tishlang_core = { path = "../tish_core", version = ">=0.1" }
13
13
 
14
14
  [features]
15
15
  default = []
16
16
  regex = ["tishlang_core/regex"]
17
+ # Propagate the `send-values` feature from `tishlang_core` so that closures
18
+ # handed to us via `NativeFn` pick up the `Send + Sync` bound automatically
19
+ # when multi-threaded `Value`s are enabled upstream.
20
+ send-values = ["tishlang_core/send-values"]
@@ -1,13 +1,14 @@
1
1
  //! Array builtin methods.
2
2
 
3
3
  use crate::helpers::normalize_index;
4
+ use tishlang_core::VmRef;
4
5
  use std::cell::RefCell;
5
6
  use std::rc::Rc;
6
7
  use tishlang_core::Value;
7
8
 
8
9
  /// Create a new array Value from a Vec of Values.
9
10
  pub fn from_vec(v: Vec<Value>) -> Value {
10
- Value::Array(Rc::new(RefCell::new(v)))
11
+ Value::Array(VmRef::new(v))
11
12
  }
12
13
 
13
14
  /// Get the length of an array.
@@ -110,7 +111,7 @@ pub fn join(arr: &Value, sep: &Value) -> Value {
110
111
  pub fn reverse(arr: &Value) -> Value {
111
112
  if let Value::Array(arr) = arr {
112
113
  arr.borrow_mut().reverse();
113
- Value::Array(Rc::clone(arr))
114
+ Value::Array(arr.clone())
114
115
  } else {
115
116
  Value::Null
116
117
  }
@@ -122,7 +123,7 @@ pub fn shuffle(arr: &Value) -> Value {
122
123
  let mut v = arr.borrow().clone();
123
124
  use rand::seq::SliceRandom;
124
125
  v.shuffle(&mut rand::rng());
125
- Value::Array(Rc::new(RefCell::new(v)))
126
+ Value::Array(VmRef::new(v))
126
127
  } else {
127
128
  Value::Null
128
129
  }
@@ -141,7 +142,7 @@ pub fn splice(arr: &Value, start: &Value, delete_count: Option<&Value>, items: &
141
142
  let removed: Vec<Value> = arr_mut
142
143
  .splice(start_idx..start_idx + actual_delete, items.iter().cloned())
143
144
  .collect();
144
- Value::Array(Rc::new(RefCell::new(removed)))
145
+ Value::Array(VmRef::new(removed))
145
146
  } else {
146
147
  Value::Null
147
148
  }
@@ -158,7 +159,7 @@ pub fn slice(arr: &Value, start: &Value, end: &Value) -> Value {
158
159
  } else {
159
160
  vec![]
160
161
  };
161
- Value::Array(Rc::new(RefCell::new(sliced)))
162
+ Value::Array(VmRef::new(sliced))
162
163
  } else {
163
164
  Value::Null
164
165
  }
@@ -174,7 +175,7 @@ pub fn concat(arr: &Value, args: &[Value]) -> Value {
174
175
  result.push(v.clone());
175
176
  }
176
177
  }
177
- Value::Array(Rc::new(RefCell::new(result)))
178
+ Value::Array(VmRef::new(result))
178
179
  } else {
179
180
  Value::Null
180
181
  }
@@ -200,7 +201,7 @@ pub fn flat(arr: &Value, depth: &Value) -> Value {
200
201
  };
201
202
  let mut result = Vec::new();
202
203
  flatten(&arr.borrow(), d, &mut result);
203
- Value::Array(Rc::new(RefCell::new(result)))
204
+ Value::Array(VmRef::new(result))
204
205
  } else {
205
206
  Value::Null
206
207
  }
@@ -217,7 +218,7 @@ pub fn map(arr: &Value, callback: &Value) -> Value {
217
218
  .enumerate()
218
219
  .map(|(i, v)| cb(&[v.clone(), Value::Number(i as f64)]))
219
220
  .collect();
220
- Value::Array(Rc::new(RefCell::new(result)))
221
+ Value::Array(VmRef::new(result))
221
222
  } else {
222
223
  Value::Null
223
224
  }
@@ -238,7 +239,7 @@ pub fn filter(arr: &Value, callback: &Value) -> Value {
238
239
  }
239
240
  })
240
241
  .collect();
241
- Value::Array(Rc::new(RefCell::new(result)))
242
+ Value::Array(VmRef::new(result))
242
243
  } else {
243
244
  Value::Null
244
245
  }
@@ -340,7 +341,7 @@ pub fn flat_map(arr: &Value, callback: &Value) -> Value {
340
341
  result.push(mapped);
341
342
  }
342
343
  }
343
- Value::Array(Rc::new(RefCell::new(result)))
344
+ Value::Array(VmRef::new(result))
344
345
  } else {
345
346
  Value::Null
346
347
  }
@@ -352,7 +353,7 @@ where
352
353
  {
353
354
  if let Value::Array(arr) = arr {
354
355
  arr.borrow_mut().sort_by(cmp);
355
- Value::Array(Rc::clone(arr))
356
+ Value::Array(arr.clone())
356
357
  } else {
357
358
  Value::Null
358
359
  }
@@ -387,7 +388,7 @@ pub fn sort_with_comparator(arr: &Value, comparator: &Value) -> Value {
387
388
  .map(|i| std::mem::replace(&mut elements[i], Value::Null))
388
389
  .collect();
389
390
  drop(arr_mut);
390
- Value::Array(Rc::clone(arr))
391
+ Value::Array(arr.clone())
391
392
  } else {
392
393
  Value::Null
393
394
  }