@tishlang/tish 1.3.8 → 1.5.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 (34) 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/integration_test.rs +48 -0
  7. package/crates/tish_build_utils/src/lib.rs +171 -1
  8. package/crates/tish_builtins/src/string.rs +248 -0
  9. package/crates/tish_bytecode/Cargo.toml +1 -0
  10. package/crates/tish_bytecode/src/compiler.rs +289 -66
  11. package/crates/tish_bytecode/src/opcode.rs +13 -3
  12. package/crates/tish_bytecode/src/peephole.rs +21 -16
  13. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
  14. package/crates/tish_compile/src/codegen.rs +214 -79
  15. package/crates/tish_compile/src/lib.rs +1 -1
  16. package/crates/tish_core/src/value.rs +1 -0
  17. package/crates/tish_eval/src/eval.rs +39 -1
  18. package/crates/tish_eval/src/lib.rs +1 -1
  19. package/crates/tish_lint/Cargo.toml +1 -0
  20. package/crates/tish_lint/src/bin/tish-lint.rs +141 -23
  21. package/crates/tish_lint/src/lib.rs +3 -1
  22. package/crates/tish_lsp/README.md +1 -1
  23. package/crates/tish_native/src/build.rs +48 -7
  24. package/crates/tish_native/src/lib.rs +8 -3
  25. package/crates/tish_runtime/src/lib.rs +4 -0
  26. package/crates/tish_vm/src/lib.rs +1 -1
  27. package/crates/tish_vm/src/vm.rs +155 -16
  28. package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
  29. package/package.json +1 -8
  30. package/platform/darwin-arm64/tish +0 -0
  31. package/platform/darwin-x64/tish +0 -0
  32. package/platform/linux-arm64/tish +0 -0
  33. package/platform/linux-x64/tish +0 -0
  34. package/platform/win32-x64/tish.exe +0 -0
@@ -515,6 +515,10 @@ impl Evaluator {
515
515
  if from.starts_with("tish:") {
516
516
  return self.load_builtin_module(from);
517
517
  }
518
+ // Scoped native modules (e.g. `@tishlang/waterui`) registered via `TishNativeModule::virtual_builtin_modules`.
519
+ if self.virtual_builtins.borrow().get(from).is_some() {
520
+ return self.load_builtin_module(from);
521
+ }
518
522
  let dir = self.current_dir.borrow().clone().ok_or_else(|| {
519
523
  EvalError::Error("Cannot resolve imports: no current file directory (use run_file)".to_string())
520
524
  })?;
@@ -1203,6 +1207,9 @@ impl Evaluator {
1203
1207
  });
1204
1208
  return Ok(Value::Number(found.unwrap_or(-1.0)));
1205
1209
  }
1210
+ "lastIndexOf" => {
1211
+ return Ok(Self::string_last_index_of_eval(&arg_vals, s));
1212
+ }
1206
1213
  "includes" => {
1207
1214
  let search = match arg_vals.first() {
1208
1215
  Some(Value::String(ss)) => ss.as_ref(),
@@ -1474,7 +1481,13 @@ impl Evaluator {
1474
1481
  }
1475
1482
  }
1476
1483
 
1477
- // Fall through to normal function call
1484
+ // Fall through to normal function call. `get_prop` only implements `length` on
1485
+ // strings, so method calls would otherwise become `call_func(Null)` → Not a function.
1486
+ if let Value::String(s) = &obj {
1487
+ if method_name.as_ref() == "lastIndexOf" {
1488
+ return Ok(Self::string_last_index_of_eval(&arg_vals, s));
1489
+ }
1490
+ }
1478
1491
  let f = self.get_prop(&obj, method_name).map_err(EvalError::Error)?;
1479
1492
  return self.call_func(&f, &arg_vals);
1480
1493
  }
@@ -2638,6 +2651,31 @@ impl Evaluator {
2638
2651
  Self::bind_destruct_pattern_scoped(&self.scope, pattern, value, mutable)
2639
2652
  }
2640
2653
 
2654
+ /// `String.prototype.lastIndexOf` (interpreter). Kept as a helper so dispatch cannot fall
2655
+ /// through to [`Self::get_prop`] + [`Self::call_func`] for string receivers.
2656
+ fn string_last_index_of_eval(arg_vals: &[Value], receiver: &Arc<str>) -> Value {
2657
+ let search = match arg_vals.first() {
2658
+ Some(Value::String(ss)) => ss.as_ref(),
2659
+ _ => return Value::Number(-1.0),
2660
+ };
2661
+ let position_core: tishlang_core::Value = match arg_vals.get(1) {
2662
+ None => tishlang_core::Value::Number(f64::INFINITY),
2663
+ Some(Value::Null) => tishlang_core::Value::Null,
2664
+ Some(Value::Number(n)) => tishlang_core::Value::Number(*n),
2665
+ Some(Value::Bool(b)) => tishlang_core::Value::Bool(*b),
2666
+ Some(_) => tishlang_core::Value::Number(0.0),
2667
+ };
2668
+ let out = tishlang_builtins::string::last_index_of_str(
2669
+ receiver.as_ref(),
2670
+ search,
2671
+ &position_core,
2672
+ );
2673
+ match out {
2674
+ tishlang_core::Value::Number(n) => Value::Number(n),
2675
+ _ => Value::Number(-1.0),
2676
+ }
2677
+ }
2678
+
2641
2679
  fn get_prop(&self, obj: &Value, key: &str) -> Result<Value, String> {
2642
2680
  match obj {
2643
2681
  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")]
@@ -12,6 +12,7 @@ path = "src/bin/tish-lint.rs"
12
12
 
13
13
  [dependencies]
14
14
  clap = { version = "4.6.0", features = ["derive"] }
15
+ serde_json = "1.0"
15
16
  walkdir = "2"
16
17
  tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
17
18
  tishlang_parser = { path = "../tish_parser", version = ">=0.1" }
@@ -3,30 +3,54 @@
3
3
  use std::fs;
4
4
  use std::path::{Path, PathBuf};
5
5
 
6
- use clap::Parser;
6
+ use clap::{Parser, ValueEnum};
7
+ use serde_json::json;
8
+
9
+ #[derive(Clone, Copy, Debug, ValueEnum)]
10
+ enum OutputFormat {
11
+ Text,
12
+ Sarif,
13
+ }
7
14
 
8
15
  #[derive(Parser)]
9
16
  #[command(name = "tish-lint")]
10
17
  #[command(about = "AST-based linter for Tish")]
11
18
  struct Cli {
19
+ /// Output format (SARIF 2.1.0 for code scanning integrations).
20
+ #[arg(long = "format", value_enum, default_value_t = OutputFormat::Text)]
21
+ output_format: OutputFormat,
22
+
12
23
  #[arg(required = true)]
13
24
  paths: Vec<String>,
14
25
  }
15
26
 
27
+ #[derive(Debug)]
28
+ struct Issue {
29
+ path: PathBuf,
30
+ line: u32,
31
+ col: u32,
32
+ code: String,
33
+ message: String,
34
+ level: &'static str,
35
+ }
36
+
16
37
  fn main() {
17
38
  let cli = Cli::parse();
18
- if let Err(e) = run(&cli.paths) {
39
+ if let Err(e) = run(&cli.paths, cli.output_format) {
19
40
  eprintln!("{}", e);
20
41
  std::process::exit(1);
21
42
  }
22
43
  }
23
44
 
24
- fn run(paths: &[String]) -> Result<(), String> {
45
+ fn collect_files(paths: &[String]) -> Result<Vec<PathBuf>, String> {
25
46
  let mut files: Vec<PathBuf> = Vec::new();
26
47
  for p in paths {
27
48
  let path = Path::new(p);
28
49
  if path.is_dir() {
29
- for e in walkdir::WalkDir::new(path).into_iter().filter_map(|e| e.ok()) {
50
+ for e in walkdir::WalkDir::new(path)
51
+ .into_iter()
52
+ .filter_map(|e| e.ok())
53
+ {
30
54
  if e.path().extension().map(|x| x == "tish").unwrap_or(false) {
31
55
  files.push(e.path().to_path_buf());
32
56
  }
@@ -40,38 +64,132 @@ fn run(paths: &[String]) -> Result<(), String> {
40
64
  if files.is_empty() {
41
65
  return Err("No .tish files to lint".into());
42
66
  }
43
- let mut errors = 0;
67
+ Ok(files)
68
+ }
69
+
70
+ fn run(paths: &[String], format: OutputFormat) -> Result<(), String> {
71
+ let files = collect_files(paths)?;
72
+ let mut issues: Vec<Issue> = Vec::new();
44
73
  for f in files {
45
74
  let src = fs::read_to_string(&f).map_err(|e| format!("{}: {}", f.display(), e))?;
46
75
  match tishlang_lint::lint_source(&src) {
47
76
  Ok(diags) => {
48
77
  for d in diags {
49
- let sev = match d.severity {
50
- tishlang_lint::Severity::Error => {
51
- errors += 1;
52
- "error"
53
- }
78
+ let level = match d.severity {
79
+ tishlang_lint::Severity::Error => "error",
54
80
  tishlang_lint::Severity::Warning => "warning",
55
81
  };
56
- println!(
57
- "{}:{}:{}: {} [{}] {}",
58
- f.display(),
59
- d.line,
60
- d.col,
61
- sev,
62
- d.code,
63
- d.message
64
- );
82
+ issues.push(Issue {
83
+ path: f.clone(),
84
+ line: d.line,
85
+ col: d.col,
86
+ code: d.code.to_string(),
87
+ message: d.message,
88
+ level,
89
+ });
65
90
  }
66
91
  }
67
92
  Err(e) => {
68
- eprintln!("{}: parse error: {}", f.display(), e);
69
- errors += 1;
93
+ issues.push(Issue {
94
+ path: f.clone(),
95
+ line: 1,
96
+ col: 1,
97
+ code: "tish-parse-error".into(),
98
+ message: e,
99
+ level: "error",
100
+ });
70
101
  }
71
102
  }
72
103
  }
73
- if errors > 0 {
74
- return Err(format!("{} issue(s)", errors));
104
+
105
+ let error_count = issues.iter().filter(|i| i.level == "error").count();
106
+
107
+ match format {
108
+ OutputFormat::Text => {
109
+ for i in &issues {
110
+ println!(
111
+ "{}:{}:{}: {} [{}] {}",
112
+ i.path.display(),
113
+ i.line,
114
+ i.col,
115
+ i.level,
116
+ i.code,
117
+ i.message
118
+ );
119
+ }
120
+ if error_count > 0 {
121
+ return Err(format!("{} issue(s)", error_count));
122
+ }
123
+ }
124
+ OutputFormat::Sarif => {
125
+ print_sarif(&issues)?;
126
+ if error_count > 0 {
127
+ return Err(format!("{} issue(s)", error_count));
128
+ }
129
+ }
75
130
  }
131
+
132
+ Ok(())
133
+ }
134
+
135
+ fn print_sarif(issues: &[Issue]) -> Result<(), String> {
136
+ let rules: Vec<_> = tishlang_lint::RULES
137
+ .iter()
138
+ .map(|(id, desc)| {
139
+ json!({
140
+ "id": id,
141
+ "name": id,
142
+ "shortDescription": { "text": desc },
143
+ "helpUri": "https://tishlang.com/docs/reference/linting/"
144
+ })
145
+ })
146
+ .chain(std::iter::once(json!({
147
+ "id": "tish-parse-error",
148
+ "name": "tish-parse-error",
149
+ "shortDescription": { "text": "Source failed to parse as Tish." },
150
+ "helpUri": "https://tishlang.com/docs/language/overview/"
151
+ })))
152
+ .collect();
153
+
154
+ let results: Vec<_> = issues
155
+ .iter()
156
+ .map(|i| {
157
+ let uri = i.path.to_str().unwrap_or("unknown").replace('\\', "/");
158
+ json!({
159
+ "ruleId": i.code,
160
+ "level": i.level,
161
+ "message": { "text": i.message },
162
+ "locations": [{
163
+ "physicalLocation": {
164
+ "artifactLocation": { "uri": uri },
165
+ "region": {
166
+ "startLine": i.line,
167
+ "startColumn": i.col
168
+ }
169
+ }
170
+ }]
171
+ })
172
+ })
173
+ .collect();
174
+
175
+ let doc = json!({
176
+ "version": "2.1.0",
177
+ "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
178
+ "runs": [{
179
+ "tool": {
180
+ "driver": {
181
+ "name": "tish-lint",
182
+ "informationUri": "https://tishlang.com/docs/reference/linting/",
183
+ "rules": rules
184
+ }
185
+ },
186
+ "results": results
187
+ }]
188
+ });
189
+
190
+ println!(
191
+ "{}",
192
+ serde_json::to_string_pretty(&doc).map_err(|e| e.to_string())?
193
+ );
76
194
  Ok(())
77
195
  }
@@ -246,7 +246,9 @@ fn lint_expr(e: &Expr, out: &mut Vec<LintDiagnostic>) {
246
246
  }
247
247
  Expr::Await { operand, .. } => lint_expr(operand, out),
248
248
  Expr::TypeOf { operand, .. } => lint_expr(operand, out),
249
- Expr::JsxElement { props, children, .. } => {
249
+ Expr::JsxElement {
250
+ props, children, ..
251
+ } => {
250
252
  for pr in props {
251
253
  match pr {
252
254
  tishlang_ast::JsxProp::Attr { value, .. } => {
@@ -19,7 +19,7 @@ Binary: `target/release/tish-lsp` (stdio LSP).
19
19
 
20
20
  ## Client configuration
21
21
 
22
- See the [Tish docs — Language server](https://tishlang.github.io/tish-docs/reference/language-server/) and [Editor setup](https://tishlang.github.io/tish-docs/getting-started/editor/).
22
+ See the [Tish docs — Language server](https://tishlang.com/docs/reference/language-server/) and [Editor setup](https://tishlang.com/docs/getting-started/editor/).
23
23
 
24
24
  ## Developing
25
25
 
@@ -5,6 +5,29 @@ 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
+
8
31
  pub fn build_via_cargo(
9
32
  rust_code: &str,
10
33
  native_modules: Vec<ResolvedNativeModule>,
@@ -19,15 +42,12 @@ pub fn build_via_cargo(
19
42
 
20
43
  let runtime_path = tishlang_build_utils::find_runtime_path()?;
21
44
 
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() {
45
+ let runtime_features = runtime_features_for_cargo(features);
46
+ let runtime_refs: Vec<&str> = runtime_features.iter().map(String::as_str).collect();
47
+ let features_str = if runtime_refs.is_empty() {
28
48
  String::new()
29
49
  } else {
30
- format!(", features = {:?}", runtime_features)
50
+ format!(", features = {:?}", runtime_refs)
31
51
  };
32
52
 
33
53
  let needs_tokio = rust_code.contains("#[tokio::main]");
@@ -114,3 +134,24 @@ tishlang_runtime = {{ path = {:?}{} }}{}{}{}
114
134
  Ok(())
115
135
  }
116
136
 
137
+ #[cfg(test)]
138
+ mod tests {
139
+ use super::runtime_features_for_cargo;
140
+
141
+ #[test]
142
+ fn runtime_features_full_expands() {
143
+ let f = runtime_features_for_cargo(&["full".to_string()]);
144
+ assert!(f.contains(&"http".to_string()));
145
+ assert!(f.contains(&"fs".to_string()));
146
+ assert!(f.contains(&"process".to_string()));
147
+ assert!(f.contains(&"regex".to_string()));
148
+ assert!(f.contains(&"ws".to_string()));
149
+ }
150
+
151
+ #[test]
152
+ fn runtime_features_merges_full_and_specific() {
153
+ let f = runtime_features_for_cargo(&["full".to_string(), "http".to_string()]);
154
+ assert_eq!(f.len(), 5);
155
+ }
156
+ }
157
+
@@ -57,7 +57,7 @@ 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(
60
+ let (rust_code, native_modules, effective_features) = tishlang_compile::compile_project_full(
61
61
  entry_path,
62
62
  project_root,
63
63
  features,
@@ -67,8 +67,13 @@ pub fn compile_to_native(
67
67
  message: e.to_string(),
68
68
  })?;
69
69
 
70
- crate::build::build_via_cargo(&rust_code, native_modules, output_path, features)
71
- .map_err(|e| NativeError { message: e })
70
+ crate::build::build_via_cargo(
71
+ &rust_code,
72
+ native_modules,
73
+ output_path,
74
+ &effective_features,
75
+ )
76
+ .map_err(|e| NativeError { message: e })
72
77
  }
73
78
  Backend::Cranelift => {
74
79
  let modules = tishlang_compile::resolve_project(entry_path, project_root)
@@ -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 {