@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,8 @@
1
1
  //! Peephole optimizations on bytecode (post-emission).
2
2
  //! B2 from optimization plan: jump chaining, etc.
3
3
 
4
+ use std::collections::BTreeSet;
5
+
4
6
  use crate::opcode::Opcode;
5
7
  use crate::Chunk;
6
8
 
@@ -90,6 +92,18 @@ fn final_jump_target(code: &[u8], jump_ip: usize) -> Option<usize> {
90
92
  skip_unconditional_jump_chain(code, first_target)
91
93
  }
92
94
 
95
+ /// Instruction boundaries from a linear scan (aligned bytecode from the compiler).
96
+ fn collect_insn_starts(code: &[u8]) -> BTreeSet<usize> {
97
+ let mut out = BTreeSet::new();
98
+ let mut ip = 0usize;
99
+ while ip < code.len() {
100
+ out.insert(ip);
101
+ let sz = instruction_size(code, ip).unwrap_or(1);
102
+ ip += sz;
103
+ }
104
+ out
105
+ }
106
+
93
107
  /// Replace instruction at [ip..ip+len) with Nops (preserves length, no offset updates).
94
108
  fn nop_out(code: &mut [u8], ip: usize, len: usize) {
95
109
  for i in 0..len {
@@ -112,19 +126,6 @@ fn remove_dup_pop(code: &mut [u8]) {
112
126
  }
113
127
  }
114
128
 
115
- /// Remove redundant LoadConst + Pop (load constant then discard = no-op).
116
- fn remove_loadconst_pop(code: &mut [u8]) {
117
- let mut ip = 0;
118
- while ip + 4 <= code.len() {
119
- if Opcode::from_u8(code[ip]) == Some(Opcode::LoadConst)
120
- && Opcode::from_u8(code[ip + 3]) == Some(Opcode::Pop)
121
- {
122
- nop_out(code, ip, 4);
123
- }
124
- ip += instruction_size(code, ip).unwrap_or(1);
125
- }
126
- }
127
-
128
129
  /// Replace no-op jumps (Jump with offset 0) with Nops.
129
130
  fn remove_noop_jumps(code: &mut [u8]) {
130
131
  let mut ip = 0;
@@ -142,6 +143,7 @@ fn remove_noop_jumps(code: &mut [u8]) {
142
143
  /// Apply jump chaining: if Jump/JumpIfFalse targets another jump, update to
143
144
  /// jump directly to the final target.
144
145
  fn chain_jumps(code: &mut [u8]) {
146
+ let insn_starts = collect_insn_starts(code);
145
147
  let mut ip = 0;
146
148
  while ip < code.len() {
147
149
  let op = match Opcode::from_u8(code[ip]) {
@@ -160,9 +162,13 @@ fn chain_jumps(code: &mut [u8]) {
160
162
  let current_offset = read_i16(code, ip + 1) as isize;
161
163
  let current_target = (ip as isize + 3 + current_offset).max(0) as usize;
162
164
  if let Some(final_target) = final_jump_target(code, ip) {
163
- if final_target != current_target {
165
+ let target_ok = final_target == code.len()
166
+ || insn_starts.contains(&final_target);
167
+ if final_target != current_target && target_ok {
164
168
  let new_offset = final_target as i32 - (ip + 3) as i32;
165
- write_u16(code, ip + 1, (new_offset as i16) as u16);
169
+ if (i16::MIN as i32..=i16::MAX as i32).contains(&new_offset) {
170
+ write_u16(code, ip + 1, (new_offset as i16) as u16);
171
+ }
166
172
  }
167
173
  }
168
174
  }
@@ -174,7 +180,6 @@ fn chain_jumps(code: &mut [u8]) {
174
180
 
175
181
  /// Run peephole optimizations on a chunk (and nested chunks).
176
182
  pub fn optimize(chunk: &mut Chunk) {
177
- remove_loadconst_pop(&mut chunk.code);
178
183
  remove_dup_pop(&mut chunk.code);
179
184
  remove_noop_jumps(&mut chunk.code);
180
185
  chain_jumps(&mut chunk.code);
@@ -0,0 +1,44 @@
1
+ //! Regression: C-style `for` `continue` must jump forward to the update clause (not JumpBack).
2
+
3
+ use std::fs;
4
+
5
+ use tishlang_bytecode::{compile, compile_unoptimized};
6
+ use tishlang_vm::run;
7
+
8
+ #[test]
9
+ fn break_continue_fixture_runs_on_vm() {
10
+ let path = format!(
11
+ "{}/../../tests/core/break_continue.tish",
12
+ env!("CARGO_MANIFEST_DIR")
13
+ );
14
+ let src = fs::read_to_string(&path).unwrap();
15
+ let prog = tishlang_parser::parse(&src).expect("parse");
16
+ let chunk = compile_unoptimized(&prog).expect("compile");
17
+ run(&chunk).expect("VM run");
18
+ }
19
+
20
+ #[test]
21
+ fn mutation_vm_ast_opt_without_peephole() {
22
+ let path = format!(
23
+ "{}/../../tests/core/mutation.tish",
24
+ env!("CARGO_MANIFEST_DIR")
25
+ );
26
+ let src = fs::read_to_string(&path).unwrap();
27
+ let mut prog = tishlang_parser::parse(&src).expect("parse");
28
+ tishlang_opt::optimize(&mut prog);
29
+ let chunk = compile_unoptimized(&prog).expect("compile");
30
+ run(&chunk).expect("VM");
31
+ }
32
+
33
+ #[test]
34
+ fn mutation_vm_ast_opt_with_peephole() {
35
+ let path = format!(
36
+ "{}/../../tests/core/mutation.tish",
37
+ env!("CARGO_MANIFEST_DIR")
38
+ );
39
+ let src = fs::read_to_string(&path).unwrap();
40
+ let mut prog = tishlang_parser::parse(&src).expect("parse");
41
+ tishlang_opt::optimize(&mut prog);
42
+ let chunk = compile(&prog).expect("compile");
43
+ run(&chunk).expect("VM");
44
+ }
@@ -393,17 +393,19 @@ pub fn compile_project(
393
393
  project_root: Option<&Path>,
394
394
  features: &[String],
395
395
  ) -> Result<String, CompileError> {
396
- let (rust, _) = compile_project_full(entry_path, project_root, features, true)?;
396
+ let (rust, _, _) = compile_project_full(entry_path, project_root, features, true)?;
397
397
  Ok(rust)
398
398
  }
399
399
 
400
- /// Compile a project and return Rust code plus resolved native modules for Cargo.toml generation.
400
+ /// Compile a project and return Rust code, resolved native modules, and the **effective** feature list
401
+ /// (CLI features plus any inferred from `tish:fs` / `tish:http` / … imports). Pass this list to
402
+ /// `tishlang_runtime` when linking (e.g. `build_via_cargo`) so Cargo `features` match codegen.
401
403
  pub fn compile_project_full(
402
404
  entry_path: &Path,
403
405
  project_root: Option<&Path>,
404
406
  features: &[String],
405
407
  optimize: bool,
406
- ) -> Result<(String, Vec<crate::resolve::ResolvedNativeModule>), CompileError> {
408
+ ) -> Result<(String, Vec<crate::resolve::ResolvedNativeModule>, Vec<String>), CompileError> {
407
409
  use crate::resolve;
408
410
  let root = project_root.unwrap_or_else(|| entry_path.parent().unwrap_or(Path::new(".")));
409
411
  let modules = resolve::resolve_project(entry_path, project_root)
@@ -421,7 +423,7 @@ pub fn compile_project_full(
421
423
  }
422
424
  }
423
425
  let rust = compile_with_native_modules(&merged, project_root, &all_features, &native_modules, optimize)?;
424
- Ok((rust, native_modules))
426
+ Ok((rust, native_modules, all_features))
425
427
  }
426
428
 
427
429
  /// Compile with explicit feature flags. When features are provided, codegen uses them
@@ -480,6 +482,9 @@ struct Codegen {
480
482
  /// Variables currently wrapped in Rc<RefCell<Value>> for mutable capture in closures
481
483
  /// These need special handling: reads via .borrow().clone(), writes via *var.borrow_mut()
482
484
  refcell_wrapped_vars: std::collections::HashSet<String>,
485
+ /// Scopes of names whose Rust binding is actually `Rc<RefCell<_>>` (emitted at VarDecl).
486
+ /// `refcell_wrapped_vars` alone is insufficient: it is set by prepasses before decl may run.
487
+ rc_cell_storage_scopes: Vec<std::collections::HashSet<String>>,
483
488
  /// Usage analyzer for move/clone optimization
484
489
  usage_analyzer: Option<UsageAnalyzer>,
485
490
  /// Type context for tracking variable types (for static typing)
@@ -509,12 +514,26 @@ impl Codegen {
509
514
  outer_params_stack: Vec::new(),
510
515
  outer_vars_stack: vec![Vec::new()], // Start with module-level scope
511
516
  refcell_wrapped_vars: std::collections::HashSet::new(),
517
+ rc_cell_storage_scopes: vec![std::collections::HashSet::new()],
512
518
  usage_analyzer: None,
513
519
  type_context: TypeContext::new(),
514
520
  program_has_jsx: false,
515
521
  }
516
522
  }
517
523
 
524
+ fn rc_cell_storage_contains(&self, name: &str) -> bool {
525
+ self.rc_cell_storage_scopes
526
+ .iter()
527
+ .rev()
528
+ .any(|s| s.contains(name))
529
+ }
530
+
531
+ fn rc_cell_storage_define(&mut self, name: &str) {
532
+ if let Some(scope) = self.rc_cell_storage_scopes.last_mut() {
533
+ scope.insert(name.to_string());
534
+ }
535
+ }
536
+
518
537
  /// Map native module spec to Rust init expression using resolved package.json modules.
519
538
  /// For built-in modules (tish:fs, tish:http, tish:process), use builtin_native_module_rust_init.
520
539
  fn native_module_rust_init(&self, spec: &str, export_name: &str) -> Option<String> {
@@ -576,28 +595,8 @@ impl Codegen {
576
595
  }
577
596
 
578
597
  fn has_feature(&self, name: &str) -> bool {
579
- if self.features.is_empty() {
580
- #[cfg(feature = "process")]
581
- if name == "process" {
582
- return true;
583
- }
584
- #[cfg(feature = "http")]
585
- if name == "http" {
586
- return true;
587
- }
588
- #[cfg(feature = "fs")]
589
- if name == "fs" {
590
- return true;
591
- }
592
- #[cfg(feature = "regex")]
593
- if name == "regex" {
594
- return true;
595
- }
596
- #[cfg(feature = "ws")]
597
- if name == "ws" {
598
- return true;
599
- }
600
- false
598
+ if self.features.contains("full") {
599
+ matches!(name, "http" | "fs" | "process" | "regex" | "ws")
601
600
  } else {
602
601
  self.features.contains(name)
603
602
  }
@@ -711,20 +710,27 @@ impl Codegen {
711
710
  let is_wrapped = self.refcell_wrapped_vars.contains(name);
712
711
  let var_type = self.type_context.get_type(name);
713
712
 
714
- // Native fast path: f64 variable avoid boxing/unboxing.
715
- if !is_wrapped && var_type == RustType::F64 {
713
+ // Native f64 (plain or Rc<RefCell<f64>> for closure-mutated locals)
714
+ if var_type == RustType::F64 {
716
715
  let op_assign = if delta.contains('+') { "+=" } else { "-=" };
716
+ if !is_wrapped {
717
+ return if is_prefix {
718
+ format!("{{ {n} {op_assign} 1.0_f64; Value::Number({n}) }}")
719
+ } else {
720
+ format!("{{ let _prev = {n}; {n} {op_assign} 1.0_f64; Value::Number(_prev) }}")
721
+ };
722
+ }
717
723
  return if is_prefix {
718
- format!("{{ {n} {op_assign} 1.0_f64; Value::Number({n}) }}")
724
+ format!("{{ *{n}.borrow_mut() {op_assign} 1.0_f64; Value::Number(*{n}.borrow()) }}")
719
725
  } else {
720
- format!("{{ let _prev = {n}; {n} {op_assign} 1.0_f64; Value::Number(_prev) }}")
726
+ format!("{{ let _prev = *{n}.borrow(); *{n}.borrow_mut() {op_assign} 1.0_f64; Value::Number(_prev) }}")
721
727
  };
722
728
  }
723
729
 
724
730
  if is_prefix {
725
731
  if is_wrapped {
726
732
  format!(
727
- "{{ *{n}.borrow_mut() = Value::Number(match &*{n}.borrow() {{ Value::Number(n) => n {delta}, _ => panic!(\"{op_name} needs number\") }}); (*{n}.borrow()).clone() }}"
733
+ "{{ let _cur = (*{n}.borrow()).clone(); *{n}.borrow_mut() = Value::Number(match &_cur {{ Value::Number(n) => n {delta}, _ => panic!(\"{op_name} needs number\") }}); (*{n}.borrow()).clone() }}"
728
734
  )
729
735
  } else {
730
736
  format!(
@@ -818,7 +824,7 @@ impl Codegen {
818
824
  if self.is_async {
819
825
  self.write("use tishlang_runtime::{fetch_promise as tish_fetch_promise, fetch_all_promise as tish_fetch_all_promise, http_serve as tish_http_serve, timer_set_timeout as tish_timer_set_timeout, timer_clear_timeout as tish_timer_clear_timeout, promise_object as tish_promise_object, await_promise as tish_await_promise};\n");
820
826
  } else {
821
- self.write("use tishlang_runtime::{fetch_promise as tish_fetch_promise, fetch_all_promise as tish_fetch_all_promise, http_serve as tish_http_serve};\n");
827
+ self.write("use tishlang_runtime::{fetch_promise as tish_fetch_promise, fetch_all_promise as tish_fetch_all_promise, http_serve as tish_http_serve, timer_set_timeout as tish_timer_set_timeout, timer_clear_timeout as tish_timer_clear_timeout};\n");
822
828
  }
823
829
  }
824
830
  if self.has_feature("fs") {
@@ -962,9 +968,9 @@ impl Codegen {
962
968
  if self.has_feature("http") {
963
969
  self.writeln("let fetch = Value::Function(Rc::new(|args: &[Value]| tish_fetch_promise(args.to_vec())));");
964
970
  self.writeln("let fetchAll = Value::Function(Rc::new(|args: &[Value]| tish_fetch_all_promise(args.to_vec())));");
971
+ self.writeln("let setTimeout = Value::Function(Rc::new(|args: &[Value]| tish_timer_set_timeout(args)));");
972
+ self.writeln("let clearTimeout = Value::Function(Rc::new(|args: &[Value]| tish_timer_clear_timeout(args)));");
965
973
  if self.is_async {
966
- self.writeln("let setTimeout = Value::Function(Rc::new(|args: &[Value]| tish_timer_set_timeout(args)));");
967
- self.writeln("let clearTimeout = Value::Function(Rc::new(|args: &[Value]| tish_timer_clear_timeout(args)));");
968
974
  self.writeln("let Promise = tish_promise_object();");
969
975
  }
970
976
  self.writeln("let serve = Value::Function(Rc::new(|args: &[Value]| {");
@@ -1050,6 +1056,7 @@ impl Codegen {
1050
1056
  self.indent += 1;
1051
1057
  self.type_context.push_scope();
1052
1058
  self.outer_vars_stack.push(Vec::new());
1059
+ self.rc_cell_storage_scopes.push(std::collections::HashSet::new());
1053
1060
  // Prepass: vars that must be RefCell because nested closures capture and mutate them
1054
1061
  let vars_mutated_by_nested = Self::collect_vars_mutated_by_nested_closures(statements);
1055
1062
  for v in &vars_mutated_by_nested {
@@ -1068,6 +1075,7 @@ impl Codegen {
1068
1075
  }
1069
1076
  self.function_scope_stack.pop(); // Exit scope
1070
1077
  self.outer_vars_stack.pop(); // Exit variable scope
1078
+ self.rc_cell_storage_scopes.pop();
1071
1079
  for v in &vars_mutated_by_nested {
1072
1080
  self.refcell_wrapped_vars.remove(v);
1073
1081
  }
@@ -1090,12 +1098,24 @@ impl Codegen {
1090
1098
 
1091
1099
  if rust_type.is_native() {
1092
1100
  // Generate native typed variable
1093
- let type_str = rust_type.to_rust_type_str();
1094
1101
  let expr_str = match init.as_ref() {
1095
1102
  Some(e) => self.emit_native_expr(e, &rust_type)?,
1096
1103
  None => rust_type.default_value(),
1097
1104
  };
1098
- self.writeln(&format!("{} {}: {} = {};", mutability, escaped_name, type_str, expr_str));
1105
+ if self.refcell_wrapped_vars.contains(name.as_ref()) {
1106
+ // Closure-mutated: same Rc<RefCell<T>> pattern as Value (assignments use borrow_mut)
1107
+ self.writeln(&format!(
1108
+ "let {} = std::rc::Rc::new(RefCell::new({}));",
1109
+ escaped_name, expr_str
1110
+ ));
1111
+ self.rc_cell_storage_define(name.as_ref());
1112
+ } else {
1113
+ let type_str = rust_type.to_rust_type_str();
1114
+ self.writeln(&format!(
1115
+ "{} {}: {} = {};",
1116
+ mutability, escaped_name, type_str, expr_str
1117
+ ));
1118
+ }
1099
1119
  } else {
1100
1120
  // Original Value-based codegen
1101
1121
  let (expr_str, clone_needed) = match init.as_ref() {
@@ -1117,6 +1137,7 @@ impl Codegen {
1117
1137
  expr_str.to_string()
1118
1138
  };
1119
1139
  self.writeln(&format!("let {} = std::rc::Rc::new(RefCell::new({}));", escaped_name, init_val));
1140
+ self.rc_cell_storage_define(name.as_ref());
1120
1141
  } else if clone_needed {
1121
1142
  self.writeln(&format!("{} {} = ({}).clone();", mutability, escaped_name, expr_str));
1122
1143
  } else {
@@ -1436,7 +1457,7 @@ impl Codegen {
1436
1457
  // If outer scope already has the var as RefCell, just clone it.
1437
1458
  for outer_var in &outer_vars {
1438
1459
  let var_escaped = Self::escape_ident(outer_var);
1439
- if self.refcell_wrapped_vars.contains(outer_var) {
1460
+ if self.rc_cell_storage_contains(outer_var) {
1440
1461
  self.writeln(&format!("let {}_cell = {}.clone();", var_escaped, var_escaped));
1441
1462
  } else {
1442
1463
  self.writeln(&format!("let {}_cell = std::rc::Rc::new(RefCell::new({}.clone()));", var_escaped, var_escaped));
@@ -1715,7 +1736,12 @@ impl Codegen {
1715
1736
  Expr::Ident { name, .. } => {
1716
1737
  let escaped = Self::escape_ident(name.as_ref());
1717
1738
  if self.refcell_wrapped_vars.contains(name.as_ref()) {
1718
- format!("(*{}.borrow()).clone()", escaped)
1739
+ let var_type = self.type_context.get_type(name.as_ref());
1740
+ if var_type.is_native() {
1741
+ var_type.to_value_expr(&format!("(*{}.borrow())", escaped))
1742
+ } else {
1743
+ format!("(*{}.borrow()).clone()", escaped)
1744
+ }
1719
1745
  } else {
1720
1746
  // Check if this is a typed variable that needs conversion to Value
1721
1747
  let var_type = self.type_context.get_type(name.as_ref());
@@ -1863,6 +1889,18 @@ impl Codegen {
1863
1889
  obj_expr, search, search, from
1864
1890
  ));
1865
1891
  }
1892
+ "lastIndexOf" => {
1893
+ let search = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
1894
+ let position = if args.len() >= 2 {
1895
+ arg_exprs.get(1).cloned().unwrap_or_else(|| "Value::Null".to_string())
1896
+ } else {
1897
+ "Value::Number(f64::INFINITY)".to_string()
1898
+ };
1899
+ return Ok(format!(
1900
+ "{{ let _obj = ({}).clone(); match &_obj {{ Value::String(_) => tishlang_runtime::string_last_index_of(&_obj, &{}, &{}), _ => Value::Number(-1.0) }} }}",
1901
+ obj_expr, search, position
1902
+ ));
1903
+ }
1866
1904
  "includes" => {
1867
1905
  let search = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
1868
1906
  let from = arg_exprs.get(1).cloned().unwrap_or_else(|| "Value::Null".to_string());
@@ -2302,37 +2340,47 @@ impl Codegen {
2302
2340
  }
2303
2341
  Expr::Assign { name, value, .. } => {
2304
2342
  let escaped = Self::escape_ident(name.as_ref());
2305
- // Native fast path: if the target is a scalar native type, emit
2306
- // a direct assignment without boxing/unboxing through Value.
2307
- if !self.refcell_wrapped_vars.contains(name.as_ref()) {
2308
- let rust_type = self.type_context.get_type(name.as_ref());
2309
- if rust_type.is_native() && matches!(rust_type, RustType::F64 | RustType::Bool | RustType::String) {
2310
- let (val_code, val_ty) = self.emit_typed_expr(value)?;
2311
- let native_val = if val_ty == rust_type {
2312
- val_code
2313
- } else if val_ty == RustType::Value {
2314
- rust_type.from_value_expr(&val_code)
2315
- } else {
2316
- val_code
2317
- };
2318
- let return_val = rust_type.to_value_expr(&escaped);
2319
- return Ok(format!(
2320
- "{{ {} = {}; {} }}",
2321
- escaped, native_val, return_val
2322
- ));
2323
- }
2343
+ let rust_type = self.type_context.get_type(name.as_ref());
2344
+ let is_ref = self.refcell_wrapped_vars.contains(name.as_ref());
2345
+ // Native fast path: direct assignment (plain or Rc<RefCell<T>> for closure capture)
2346
+ if rust_type.is_native()
2347
+ && matches!(rust_type, RustType::F64 | RustType::Bool | RustType::String)
2348
+ {
2349
+ let (val_code, val_ty) = self.emit_typed_expr(value)?;
2350
+ let native_val = if val_ty == rust_type {
2351
+ val_code
2352
+ } else if val_ty == RustType::Value {
2353
+ rust_type.from_value_expr(&val_code)
2354
+ } else {
2355
+ val_code
2356
+ };
2357
+ let return_val = if is_ref {
2358
+ rust_type.to_value_expr(&format!("(*{}.borrow())", escaped))
2359
+ } else {
2360
+ rust_type.to_value_expr(&escaped)
2361
+ };
2362
+ // Rust evaluates the assignment place before the RHS; RHS must not call
2363
+ // `.borrow()` on the same RefCell while `borrow_mut()` is active.
2364
+ let assign_stmt = if is_ref {
2365
+ format!(
2366
+ "let _assign_tmp = {}; *{}.borrow_mut() = _assign_tmp",
2367
+ native_val, escaped
2368
+ )
2369
+ } else {
2370
+ format!("{} = {}", escaped, native_val)
2371
+ };
2372
+ return Ok(format!("{{ {}; {} }}", assign_stmt, return_val));
2324
2373
  }
2325
2374
  // Fallback: Value path
2326
2375
  let val = self.emit_expr(value)?;
2327
2376
  let needs_outer_clone = self.should_clone(value);
2328
- if self.refcell_wrapped_vars.contains(name.as_ref()) {
2377
+ if is_ref {
2329
2378
  if needs_outer_clone {
2330
2379
  format!("{{ let _v = ({}).clone(); *{}.borrow_mut() = _v.clone(); _v }}", val, escaped)
2331
2380
  } else {
2332
2381
  format!("{{ let _v = {}; *{}.borrow_mut() = _v.clone(); _v }}", val, escaped)
2333
2382
  }
2334
2383
  } else {
2335
- let rust_type = self.type_context.get_type(name.as_ref());
2336
2384
  let assign_rhs = if matches!(rust_type, RustType::Value) {
2337
2385
  "_v.clone()".to_string()
2338
2386
  } else {
@@ -2394,6 +2442,31 @@ impl Codegen {
2394
2442
  let is_refcell = self.refcell_wrapped_vars.contains(name.as_ref());
2395
2443
  let var_type = self.type_context.get_type(name.as_ref());
2396
2444
 
2445
+ // ── native f64 in Rc<RefCell<f64>> (closure-mutated) ───────────
2446
+ if is_refcell && var_type == RustType::F64 {
2447
+ let (rhs_code, rhs_ty) = self.emit_typed_expr(value)?;
2448
+ let rhs_f64 = if rhs_ty == RustType::F64 {
2449
+ rhs_code
2450
+ } else {
2451
+ let rhs_val = if rhs_ty.is_native() {
2452
+ rhs_ty.to_value_expr(&rhs_code)
2453
+ } else {
2454
+ rhs_code
2455
+ };
2456
+ format!("(match &({}) {{ Value::Number(n) => *n, v => panic!(\"compound assign: expected number, got {{:?}}\", v) }})", rhs_val)
2457
+ };
2458
+ let op_str = match op {
2459
+ CompoundOp::Add => "+=",
2460
+ CompoundOp::Sub => "-=",
2461
+ CompoundOp::Mul => "*=",
2462
+ CompoundOp::Div => "/=",
2463
+ CompoundOp::Mod => "%=",
2464
+ };
2465
+ return Ok(format!(
2466
+ "{{ let _op_rhs = {rhs_f64}; *{n}.borrow_mut() {op_str} _op_rhs; Value::Number(*{n}.borrow()) }}"
2467
+ ));
2468
+ }
2469
+
2397
2470
  // ── native f64 fast path: direct arithmetic operators ─────────
2398
2471
  // emit_expr must return a Value expression; wrap the result back.
2399
2472
  if !is_refcell && var_type == RustType::F64 {
@@ -2420,6 +2493,32 @@ impl Codegen {
2420
2493
  return Ok(format!("{{ {} {} {}; Value::Number({}) }}", n, op_str, rhs_f64, n));
2421
2494
  }
2422
2495
 
2496
+ // ── native String += in Rc<RefCell<String>> ───────────────────
2497
+ if is_refcell && var_type == RustType::String && matches!(op, CompoundOp::Add) {
2498
+ let (rhs_code, rhs_ty) = self.emit_typed_expr(value)?;
2499
+ let rhs_str = if rhs_ty == RustType::String {
2500
+ rhs_code
2501
+ } else {
2502
+ let rhs_val = if rhs_ty.is_native() {
2503
+ rhs_ty.to_value_expr(&rhs_code)
2504
+ } else {
2505
+ rhs_code
2506
+ };
2507
+ format!(
2508
+ "match &({}) {{ \
2509
+ Value::String(s) => s.to_string(), \
2510
+ Value::Number(n) => {{ let i = *n as i64; if (*n - i as f64).abs() < f64::EPSILON {{ i.to_string() }} else {{ n.to_string() }} }}, \
2511
+ Value::Bool(b) => b.to_string(), \
2512
+ Value::Null => \"null\".to_string(), \
2513
+ other => format!(\"{{:?}}\", other) }}",
2514
+ rhs_val
2515
+ )
2516
+ };
2517
+ return Ok(format!(
2518
+ "{{ let _push_rhs = {rhs_str}; (*{n}.borrow_mut()).push_str(&_push_rhs); Value::String((*{n}.borrow()).clone().into()) }}"
2519
+ ));
2520
+ }
2521
+
2423
2522
  // ── native String += fast path: push_str ─────────────────────
2424
2523
  if !is_refcell && var_type == RustType::String && matches!(op, CompoundOp::Add) {
2425
2524
  let (rhs_code, rhs_ty) = self.emit_typed_expr(value)?;
@@ -2458,8 +2557,8 @@ impl Codegen {
2458
2557
  };
2459
2558
  if is_refcell {
2460
2559
  format!(
2461
- "{{ let _rhs = ({}).clone(); *{}.borrow_mut() = tishlang_runtime::ops::{}(&*{}.borrow(), &_rhs)?; (*{}.borrow()).clone() }}",
2462
- val, n, op_fn, n, n
2560
+ "{{ let _lhs_v = (*{}.borrow()).clone(); let _rhs = ({}).clone(); let _new = tishlang_runtime::ops::{}(&_lhs_v, &_rhs)?; *{}.borrow_mut() = _new; (*{}.borrow()).clone() }}",
2561
+ n, val, op_fn, n, n
2463
2562
  )
2464
2563
  } else if var_type.is_native() {
2465
2564
  // Wrap native lhs as Value, run ops::, unbox result back to native
@@ -2484,26 +2583,56 @@ impl Codegen {
2484
2583
  let var_type = self.type_context.get_type(name.as_ref());
2485
2584
 
2486
2585
  // ── native type: wrap for condition, unbox for assignment ──────
2487
- if !is_refcell && var_type.is_native() {
2488
- // n_as_value uses .clone() for String so we don't consume n
2489
- let n_as_value = var_type.to_value_expr(&n);
2586
+ // (plain binding or Rc<RefCell<T>> when closure-mutated)
2587
+ if var_type.is_native() {
2588
+ let inner = if is_refcell {
2589
+ format!("(*{}.borrow())", n)
2590
+ } else {
2591
+ n.clone()
2592
+ };
2593
+ let n_as_value = var_type.to_value_expr(&inner);
2490
2594
  let val_as_native = var_type.from_value_expr("_v");
2595
+ let ret_expr = if is_refcell {
2596
+ var_type.to_value_expr(&format!("(*{}.borrow())", n))
2597
+ } else {
2598
+ var_type.to_value_expr(&n)
2599
+ };
2491
2600
  let (cond, assign_and_return, else_expr) = match op {
2492
2601
  LogicalAssignOp::AndAnd => (
2493
2602
  format!("{{ let __chk = {}; __chk.is_truthy() }}", n_as_value),
2494
- format!("{{ let _v = ({}).clone(); {} = {}; {} }}", val, n, val_as_native, var_type.to_value_expr(&n)),
2495
- var_type.to_value_expr(&n),
2603
+ if is_refcell {
2604
+ format!(
2605
+ "{{ let _v = ({}).clone(); *{}.borrow_mut() = {}; {} }}",
2606
+ val, n, val_as_native, ret_expr
2607
+ )
2608
+ } else {
2609
+ format!(
2610
+ "{{ let _v = ({}).clone(); {} = {}; {} }}",
2611
+ val, n, val_as_native, ret_expr
2612
+ )
2613
+ },
2614
+ ret_expr.clone(),
2496
2615
  ),
2497
2616
  LogicalAssignOp::OrOr => (
2498
2617
  format!("!{{ let __chk = {}; __chk.is_truthy() }}", n_as_value),
2499
- format!("{{ let _v = ({}).clone(); {} = {}; {} }}", val, n, val_as_native, var_type.to_value_expr(&n)),
2500
- var_type.to_value_expr(&n),
2618
+ if is_refcell {
2619
+ format!(
2620
+ "{{ let _v = ({}).clone(); *{}.borrow_mut() = {}; {} }}",
2621
+ val, n, val_as_native, ret_expr
2622
+ )
2623
+ } else {
2624
+ format!(
2625
+ "{{ let _v = ({}).clone(); {} = {}; {} }}",
2626
+ val, n, val_as_native, ret_expr
2627
+ )
2628
+ },
2629
+ ret_expr.clone(),
2501
2630
  ),
2502
2631
  // Native types (f64, String, bool) are never null — ??= is a no-op
2503
2632
  LogicalAssignOp::Nullish => (
2504
2633
  "false".to_string(),
2505
- var_type.to_value_expr(&n), // unreachable but must type-check
2506
- var_type.to_value_expr(&n),
2634
+ ret_expr.clone(),
2635
+ ret_expr.clone(),
2507
2636
  ),
2508
2637
  };
2509
2638
  return Ok(format!("{{ if {} {{ {} }} else {{ {} }} }}", cond, assign_and_return, else_expr));
@@ -3433,14 +3562,12 @@ impl Codegen {
3433
3562
  let mutable_outer_vars: Vec<String> = outer_vars.iter().filter(|v| assigned_in_body.contains(*v)).cloned().collect();
3434
3563
  let read_only_outer_vars: Vec<String> = outer_vars.iter().filter(|v| !assigned_in_body.contains(*v)).cloned().collect();
3435
3564
 
3436
- // Track which vars are already RefCell-wrapped (from outer closure) to avoid double-wrapping
3437
- let already_wrapped = self.refcell_wrapped_vars.clone();
3438
-
3439
- // Wrap outer captures in Rc<RefCell<>> and use _ref suffix
3565
+ // Wrap outer captures in Rc<RefCell<>> and use _ref suffix.
3566
+ // Clone existing Rc only when VarDecl actually emitted `Rc<RefCell<...>>` (see rc_cell_storage_*).
3440
3567
  for outer_param in &outer_params {
3441
3568
  let param_escaped = Self::escape_ident(outer_param);
3442
3569
  let ref_name = format!("{}_ref", param_escaped);
3443
- if already_wrapped.contains(outer_param) {
3570
+ if self.rc_cell_storage_contains(outer_param) {
3444
3571
  code.push_str(&format!(" let {} = {}.clone();\n", ref_name, param_escaped));
3445
3572
  } else {
3446
3573
  code.push_str(&format!(" let {} = std::rc::Rc::new(RefCell::new({}.clone()));\n", ref_name, param_escaped));
@@ -3449,7 +3576,7 @@ impl Codegen {
3449
3576
  for outer_var in &outer_vars {
3450
3577
  let var_escaped = Self::escape_ident(outer_var);
3451
3578
  let ref_name = format!("{}_ref", var_escaped);
3452
- if already_wrapped.contains(outer_var) {
3579
+ if self.rc_cell_storage_contains(outer_var) {
3453
3580
  code.push_str(&format!(" let {} = {}.clone();\n", ref_name, var_escaped));
3454
3581
  } else {
3455
3582
  code.push_str(&format!(" let {} = std::rc::Rc::new(RefCell::new({}.clone()));\n", ref_name, var_escaped));
@@ -3625,7 +3752,11 @@ impl Codegen {
3625
3752
  if let Expr::Ident { name, .. } = expr {
3626
3753
  let var_type = self.type_context.get_type(name.as_ref());
3627
3754
  if &var_type == target_type {
3628
- return Ok(Self::escape_ident(name.as_ref()).into_owned());
3755
+ let esc = Self::escape_ident(name.as_ref()).into_owned();
3756
+ if self.refcell_wrapped_vars.contains(name.as_ref()) {
3757
+ return Ok(format!("(*{}.borrow()).clone()", esc));
3758
+ }
3759
+ return Ok(esc);
3629
3760
  }
3630
3761
  }
3631
3762
 
@@ -3658,8 +3789,12 @@ impl Codegen {
3658
3789
  Expr::Ident { name, .. } => {
3659
3790
  let escaped = Self::escape_ident(name.as_ref());
3660
3791
  if self.refcell_wrapped_vars.contains(name.as_ref()) {
3661
- // RefCell-wrapped: unwrap via borrow and return Value
3662
- Ok((format!("(*{}.borrow()).clone()", escaped), RustType::Value))
3792
+ let var_type = self.type_context.get_type(name.as_ref());
3793
+ if var_type.is_native() {
3794
+ Ok((format!("(*{}.borrow()).clone()", escaped), var_type))
3795
+ } else {
3796
+ Ok((format!("(*{}.borrow()).clone()", escaped), RustType::Value))
3797
+ }
3663
3798
  } else {
3664
3799
  let var_type = self.type_context.get_type(name.as_ref());
3665
3800
  if var_type.is_native() {
@@ -108,7 +108,7 @@ fn factory() {
108
108
  .into_iter()
109
109
  .map(String::from)
110
110
  .collect::<Vec<_>>();
111
- let (rust, _) = compile_project_full(&bench, bench.parent(), &features, true).unwrap();
111
+ let (rust, _, _) = compile_project_full(&bench, bench.parent(), &features, true).unwrap();
112
112
  // outerVar = 42 is inferred as f64; f64 is Copy so no .clone() is emitted.
113
113
  assert!(
114
114
  rust.contains("let mut outerVar: f64"),
@@ -363,6 +363,7 @@ impl Value {
363
363
  "endsWith".into(),
364
364
  "includes".into(),
365
365
  "indexOf".into(),
366
+ "lastIndexOf".into(),
366
367
  "padEnd".into(),
367
368
  "padStart".into(),
368
369
  "repeat".into(),