@tishlang/tish 1.4.2 → 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.
- package/bin/tish +0 -0
- package/crates/tish/Cargo.toml +2 -2
- package/crates/tish/src/cli_help.rs +504 -0
- package/crates/tish/src/main.rs +76 -90
- package/crates/tish/src/repl_completion.rs +1 -1
- package/crates/tish/tests/integration_test.rs +48 -0
- package/crates/tish_build_utils/src/lib.rs +171 -1
- package/crates/tish_builtins/src/string.rs +248 -0
- package/crates/tish_bytecode/Cargo.toml +1 -0
- package/crates/tish_bytecode/src/compiler.rs +289 -66
- package/crates/tish_bytecode/src/opcode.rs +13 -3
- package/crates/tish_bytecode/src/peephole.rs +21 -16
- package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
- package/crates/tish_compile/src/codegen.rs +214 -79
- package/crates/tish_compile/src/lib.rs +1 -1
- package/crates/tish_core/src/value.rs +1 -0
- package/crates/tish_eval/src/eval.rs +39 -1
- package/crates/tish_eval/src/lib.rs +1 -1
- package/crates/tish_native/src/build.rs +48 -7
- package/crates/tish_native/src/lib.rs +8 -3
- package/crates/tish_runtime/src/lib.rs +4 -0
- package/crates/tish_vm/src/lib.rs +1 -1
- package/crates/tish_vm/src/vm.rs +155 -16
- package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- 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)),
|
|
@@ -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
|
|
23
|
-
|
|
24
|
-
|
|
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 = {:?}",
|
|
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(
|
|
71
|
-
|
|
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 {
|
package/crates/tish_vm/src/vm.rs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
//! Stack-based bytecode VM.
|
|
2
2
|
|
|
3
3
|
use std::cell::RefCell;
|
|
4
|
+
use std::collections::HashSet;
|
|
4
5
|
use std::rc::Rc;
|
|
5
6
|
use std::sync::Arc;
|
|
6
7
|
|
|
@@ -15,15 +16,40 @@ use tishlang_core::{ObjectMap, Value};
|
|
|
15
16
|
|
|
16
17
|
type ArrayMethodFn = Rc<dyn Fn(&[Value]) -> Value>;
|
|
17
18
|
|
|
19
|
+
/// Feature names enabled for this VM run (`tish run --feature …`). `full` enables every optional capability.
|
|
20
|
+
#[cfg_attr(
|
|
21
|
+
not(any(feature = "fs", feature = "http", feature = "process", feature = "ws")),
|
|
22
|
+
allow(dead_code)
|
|
23
|
+
)]
|
|
24
|
+
fn cap_allows(enabled: &HashSet<String>, name: &str) -> bool {
|
|
25
|
+
enabled.contains("full") || enabled.contains(name)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Capabilities linked into this `tishlang_vm` binary (compile-time). Used by [`Vm::new`] and `run()`.
|
|
29
|
+
pub fn all_compiled_capabilities() -> HashSet<String> {
|
|
30
|
+
#[allow(unused_mut)]
|
|
31
|
+
let mut s = HashSet::new();
|
|
32
|
+
#[cfg(feature = "http")]
|
|
33
|
+
s.insert("http".to_string());
|
|
34
|
+
#[cfg(feature = "fs")]
|
|
35
|
+
s.insert("fs".to_string());
|
|
36
|
+
#[cfg(feature = "process")]
|
|
37
|
+
s.insert("process".to_string());
|
|
38
|
+
#[cfg(feature = "regex")]
|
|
39
|
+
s.insert("regex".to_string());
|
|
40
|
+
#[cfg(feature = "ws")]
|
|
41
|
+
s.insert("ws".to_string());
|
|
42
|
+
s
|
|
43
|
+
}
|
|
44
|
+
|
|
18
45
|
/// Look up built-in module export for LoadNativeExport. Returns None if unknown or feature disabled.
|
|
19
|
-
/// Parameters are only used when the corresponding feature (fs, http, process) is enabled.
|
|
20
46
|
#[cfg_attr(
|
|
21
47
|
not(any(feature = "fs", feature = "http", feature = "process", feature = "ws")),
|
|
22
48
|
allow(unused_variables)
|
|
23
49
|
)]
|
|
24
|
-
fn get_builtin_export(spec: &str, export_name: &str) -> Option<Value> {
|
|
50
|
+
fn get_builtin_export(enabled: &HashSet<String>, spec: &str, export_name: &str) -> Option<Value> {
|
|
25
51
|
#[cfg(feature = "fs")]
|
|
26
|
-
if spec == "tish:fs" {
|
|
52
|
+
if spec == "tish:fs" && cap_allows(enabled, "fs") {
|
|
27
53
|
return match export_name {
|
|
28
54
|
"readFile" => Some(Value::Function(Rc::new(|args: &[Value]| tishlang_runtime::read_file(args)))),
|
|
29
55
|
"writeFile" => Some(Value::Function(Rc::new(|args: &[Value]| tishlang_runtime::write_file(args)))),
|
|
@@ -35,7 +61,7 @@ fn get_builtin_export(spec: &str, export_name: &str) -> Option<Value> {
|
|
|
35
61
|
};
|
|
36
62
|
}
|
|
37
63
|
#[cfg(feature = "http")]
|
|
38
|
-
if spec == "tish:http" {
|
|
64
|
+
if spec == "tish:http" && cap_allows(enabled, "http") {
|
|
39
65
|
return match export_name {
|
|
40
66
|
// Bytecode compiler lowers `await expr` to `tish:http.await(promise)` (see tish_bytecode compiler).
|
|
41
67
|
"await" => Some(Value::Function(Rc::new(|args: &[Value]| {
|
|
@@ -59,7 +85,7 @@ fn get_builtin_export(spec: &str, export_name: &str) -> Option<Value> {
|
|
|
59
85
|
};
|
|
60
86
|
}
|
|
61
87
|
#[cfg(feature = "process")]
|
|
62
|
-
if spec == "tish:process" {
|
|
88
|
+
if spec == "tish:process" && cap_allows(enabled, "process") {
|
|
63
89
|
return match export_name {
|
|
64
90
|
"exit" => Some(Value::Function(Rc::new(|args: &[Value]| tishlang_runtime::process_exit(args)))),
|
|
65
91
|
"cwd" => Some(Value::Function(Rc::new(|args: &[Value]| tishlang_runtime::process_cwd(args)))),
|
|
@@ -97,7 +123,7 @@ fn get_builtin_export(spec: &str, export_name: &str) -> Option<Value> {
|
|
|
97
123
|
};
|
|
98
124
|
}
|
|
99
125
|
#[cfg(feature = "ws")]
|
|
100
|
-
if spec == "tish:ws" {
|
|
126
|
+
if spec == "tish:ws" && cap_allows(enabled, "ws") {
|
|
101
127
|
return match export_name {
|
|
102
128
|
"WebSocket" => Some(Value::Function(Rc::new(|args: &[Value]| {
|
|
103
129
|
tishlang_runtime::web_socket_client(args)
|
|
@@ -149,7 +175,8 @@ fn vm_log_err(s: &str) {
|
|
|
149
175
|
}
|
|
150
176
|
|
|
151
177
|
/// Initialize default globals (console, Math, JSON, etc.)
|
|
152
|
-
|
|
178
|
+
#[allow(unused_variables)]
|
|
179
|
+
fn init_globals(enabled: &HashSet<String>) -> ObjectMap {
|
|
153
180
|
let mut g = ObjectMap::default();
|
|
154
181
|
|
|
155
182
|
let mut console = ObjectMap::default();
|
|
@@ -381,7 +408,7 @@ fn init_globals() -> ObjectMap {
|
|
|
381
408
|
g.insert("document".into(), Value::Object(Rc::new(RefCell::new(document_obj))));
|
|
382
409
|
|
|
383
410
|
#[cfg(feature = "process")]
|
|
384
|
-
{
|
|
411
|
+
if cap_allows(enabled, "process") {
|
|
385
412
|
let mut process_obj = ObjectMap::default();
|
|
386
413
|
process_obj.insert(
|
|
387
414
|
"exit".into(),
|
|
@@ -413,7 +440,7 @@ fn init_globals() -> ObjectMap {
|
|
|
413
440
|
}
|
|
414
441
|
|
|
415
442
|
#[cfg(feature = "http")]
|
|
416
|
-
{
|
|
443
|
+
if cap_allows(enabled, "http") {
|
|
417
444
|
g.insert(
|
|
418
445
|
"serve".into(),
|
|
419
446
|
Value::Function(Rc::new(|args: &[Value]| {
|
|
@@ -433,21 +460,44 @@ fn init_globals() -> ObjectMap {
|
|
|
433
460
|
/// Shared scope for closure capture (parent frame's locals).
|
|
434
461
|
type ScopeMap = Rc<RefCell<ObjectMap>>;
|
|
435
462
|
|
|
463
|
+
/// Options for the convenience [`run_with_options`] helper (one-shot VM run from the CLI).
|
|
464
|
+
#[derive(Clone, Debug, Default)]
|
|
465
|
+
pub struct VmRunOptions {
|
|
466
|
+
/// When true and not inside a nested chunk (`enclosing` is `None`), top-level [`Opcode::DeclareVar`]
|
|
467
|
+
/// also writes to globals so the REPL keeps bindings across input lines.
|
|
468
|
+
pub repl_mode: bool,
|
|
469
|
+
/// Enabled capabilities for this run (e.g. `fs`, `http`, `full`). Empty = none (secure default).
|
|
470
|
+
pub capabilities: HashSet<String>,
|
|
471
|
+
}
|
|
472
|
+
|
|
436
473
|
pub struct Vm {
|
|
437
474
|
stack: Vec<Value>,
|
|
438
475
|
scope: ObjectMap,
|
|
439
476
|
/// Enclosing scope for closures (captured parent frame locals).
|
|
440
477
|
enclosing: Option<ScopeMap>,
|
|
441
478
|
globals: Rc<RefCell<ObjectMap>>,
|
|
479
|
+
/// Capabilities for `LoadNativeExport` and globals such as `process` / `serve`.
|
|
480
|
+
capabilities: Arc<HashSet<String>>,
|
|
442
481
|
}
|
|
443
482
|
|
|
444
483
|
impl Vm {
|
|
484
|
+
/// VM with every capability that exists in this `tishlang_vm` build (embedders, tests, `run()`).
|
|
445
485
|
pub fn new() -> Self {
|
|
486
|
+
Self::with_capabilities_arc(Arc::new(all_compiled_capabilities()))
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/// VM with an explicit capability set (e.g. from `tish run --feature …`).
|
|
490
|
+
pub fn with_capabilities(capabilities: HashSet<String>) -> Self {
|
|
491
|
+
Self::with_capabilities_arc(Arc::new(capabilities))
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
fn with_capabilities_arc(capabilities: Arc<HashSet<String>>) -> Self {
|
|
446
495
|
Self {
|
|
447
496
|
stack: Vec::new(),
|
|
448
497
|
scope: ObjectMap::default(),
|
|
449
498
|
enclosing: None,
|
|
450
|
-
globals: Rc::new(RefCell::new(init_globals())),
|
|
499
|
+
globals: Rc::new(RefCell::new(init_globals(capabilities.as_ref()))),
|
|
500
|
+
capabilities,
|
|
451
501
|
}
|
|
452
502
|
}
|
|
453
503
|
|
|
@@ -476,7 +526,12 @@ impl Vm {
|
|
|
476
526
|
}
|
|
477
527
|
|
|
478
528
|
pub fn run(&mut self, chunk: &Chunk) -> Result<Value, String> {
|
|
479
|
-
self.
|
|
529
|
+
self.run_with_options(chunk, false)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/// Run a chunk using this VM's capability set. `repl_mode` persists top-level `let` across REPL lines.
|
|
533
|
+
pub fn run_with_options(&mut self, chunk: &Chunk, repl_mode: bool) -> Result<Value, String> {
|
|
534
|
+
self.run_chunk(chunk, &chunk.nested, &[], repl_mode)
|
|
480
535
|
}
|
|
481
536
|
|
|
482
537
|
fn run_chunk(
|
|
@@ -484,6 +539,7 @@ impl Vm {
|
|
|
484
539
|
chunk: &Chunk,
|
|
485
540
|
nested: &[Chunk],
|
|
486
541
|
args: &[Value],
|
|
542
|
+
repl_mode: bool,
|
|
487
543
|
) -> Result<Value, String> {
|
|
488
544
|
let code = &chunk.code;
|
|
489
545
|
let constants = &chunk.constants;
|
|
@@ -517,6 +573,7 @@ impl Vm {
|
|
|
517
573
|
}
|
|
518
574
|
}
|
|
519
575
|
let mut try_handlers: Vec<(usize, usize)> = vec![];
|
|
576
|
+
let mut block_undo_stack: Vec<Vec<(Arc<str>, Option<Value>)>> = vec![];
|
|
520
577
|
|
|
521
578
|
loop {
|
|
522
579
|
if ip >= code.len() {
|
|
@@ -548,14 +605,16 @@ impl Vm {
|
|
|
548
605
|
let inner_clone = inner.clone();
|
|
549
606
|
let globals = Rc::clone(&self.globals);
|
|
550
607
|
let enclosing = Some(Rc::clone(&local_scope));
|
|
608
|
+
let capabilities = Arc::clone(&self.capabilities);
|
|
551
609
|
Value::Function(Rc::new(move |args: &[Value]| {
|
|
552
610
|
let mut vm = Vm {
|
|
553
611
|
stack: Vec::new(),
|
|
554
612
|
scope: ObjectMap::default(),
|
|
555
613
|
enclosing: enclosing.clone(),
|
|
556
614
|
globals: Rc::clone(&globals),
|
|
615
|
+
capabilities: Arc::clone(&capabilities),
|
|
557
616
|
};
|
|
558
|
-
vm.run_chunk(&inner_clone, &inner_clone.nested, args)
|
|
617
|
+
vm.run_chunk(&inner_clone, &inner_clone.nested, args, false)
|
|
559
618
|
.unwrap_or(Value::Null)
|
|
560
619
|
}))
|
|
561
620
|
}
|
|
@@ -616,6 +675,62 @@ impl Vm {
|
|
|
616
675
|
}
|
|
617
676
|
}
|
|
618
677
|
}
|
|
678
|
+
Opcode::DeclareVar => {
|
|
679
|
+
let idx = Self::read_u16(code, &mut ip);
|
|
680
|
+
let name = names
|
|
681
|
+
.get(idx as usize)
|
|
682
|
+
.ok_or_else(|| format!("Name index out of bounds: {}", idx))?;
|
|
683
|
+
let v = self
|
|
684
|
+
.stack
|
|
685
|
+
.pop()
|
|
686
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
687
|
+
if let Some(frame) = block_undo_stack.last_mut() {
|
|
688
|
+
let old = local_scope.borrow().get(name.as_ref()).cloned();
|
|
689
|
+
frame.push((Arc::clone(name), old));
|
|
690
|
+
}
|
|
691
|
+
// REPL: persist top-level bindings only (not block-locals shadowing globals).
|
|
692
|
+
if repl_mode && self.enclosing.is_none() && block_undo_stack.is_empty() {
|
|
693
|
+
self.globals
|
|
694
|
+
.borrow_mut()
|
|
695
|
+
.insert(Arc::clone(name), v.clone());
|
|
696
|
+
}
|
|
697
|
+
local_scope.borrow_mut().insert(Arc::clone(name), v);
|
|
698
|
+
}
|
|
699
|
+
Opcode::DeclareVarPlain => {
|
|
700
|
+
let idx = Self::read_u16(code, &mut ip);
|
|
701
|
+
let name = names
|
|
702
|
+
.get(idx as usize)
|
|
703
|
+
.ok_or_else(|| format!("Name index out of bounds: {}", idx))?;
|
|
704
|
+
let v = self
|
|
705
|
+
.stack
|
|
706
|
+
.pop()
|
|
707
|
+
.ok_or_else(|| "Stack underflow".to_string())?;
|
|
708
|
+
if repl_mode && self.enclosing.is_none() && block_undo_stack.is_empty() {
|
|
709
|
+
self.globals
|
|
710
|
+
.borrow_mut()
|
|
711
|
+
.insert(Arc::clone(name), v.clone());
|
|
712
|
+
}
|
|
713
|
+
local_scope.borrow_mut().insert(Arc::clone(name), v);
|
|
714
|
+
}
|
|
715
|
+
Opcode::EnterBlock => {
|
|
716
|
+
block_undo_stack.push(Vec::new());
|
|
717
|
+
}
|
|
718
|
+
Opcode::ExitBlock => {
|
|
719
|
+
let frame = block_undo_stack.pop().ok_or_else(|| {
|
|
720
|
+
"ExitBlock without matching EnterBlock".to_string()
|
|
721
|
+
})?;
|
|
722
|
+
for (name, old) in frame.into_iter().rev() {
|
|
723
|
+
let mut ls = local_scope.borrow_mut();
|
|
724
|
+
match old {
|
|
725
|
+
Some(prev) => {
|
|
726
|
+
ls.insert(name, prev);
|
|
727
|
+
}
|
|
728
|
+
None => {
|
|
729
|
+
ls.remove(name.as_ref());
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
619
734
|
Opcode::LoadGlobal => {
|
|
620
735
|
let idx = Self::read_u16(code, &mut ip);
|
|
621
736
|
let name = names
|
|
@@ -1144,9 +1259,9 @@ impl Vm {
|
|
|
1144
1259
|
return Err("LoadNativeExport: export_name constant out of bounds or not string".to_string());
|
|
1145
1260
|
}
|
|
1146
1261
|
};
|
|
1147
|
-
let v = get_builtin_export(spec, export_name).ok_or_else(|| {
|
|
1262
|
+
let v = get_builtin_export(self.capabilities.as_ref(), spec, export_name).ok_or_else(|| {
|
|
1148
1263
|
format!(
|
|
1149
|
-
"Built-in module '{}' does not export '{}' or
|
|
1264
|
+
"Built-in module '{}' does not export '{}' or capability not enabled for this run. Use e.g. tish run --feature fs (or full). The tish binary must also be built with that capability linked in.",
|
|
1150
1265
|
spec, export_name
|
|
1151
1266
|
)
|
|
1152
1267
|
})?;
|
|
@@ -1379,6 +1494,12 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
1379
1494
|
}
|
|
1380
1495
|
Value::String(s) => {
|
|
1381
1496
|
let key_s = key.as_ref();
|
|
1497
|
+
if let Ok(idx) = key_s.parse::<usize>() {
|
|
1498
|
+
return match s.chars().nth(idx) {
|
|
1499
|
+
Some(c) => Ok(Value::String(Arc::from(c.to_string()))),
|
|
1500
|
+
None => Err("Index out of bounds".to_string()),
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1382
1503
|
if key_s == "length" {
|
|
1383
1504
|
return Ok(Value::Number(s.chars().count() as f64));
|
|
1384
1505
|
}
|
|
@@ -1389,6 +1510,18 @@ fn get_member(obj: &Value, key: &Arc<str>) -> Result<Value, String> {
|
|
|
1389
1510
|
let from = args.get(1);
|
|
1390
1511
|
str_builtins::index_of(&Value::String(Arc::clone(&s_clone)), search, from)
|
|
1391
1512
|
}),
|
|
1513
|
+
"lastIndexOf" => Rc::new(move |args: &[Value]| {
|
|
1514
|
+
let search = args.first().unwrap_or(&Value::Null);
|
|
1515
|
+
let position = args
|
|
1516
|
+
.get(1)
|
|
1517
|
+
.cloned()
|
|
1518
|
+
.unwrap_or(Value::Number(f64::INFINITY));
|
|
1519
|
+
str_builtins::last_index_of(
|
|
1520
|
+
&Value::String(Arc::clone(&s_clone)),
|
|
1521
|
+
search,
|
|
1522
|
+
&position,
|
|
1523
|
+
)
|
|
1524
|
+
}),
|
|
1392
1525
|
"includes" => Rc::new(move |args: &[Value]| {
|
|
1393
1526
|
let search = args.first().unwrap_or(&Value::Null);
|
|
1394
1527
|
let from = args.get(1);
|
|
@@ -1490,8 +1623,14 @@ fn set_index(obj: &Value, idx: &Value, val: Value) -> Result<(), String> {
|
|
|
1490
1623
|
set_member(obj, &key, val)
|
|
1491
1624
|
}
|
|
1492
1625
|
|
|
1493
|
-
/// Run a chunk
|
|
1626
|
+
/// Run a chunk with every capability linked into this `tishlang_vm` build (tests, embedders).
|
|
1494
1627
|
pub fn run(chunk: &Chunk) -> Result<Value, String> {
|
|
1495
1628
|
let mut vm = Vm::new();
|
|
1496
|
-
vm.
|
|
1629
|
+
vm.run_with_options(chunk, false)
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/// Run a chunk with options (e.g. REPL persistence for top-level declarations).
|
|
1633
|
+
pub fn run_with_options(chunk: &Chunk, opts: VmRunOptions) -> Result<Value, String> {
|
|
1634
|
+
let mut vm = Vm::with_capabilities(opts.capabilities);
|
|
1635
|
+
vm.run_with_options(chunk, opts.repl_mode)
|
|
1497
1636
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//! `DeclareVar` + block scopes: function `let` shadows script-level names (bytecode VM).
|
|
2
|
+
|
|
3
|
+
use tishlang_bytecode::compile;
|
|
4
|
+
use tishlang_vm::run;
|
|
5
|
+
|
|
6
|
+
#[test]
|
|
7
|
+
fn declare_var_shadows_script_let_inside_fn() {
|
|
8
|
+
let src = r#"
|
|
9
|
+
let x = 1
|
|
10
|
+
fn f() {
|
|
11
|
+
let x = 2
|
|
12
|
+
return x
|
|
13
|
+
}
|
|
14
|
+
let r = f()
|
|
15
|
+
console.log("script", x, "fn", r)
|
|
16
|
+
"#;
|
|
17
|
+
let program = tishlang_parser::parse(src).expect("parse");
|
|
18
|
+
let chunk = compile(&program).expect("compile");
|
|
19
|
+
run(&chunk).expect("run");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#[test]
|
|
23
|
+
fn block_let_restores_outer_binding() {
|
|
24
|
+
let src = r#"
|
|
25
|
+
let x = 1
|
|
26
|
+
{
|
|
27
|
+
let x = 2
|
|
28
|
+
}
|
|
29
|
+
console.log(x)
|
|
30
|
+
"#;
|
|
31
|
+
let program = tishlang_parser::parse(src).expect("parse");
|
|
32
|
+
let chunk = compile(&program).expect("compile");
|
|
33
|
+
run(&chunk).expect("run");
|
|
34
|
+
}
|
package/package.json
CHANGED
|
Binary file
|
package/platform/darwin-x64/tish
CHANGED
|
Binary file
|
|
Binary file
|
package/platform/linux-x64/tish
CHANGED
|
Binary file
|
|
Binary file
|