@tishlang/tish 1.4.2 → 1.6.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 (39) hide show
  1. package/bin/tish +0 -0
  2. package/crates/tish/Cargo.toml +2 -2
  3. package/crates/tish/src/cli_help.rs +504 -0
  4. package/crates/tish/src/main.rs +76 -90
  5. package/crates/tish/src/repl_completion.rs +1 -1
  6. package/crates/tish/tests/cargo_example_compile.rs +65 -0
  7. package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
  8. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
  9. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
  10. package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
  11. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
  12. package/crates/tish/tests/integration_test.rs +48 -0
  13. package/crates/tish_build_utils/src/lib.rs +204 -1
  14. package/crates/tish_builtins/src/string.rs +248 -0
  15. package/crates/tish_bytecode/Cargo.toml +1 -0
  16. package/crates/tish_bytecode/src/compiler.rs +289 -66
  17. package/crates/tish_bytecode/src/opcode.rs +13 -3
  18. package/crates/tish_bytecode/src/peephole.rs +21 -16
  19. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
  20. package/crates/tish_compile/Cargo.toml +1 -0
  21. package/crates/tish_compile/src/codegen.rs +277 -93
  22. package/crates/tish_compile/src/lib.rs +7 -4
  23. package/crates/tish_compile/src/resolve.rs +418 -40
  24. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +1 -0
  25. package/crates/tish_core/src/value.rs +1 -0
  26. package/crates/tish_eval/src/eval.rs +49 -1
  27. package/crates/tish_eval/src/lib.rs +1 -1
  28. package/crates/tish_native/src/build.rs +86 -17
  29. package/crates/tish_native/src/lib.rs +36 -16
  30. package/crates/tish_runtime/src/lib.rs +4 -0
  31. package/crates/tish_vm/src/lib.rs +1 -1
  32. package/crates/tish_vm/src/vm.rs +165 -19
  33. package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
  34. package/package.json +1 -1
  35. package/platform/darwin-arm64/tish +0 -0
  36. package/platform/darwin-x64/tish +0 -0
  37. package/platform/linux-arm64/tish +0 -0
  38. package/platform/linux-x64/tish +0 -0
  39. package/platform/win32-x64/tish.exe +0 -0
@@ -512,9 +512,18 @@ impl Evaluator {
512
512
 
513
513
  /// Load and evaluate a module, returning its exports object. Uses cache.
514
514
  fn load_module(&mut self, from: &str) -> Result<Value, EvalError> {
515
+ if from.starts_with("cargo:") {
516
+ return Err(EvalError::Error(
517
+ "cargo:… imports are only supported by `tish build` with the Rust native backend.".into(),
518
+ ));
519
+ }
515
520
  if from.starts_with("tish:") {
516
521
  return self.load_builtin_module(from);
517
522
  }
523
+ // Scoped native modules (e.g. `@tishlang/waterui`) registered via `TishNativeModule::virtual_builtin_modules`.
524
+ if self.virtual_builtins.borrow().get(from).is_some() {
525
+ return self.load_builtin_module(from);
526
+ }
518
527
  let dir = self.current_dir.borrow().clone().ok_or_else(|| {
519
528
  EvalError::Error("Cannot resolve imports: no current file directory (use run_file)".to_string())
520
529
  })?;
@@ -595,6 +604,11 @@ impl Evaluator {
595
604
 
596
605
  /// Load built-in module (tish:fs, tish:http, tish:process, …) or a virtual module from native crates.
597
606
  fn load_builtin_module(&self, spec: &str) -> Result<Value, EvalError> {
607
+ if spec.starts_with("cargo:") {
608
+ return Err(EvalError::Error(
609
+ "cargo:… imports are only supported when compiling with `tish build` and the Rust native backend. They link Cargo crates via package.json tish.rustDependencies and a generated native wrapper — not the interpreter or VM.".into(),
610
+ ));
611
+ }
598
612
  if let Some(v) = self.virtual_builtins.borrow().get(spec) {
599
613
  return Ok(v.clone());
600
614
  }
@@ -1203,6 +1217,9 @@ impl Evaluator {
1203
1217
  });
1204
1218
  return Ok(Value::Number(found.unwrap_or(-1.0)));
1205
1219
  }
1220
+ "lastIndexOf" => {
1221
+ return Ok(Self::string_last_index_of_eval(&arg_vals, s));
1222
+ }
1206
1223
  "includes" => {
1207
1224
  let search = match arg_vals.first() {
1208
1225
  Some(Value::String(ss)) => ss.as_ref(),
@@ -1474,7 +1491,13 @@ impl Evaluator {
1474
1491
  }
1475
1492
  }
1476
1493
 
1477
- // Fall through to normal function call
1494
+ // Fall through to normal function call. `get_prop` only implements `length` on
1495
+ // strings, so method calls would otherwise become `call_func(Null)` → Not a function.
1496
+ if let Value::String(s) = &obj {
1497
+ if method_name.as_ref() == "lastIndexOf" {
1498
+ return Ok(Self::string_last_index_of_eval(&arg_vals, s));
1499
+ }
1500
+ }
1478
1501
  let f = self.get_prop(&obj, method_name).map_err(EvalError::Error)?;
1479
1502
  return self.call_func(&f, &arg_vals);
1480
1503
  }
@@ -2638,6 +2661,31 @@ impl Evaluator {
2638
2661
  Self::bind_destruct_pattern_scoped(&self.scope, pattern, value, mutable)
2639
2662
  }
2640
2663
 
2664
+ /// `String.prototype.lastIndexOf` (interpreter). Kept as a helper so dispatch cannot fall
2665
+ /// through to [`Self::get_prop`] + [`Self::call_func`] for string receivers.
2666
+ fn string_last_index_of_eval(arg_vals: &[Value], receiver: &Arc<str>) -> Value {
2667
+ let search = match arg_vals.first() {
2668
+ Some(Value::String(ss)) => ss.as_ref(),
2669
+ _ => return Value::Number(-1.0),
2670
+ };
2671
+ let position_core: tishlang_core::Value = match arg_vals.get(1) {
2672
+ None => tishlang_core::Value::Number(f64::INFINITY),
2673
+ Some(Value::Null) => tishlang_core::Value::Null,
2674
+ Some(Value::Number(n)) => tishlang_core::Value::Number(*n),
2675
+ Some(Value::Bool(b)) => tishlang_core::Value::Bool(*b),
2676
+ Some(_) => tishlang_core::Value::Number(0.0),
2677
+ };
2678
+ let out = tishlang_builtins::string::last_index_of_str(
2679
+ receiver.as_ref(),
2680
+ search,
2681
+ &position_core,
2682
+ );
2683
+ match out {
2684
+ tishlang_core::Value::Number(n) => Value::Number(n),
2685
+ _ => Value::Number(-1.0),
2686
+ }
2687
+ }
2688
+
2641
2689
  fn get_prop(&self, obj: &Value, key: &str) -> Result<Value, String> {
2642
2690
  match obj {
2643
2691
  Value::Object(map) => Ok(map.borrow().get(key).cloned().unwrap_or(Value::Null)),
@@ -3,7 +3,7 @@
3
3
  mod eval;
4
4
  #[cfg(feature = "http")]
5
5
  mod http;
6
- mod value_convert;
6
+ pub mod value_convert;
7
7
  #[cfg(feature = "http")]
8
8
  mod promise;
9
9
  #[cfg(feature = "http")]
@@ -5,11 +5,47 @@ use std::path::Path;
5
5
 
6
6
  use tishlang_compile::ResolvedNativeModule;
7
7
 
8
+ /// `tishlang_runtime` Cargo feature names (subset of CLI / compile feature names).
9
+ const RUNTIME_CARGO_FEATURES: &[&str] = &["http", "fs", "process", "regex", "ws"];
10
+
11
+ /// Map CLI/compile features to flags passed to `tishlang_runtime` in the temp crate's Cargo.toml.
12
+ /// `full` enables every optional runtime capability (matches `tish build --feature full` / LANGUAGE.md).
13
+ fn runtime_features_for_cargo(features: &[String]) -> Vec<String> {
14
+ let mut out = Vec::new();
15
+ for f in features {
16
+ if f == "full" {
17
+ for name in RUNTIME_CARGO_FEATURES {
18
+ if !out.iter().any(|x: &String| x == *name) {
19
+ out.push((*name).to_string());
20
+ }
21
+ }
22
+ continue;
23
+ }
24
+ if RUNTIME_CARGO_FEATURES.contains(&f.as_str()) && !out.contains(f) {
25
+ out.push(f.clone());
26
+ }
27
+ }
28
+ out
29
+ }
30
+
31
+ /// Inject `mod generated_native;` after the crate attribute so the binary crate can call `crate::generated_native::…`.
32
+ fn inject_generated_native_mod(rust_code: &str) -> String {
33
+ if let Some(pos) = rust_code.find("\n\n") {
34
+ let (a, b) = rust_code.split_at(pos + 2);
35
+ format!("{}mod generated_native;\n{}", a, b)
36
+ } else {
37
+ format!("{}\n\nmod generated_native;\n", rust_code)
38
+ }
39
+ }
40
+
8
41
  pub fn build_via_cargo(
9
42
  rust_code: &str,
10
43
  native_modules: Vec<ResolvedNativeModule>,
11
44
  output_path: &Path,
12
45
  features: &[String],
46
+ extra_dependencies_toml: &str,
47
+ generated_native_rs: Option<&str>,
48
+ project_root: Option<&Path>,
13
49
  ) -> Result<(), String> {
14
50
  let out_name = output_path
15
51
  .file_stem()
@@ -17,17 +53,14 @@ pub fn build_via_cargo(
17
53
  .unwrap_or("tish_out");
18
54
  let build_dir = tishlang_build_utils::create_build_dir("tish_build", out_name)?;
19
55
 
20
- let runtime_path = tishlang_build_utils::find_runtime_path()?;
56
+ let runtime_path = tishlang_build_utils::find_runtime_path_for_project(project_root)?;
21
57
 
22
- let runtime_features: Vec<&str> = features
23
- .iter()
24
- .filter(|f| ["http", "fs", "process", "regex", "ws"].contains(&f.as_str()))
25
- .map(String::as_str)
26
- .collect();
27
- let features_str = if runtime_features.is_empty() {
58
+ let runtime_features = runtime_features_for_cargo(features);
59
+ let runtime_refs: Vec<&str> = runtime_features.iter().map(String::as_str).collect();
60
+ let features_str = if runtime_refs.is_empty() {
28
61
  String::new()
29
62
  } else {
30
- format!(", features = {:?}", runtime_features)
63
+ format!(", features = {:?}", runtime_refs)
31
64
  };
32
65
 
33
66
  let needs_tokio = rust_code.contains("#[tokio::main]");
@@ -39,12 +72,28 @@ pub fn build_via_cargo(
39
72
 
40
73
  let native_deps: String = native_modules
41
74
  .iter()
75
+ .filter(|m| m.use_path_dependency)
42
76
  .map(|m| {
43
77
  let path = m.crate_path.display().to_string().replace('\\', "/");
44
78
  format!("{} = {{ path = {:?} }}\n", m.package_name, path)
45
79
  })
46
80
  .collect();
47
81
 
82
+ let mut more_deps = String::new();
83
+ more_deps.push_str(&tokio_dep);
84
+ if !native_deps.is_empty() {
85
+ more_deps.push_str(&format!("\n{}", native_deps));
86
+ }
87
+ if !extra_dependencies_toml.trim().is_empty() {
88
+ more_deps.push_str(&format!("\n{}", extra_dependencies_toml));
89
+ }
90
+
91
+ let rust_main = if generated_native_rs.is_some() {
92
+ inject_generated_native_mod(rust_code)
93
+ } else {
94
+ rust_code.to_string()
95
+ };
96
+
48
97
  let tish_ui_path = std::path::Path::new(&runtime_path)
49
98
  .parent()
50
99
  .ok_or_else(|| "invalid tishlang_runtime path (no parent)".to_string())?
@@ -76,23 +125,22 @@ codegen-units = 1
76
125
  lto = "thin"
77
126
 
78
127
  [dependencies]
79
- tishlang_runtime = {{ path = {:?}{} }}{}{}{}
80
- "#,
128
+ tishlang_runtime = {{ path = {:?}{} }}
129
+ {}{}"#,
81
130
  out_name,
82
131
  runtime_path,
83
132
  features_str,
84
- tokio_dep,
85
- if native_deps.is_empty() {
86
- String::new()
87
- } else {
88
- format!("\n{}", native_deps)
89
- },
133
+ more_deps,
90
134
  ui_dep
91
135
  );
92
136
 
93
137
  fs::write(build_dir.join("Cargo.toml"), cargo_toml)
94
138
  .map_err(|e| format!("Cannot write Cargo.toml: {}", e))?;
95
- fs::write(build_dir.join("src/main.rs"), rust_code)
139
+ if let Some(gen) = generated_native_rs {
140
+ fs::write(build_dir.join("src/generated_native.rs"), gen)
141
+ .map_err(|e| format!("Cannot write generated_native.rs: {}", e))?;
142
+ }
143
+ fs::write(build_dir.join("src/main.rs"), rust_main)
96
144
  .map_err(|e| format!("Cannot write main.rs: {}", e))?;
97
145
 
98
146
  let workspace_target = Path::new(&runtime_path)
@@ -114,3 +162,24 @@ tishlang_runtime = {{ path = {:?}{} }}{}{}{}
114
162
  Ok(())
115
163
  }
116
164
 
165
+ #[cfg(test)]
166
+ mod tests {
167
+ use super::runtime_features_for_cargo;
168
+
169
+ #[test]
170
+ fn runtime_features_full_expands() {
171
+ let f = runtime_features_for_cargo(&["full".to_string()]);
172
+ assert!(f.contains(&"http".to_string()));
173
+ assert!(f.contains(&"fs".to_string()));
174
+ assert!(f.contains(&"process".to_string()));
175
+ assert!(f.contains(&"regex".to_string()));
176
+ assert!(f.contains(&"ws".to_string()));
177
+ }
178
+
179
+ #[test]
180
+ fn runtime_features_merges_full_and_specific() {
181
+ let f = runtime_features_for_cargo(&["full".to_string(), "http".to_string()]);
182
+ assert_eq!(f.len(), 5);
183
+ }
184
+ }
185
+
@@ -57,18 +57,27 @@ pub fn compile_to_native(
57
57
 
58
58
  match backend {
59
59
  Backend::Rust => {
60
- let (rust_code, native_modules) = tishlang_compile::compile_project_full(
61
- entry_path,
60
+ let (rust_code, native_modules, effective_features, native_build) =
61
+ tishlang_compile::compile_project_full(
62
+ entry_path,
63
+ project_root,
64
+ features,
65
+ optimize,
66
+ )
67
+ .map_err(|e| NativeError {
68
+ message: e.to_string(),
69
+ })?;
70
+
71
+ crate::build::build_via_cargo(
72
+ &rust_code,
73
+ native_modules,
74
+ output_path,
75
+ &effective_features,
76
+ &native_build.rust_dependencies_toml,
77
+ native_build.generated_native_rs.as_deref(),
62
78
  project_root,
63
- features,
64
- optimize,
65
79
  )
66
- .map_err(|e| NativeError {
67
- message: e.to_string(),
68
- })?;
69
-
70
- crate::build::build_via_cargo(&rust_code, native_modules, output_path, features)
71
- .map_err(|e| NativeError { message: e })
80
+ .map_err(|e| NativeError { message: e })
72
81
  }
73
82
  Backend::Cranelift => {
74
83
  let modules = tishlang_compile::resolve_project(entry_path, project_root)
@@ -91,7 +100,7 @@ pub fn compile_to_native(
91
100
 
92
101
  if tishlang_compile::has_external_native_imports(&program) {
93
102
  return Err(NativeError {
94
- message: "Cranelift backend does not support external native imports (tish:egui, @scope/pkg). Built-in tish:fs, tish:http, tish:process are supported. Use --native-backend rust for external modules.".to_string(),
103
+ message: "Cranelift backend does not support external native imports (tish:…, cargo:…, @scope/pkg). Built-in tish:fs, tish:http, tish:process are supported. Use --native-backend rust for external modules.".to_string(),
95
104
  });
96
105
  }
97
106
 
@@ -127,7 +136,7 @@ pub fn compile_to_native(
127
136
  };
128
137
  if tishlang_compile::has_external_native_imports(&program) {
129
138
  return Err(NativeError {
130
- message: "LLVM backend does not support external native imports. Built-in tish:fs, tish:http, tish:process are supported.".to_string(),
139
+ message: "LLVM backend does not support external native imports (tish:…, cargo:…, @scope/pkg). Built-in tish:fs, tish:http, tish:process are supported.".to_string(),
131
140
  });
132
141
  }
133
142
  let chunk = if optimize {
@@ -175,6 +184,8 @@ pub fn compile_program_to_native(
175
184
  let root = project_root.unwrap_or_else(|| Path::new("."));
176
185
  let native_modules = tishlang_compile::resolve_native_modules(&program, root)
177
186
  .map_err(|e| NativeError { message: e })?;
187
+ let native_build = tishlang_compile::compute_native_build_artifacts(&program, root, &native_modules)
188
+ .map_err(|e| NativeError { message: e })?;
178
189
  let mut all_features = features.to_vec();
179
190
  for f in tishlang_compile::extract_native_import_features(&program) {
180
191
  if !all_features.contains(&f) {
@@ -186,18 +197,27 @@ pub fn compile_program_to_native(
186
197
  project_root,
187
198
  &all_features,
188
199
  &native_modules,
200
+ &native_build.native_init,
189
201
  optimize,
190
202
  )
191
203
  .map_err(|e| NativeError {
192
204
  message: e.message,
193
205
  })?;
194
- crate::build::build_via_cargo(&rust_code, native_modules, output_path, &all_features)
195
- .map_err(|e| NativeError { message: e })
206
+ crate::build::build_via_cargo(
207
+ &rust_code,
208
+ native_modules,
209
+ output_path,
210
+ &all_features,
211
+ &native_build.rust_dependencies_toml,
212
+ native_build.generated_native_rs.as_deref(),
213
+ Some(root),
214
+ )
215
+ .map_err(|e| NativeError { message: e })
196
216
  }
197
217
  Backend::Cranelift => {
198
218
  if tishlang_compile::has_external_native_imports(program) {
199
219
  return Err(NativeError {
200
- message: "Cranelift backend does not support external native imports. Built-in tish:fs, tish:http, tish:process are supported.".to_string(),
220
+ message: "Cranelift backend does not support external native imports (tish:…, cargo:…, @scope/pkg). Built-in tish:fs, tish:http, tish:process are supported.".to_string(),
201
221
  });
202
222
  }
203
223
  let program = if optimize { tishlang_opt::optimize(program) } else { program.clone() };
@@ -213,7 +233,7 @@ pub fn compile_program_to_native(
213
233
  Backend::Llvm => {
214
234
  if tishlang_compile::has_external_native_imports(program) {
215
235
  return Err(NativeError {
216
- message: "LLVM backend does not support external native imports.".to_string(),
236
+ message: "LLVM backend does not support external native imports (tish:…, cargo:…, @scope/pkg).".to_string(),
217
237
  });
218
238
  }
219
239
  let program = if optimize { tishlang_opt::optimize(program) } else { program.clone() };
@@ -71,6 +71,7 @@ pub use tishlang_builtins::string::{
71
71
  repeat as string_repeat_impl,
72
72
  pad_start as string_pad_start_impl,
73
73
  pad_end as string_pad_end_impl,
74
+ last_index_of as string_last_index_of_impl,
74
75
  };
75
76
 
76
77
  // Wrapper functions to maintain API compatibility
@@ -119,6 +120,9 @@ pub fn string_char_code_at(s: &Value, idx: &Value) -> Value { string_char_code_a
119
120
  pub fn string_repeat(s: &Value, count: &Value) -> Value { string_repeat_impl(s, count) }
120
121
  pub fn string_pad_start(s: &Value, target_len: &Value, pad: &Value) -> Value { string_pad_start_impl(s, target_len, pad) }
121
122
  pub fn string_pad_end(s: &Value, target_len: &Value, pad: &Value) -> Value { string_pad_end_impl(s, target_len, pad) }
123
+ pub fn string_last_index_of(s: &Value, search: &Value, position: &Value) -> Value {
124
+ string_last_index_of_impl(s, search, position)
125
+ }
122
126
 
123
127
  /// Number.prototype.toFixed(digits) - format number with fixed decimal places (0-20)
124
128
  pub fn number_to_fixed(n: &Value, digits: &Value) -> Value {
@@ -2,7 +2,7 @@
2
2
 
3
3
  mod vm;
4
4
 
5
- pub use vm::{run, Vm};
5
+ pub use vm::{all_compiled_capabilities, run, run_with_options, Vm, VmRunOptions};
6
6
 
7
7
  #[cfg(test)]
8
8
  mod tests {