@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
@@ -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
- fn init_globals() -> ObjectMap {
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.run_chunk(chunk, &chunk.nested, &[])
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 feature not enabled. Rebuild with --features full (or fs, http, process, ws).",
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 and return the result.
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.run(chunk)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tishlang/tish",
3
- "version": "1.3.8",
3
+ "version": "1.5.0",
4
4
  "description": "Tish - minimal TS/JS-compatible language. Run, REPL, build to native or other targets.",
5
5
  "license": "PIF",
6
6
  "repository": {
@@ -29,13 +29,6 @@
29
29
  "engines": {
30
30
  "node": ">=22"
31
31
  },
32
- "devDependencies": {
33
- "@semantic-release/commit-analyzer": "^13.0.1",
34
- "@semantic-release/github": "^12.0.6",
35
- "@semantic-release/npm": "^13.1.5",
36
- "@semantic-release/release-notes-generator": "^14.1.0",
37
- "semantic-release": "^25.0.3"
38
- },
39
32
  "keywords": [
40
33
  "tish",
41
34
  "language",
Binary file
Binary file
Binary file
Binary file
Binary file