@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
@@ -393,17 +393,27 @@ 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, the **effective** feature list
401
+ /// (CLI features plus any inferred from `tish:fs` / `tish:http` / … imports), and native build
402
+ /// artifacts (Cargo dep lines, optional `generated_native.rs` source, init strategy per spec).
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<
409
+ (
410
+ String,
411
+ Vec<crate::resolve::ResolvedNativeModule>,
412
+ Vec<String>,
413
+ crate::resolve::NativeBuildArtifacts,
414
+ ),
415
+ CompileError,
416
+ > {
407
417
  use crate::resolve;
408
418
  let root = project_root.unwrap_or_else(|| entry_path.parent().unwrap_or(Path::new(".")));
409
419
  let modules = resolve::resolve_project(entry_path, project_root)
@@ -414,14 +424,23 @@ pub fn compile_project_full(
414
424
  .map_err(|e| CompileError { message: e, span: None })?;
415
425
  let native_modules = resolve::resolve_native_modules(&merged, root)
416
426
  .map_err(|e| CompileError { message: e, span: None })?;
427
+ let native_build = resolve::compute_native_build_artifacts(&merged, root, &native_modules)
428
+ .map_err(|e| CompileError { message: e, span: None })?;
417
429
  let mut all_features: Vec<String> = features.to_vec();
418
430
  for f in resolve::extract_native_import_features(&merged) {
419
431
  if !all_features.contains(&f) {
420
432
  all_features.push(f);
421
433
  }
422
434
  }
423
- let rust = compile_with_native_modules(&merged, project_root, &all_features, &native_modules, optimize)?;
424
- Ok((rust, native_modules))
435
+ let rust = compile_with_native_modules(
436
+ &merged,
437
+ project_root,
438
+ &all_features,
439
+ &native_modules,
440
+ &native_build.native_init,
441
+ optimize,
442
+ )?;
443
+ Ok((rust, native_modules, all_features, native_build))
425
444
  }
426
445
 
427
446
  /// Compile with explicit feature flags. When features are provided, codegen uses them
@@ -431,7 +450,8 @@ pub fn compile_with_features(
431
450
  project_root: Option<&Path>,
432
451
  features: &[String],
433
452
  ) -> Result<String, CompileError> {
434
- compile_with_native_modules(program, project_root, features, &[], true)
453
+ let empty = std::collections::HashMap::new();
454
+ compile_with_native_modules(program, project_root, features, &[], &empty, true)
435
455
  }
436
456
 
437
457
  /// Compile with resolved native modules. Native imports emit calls to the module crates directly.
@@ -440,16 +460,30 @@ pub fn compile_with_native_modules(
440
460
  project_root: Option<&Path>,
441
461
  features: &[String],
442
462
  native_modules: &[crate::resolve::ResolvedNativeModule],
463
+ native_init: &std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
443
464
  optimize: bool,
444
465
  ) -> Result<String, CompileError> {
445
466
  let program = if optimize { tishlang_opt::optimize(program) } else { program.clone() };
446
467
  // Type-inference pass: fills in `type_ann` on unannotated VarDecl nodes where
447
468
  // the type is unambiguous (literals, arithmetic of typed vars, etc.).
448
469
  let program = crate::infer::infer_program(&program);
449
- let map: std::collections::HashMap<String, (String, String)> = native_modules
450
- .iter()
451
- .map(|m| (m.spec.clone(), (m.crate_name.clone(), m.export_fn.clone())))
452
- .collect();
470
+ let map: std::collections::HashMap<String, crate::resolve::NativeModuleInit> =
471
+ if native_init.is_empty() {
472
+ native_modules
473
+ .iter()
474
+ .map(|m| {
475
+ (
476
+ m.spec.clone(),
477
+ crate::resolve::NativeModuleInit::Legacy {
478
+ crate_name: m.crate_name.clone(),
479
+ export_fn: m.export_fn.clone(),
480
+ },
481
+ )
482
+ })
483
+ .collect()
484
+ } else {
485
+ native_init.clone()
486
+ };
453
487
  let mut g = Codegen::new_with_native_modules(project_root, features, map);
454
488
  g.emit_program(&program)?;
455
489
  Ok(g.output)
@@ -463,8 +497,8 @@ struct Codegen {
463
497
  project_root: Option<std::path::PathBuf>,
464
498
  /// Requested features (http, process, fs, regex, polars). When non-empty, used instead of #[cfg].
465
499
  features: std::collections::HashSet<String>,
466
- /// spec -> (crate_name, export_fn) for native modules resolved via package.json
467
- native_module_map: std::collections::HashMap<String, (String, String)>,
500
+ /// spec -> native init strategy (legacy adapter object vs generated `generated_native` wrapper)
501
+ native_module_init: std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
468
502
  /// Stack: true = async Rust context (run body), false = sync closure (Tish fn body)
469
503
  async_context_stack: Vec<bool>,
470
504
  loop_stack: Vec<(String, Option<String>)>, // (break_label, continue_update) for innermost loop
@@ -480,6 +514,9 @@ struct Codegen {
480
514
  /// Variables currently wrapped in Rc<RefCell<Value>> for mutable capture in closures
481
515
  /// These need special handling: reads via .borrow().clone(), writes via *var.borrow_mut()
482
516
  refcell_wrapped_vars: std::collections::HashSet<String>,
517
+ /// Scopes of names whose Rust binding is actually `Rc<RefCell<_>>` (emitted at VarDecl).
518
+ /// `refcell_wrapped_vars` alone is insufficient: it is set by prepasses before decl may run.
519
+ rc_cell_storage_scopes: Vec<std::collections::HashSet<String>>,
483
520
  /// Usage analyzer for move/clone optimization
484
521
  usage_analyzer: Option<UsageAnalyzer>,
485
522
  /// Type context for tracking variable types (for static typing)
@@ -492,7 +529,7 @@ impl Codegen {
492
529
  fn new_with_native_modules(
493
530
  project_root: Option<&Path>,
494
531
  features: &[String],
495
- native_module_map: std::collections::HashMap<String, (String, String)>,
532
+ native_module_init: std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
496
533
  ) -> Self {
497
534
  let features: std::collections::HashSet<String> = features.iter().cloned().collect();
498
535
  Self {
@@ -502,31 +539,54 @@ impl Codegen {
502
539
  is_async: false,
503
540
  project_root: project_root.map(|p| p.to_path_buf()),
504
541
  features,
505
- native_module_map,
542
+ native_module_init,
506
543
  async_context_stack: Vec::new(),
507
544
  loop_stack: Vec::new(),
508
545
  function_scope_stack: vec![Vec::new()], // Start with global scope
509
546
  outer_params_stack: Vec::new(),
510
547
  outer_vars_stack: vec![Vec::new()], // Start with module-level scope
511
548
  refcell_wrapped_vars: std::collections::HashSet::new(),
549
+ rc_cell_storage_scopes: vec![std::collections::HashSet::new()],
512
550
  usage_analyzer: None,
513
551
  type_context: TypeContext::new(),
514
552
  program_has_jsx: false,
515
553
  }
516
554
  }
517
555
 
556
+ fn rc_cell_storage_contains(&self, name: &str) -> bool {
557
+ self.rc_cell_storage_scopes
558
+ .iter()
559
+ .rev()
560
+ .any(|s| s.contains(name))
561
+ }
562
+
563
+ fn rc_cell_storage_define(&mut self, name: &str) {
564
+ if let Some(scope) = self.rc_cell_storage_scopes.last_mut() {
565
+ scope.insert(name.to_string());
566
+ }
567
+ }
568
+
518
569
  /// Map native module spec to Rust init expression using resolved package.json modules.
519
570
  /// For built-in modules (tish:fs, tish:http, tish:process), use builtin_native_module_rust_init.
520
571
  fn native_module_rust_init(&self, spec: &str, export_name: &str) -> Option<String> {
521
572
  if is_builtin_native_spec(spec) {
522
573
  return self.builtin_native_module_rust_init(spec, export_name);
523
574
  }
524
- self.native_module_map.get(spec).map(|(crate_name, export_fn)| {
575
+ self.native_module_init.get(spec).map(|init| {
525
576
  // Native modules return a namespace object (like an ES module).
526
577
  // Named imports extract the field from that namespace: `import { foo } from "pkg"` → `ns.foo`.
578
+ let init_expr = match init {
579
+ crate::resolve::NativeModuleInit::Legacy {
580
+ crate_name,
581
+ export_fn,
582
+ } => format!("{}::{}()", crate_name, export_fn),
583
+ crate::resolve::NativeModuleInit::Generated { export_fn, .. } => {
584
+ format!("crate::generated_native::{}()", export_fn)
585
+ }
586
+ };
527
587
  format!(
528
- "{{ let _ns = {}::{}(); match _ns {{ Value::Object(ref _o) => _o.borrow().get({:?}).cloned().unwrap_or(Value::Null), _ => Value::Null }} }}",
529
- crate_name, export_fn, export_name
588
+ "{{ let _ns = {}; match _ns {{ Value::Object(ref _o) => _o.borrow().get({:?}).cloned().unwrap_or(Value::Null), _ => Value::Null }} }}",
589
+ init_expr, export_name
530
590
  )
531
591
  })
532
592
  }
@@ -576,28 +636,8 @@ impl Codegen {
576
636
  }
577
637
 
578
638
  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
639
+ if self.features.contains("full") {
640
+ matches!(name, "http" | "fs" | "process" | "regex" | "ws")
601
641
  } else {
602
642
  self.features.contains(name)
603
643
  }
@@ -711,20 +751,27 @@ impl Codegen {
711
751
  let is_wrapped = self.refcell_wrapped_vars.contains(name);
712
752
  let var_type = self.type_context.get_type(name);
713
753
 
714
- // Native fast path: f64 variable avoid boxing/unboxing.
715
- if !is_wrapped && var_type == RustType::F64 {
754
+ // Native f64 (plain or Rc<RefCell<f64>> for closure-mutated locals)
755
+ if var_type == RustType::F64 {
716
756
  let op_assign = if delta.contains('+') { "+=" } else { "-=" };
757
+ if !is_wrapped {
758
+ return if is_prefix {
759
+ format!("{{ {n} {op_assign} 1.0_f64; Value::Number({n}) }}")
760
+ } else {
761
+ format!("{{ let _prev = {n}; {n} {op_assign} 1.0_f64; Value::Number(_prev) }}")
762
+ };
763
+ }
717
764
  return if is_prefix {
718
- format!("{{ {n} {op_assign} 1.0_f64; Value::Number({n}) }}")
765
+ format!("{{ *{n}.borrow_mut() {op_assign} 1.0_f64; Value::Number(*{n}.borrow()) }}")
719
766
  } else {
720
- format!("{{ let _prev = {n}; {n} {op_assign} 1.0_f64; Value::Number(_prev) }}")
767
+ format!("{{ let _prev = *{n}.borrow(); *{n}.borrow_mut() {op_assign} 1.0_f64; Value::Number(_prev) }}")
721
768
  };
722
769
  }
723
770
 
724
771
  if is_prefix {
725
772
  if is_wrapped {
726
773
  format!(
727
- "{{ *{n}.borrow_mut() = Value::Number(match &*{n}.borrow() {{ Value::Number(n) => n {delta}, _ => panic!(\"{op_name} needs number\") }}); (*{n}.borrow()).clone() }}"
774
+ "{{ 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
775
  )
729
776
  } else {
730
777
  format!(
@@ -818,7 +865,7 @@ impl Codegen {
818
865
  if self.is_async {
819
866
  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
867
  } 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");
868
+ 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
869
  }
823
870
  }
824
871
  if self.has_feature("fs") {
@@ -962,9 +1009,9 @@ impl Codegen {
962
1009
  if self.has_feature("http") {
963
1010
  self.writeln("let fetch = Value::Function(Rc::new(|args: &[Value]| tish_fetch_promise(args.to_vec())));");
964
1011
  self.writeln("let fetchAll = Value::Function(Rc::new(|args: &[Value]| tish_fetch_all_promise(args.to_vec())));");
1012
+ self.writeln("let setTimeout = Value::Function(Rc::new(|args: &[Value]| tish_timer_set_timeout(args)));");
1013
+ self.writeln("let clearTimeout = Value::Function(Rc::new(|args: &[Value]| tish_timer_clear_timeout(args)));");
965
1014
  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
1015
  self.writeln("let Promise = tish_promise_object();");
969
1016
  }
970
1017
  self.writeln("let serve = Value::Function(Rc::new(|args: &[Value]| {");
@@ -1050,6 +1097,7 @@ impl Codegen {
1050
1097
  self.indent += 1;
1051
1098
  self.type_context.push_scope();
1052
1099
  self.outer_vars_stack.push(Vec::new());
1100
+ self.rc_cell_storage_scopes.push(std::collections::HashSet::new());
1053
1101
  // Prepass: vars that must be RefCell because nested closures capture and mutate them
1054
1102
  let vars_mutated_by_nested = Self::collect_vars_mutated_by_nested_closures(statements);
1055
1103
  for v in &vars_mutated_by_nested {
@@ -1068,6 +1116,7 @@ impl Codegen {
1068
1116
  }
1069
1117
  self.function_scope_stack.pop(); // Exit scope
1070
1118
  self.outer_vars_stack.pop(); // Exit variable scope
1119
+ self.rc_cell_storage_scopes.pop();
1071
1120
  for v in &vars_mutated_by_nested {
1072
1121
  self.refcell_wrapped_vars.remove(v);
1073
1122
  }
@@ -1090,12 +1139,24 @@ impl Codegen {
1090
1139
 
1091
1140
  if rust_type.is_native() {
1092
1141
  // Generate native typed variable
1093
- let type_str = rust_type.to_rust_type_str();
1094
1142
  let expr_str = match init.as_ref() {
1095
1143
  Some(e) => self.emit_native_expr(e, &rust_type)?,
1096
1144
  None => rust_type.default_value(),
1097
1145
  };
1098
- self.writeln(&format!("{} {}: {} = {};", mutability, escaped_name, type_str, expr_str));
1146
+ if self.refcell_wrapped_vars.contains(name.as_ref()) {
1147
+ // Closure-mutated: same Rc<RefCell<T>> pattern as Value (assignments use borrow_mut)
1148
+ self.writeln(&format!(
1149
+ "let {} = std::rc::Rc::new(RefCell::new({}));",
1150
+ escaped_name, expr_str
1151
+ ));
1152
+ self.rc_cell_storage_define(name.as_ref());
1153
+ } else {
1154
+ let type_str = rust_type.to_rust_type_str();
1155
+ self.writeln(&format!(
1156
+ "{} {}: {} = {};",
1157
+ mutability, escaped_name, type_str, expr_str
1158
+ ));
1159
+ }
1099
1160
  } else {
1100
1161
  // Original Value-based codegen
1101
1162
  let (expr_str, clone_needed) = match init.as_ref() {
@@ -1117,6 +1178,7 @@ impl Codegen {
1117
1178
  expr_str.to_string()
1118
1179
  };
1119
1180
  self.writeln(&format!("let {} = std::rc::Rc::new(RefCell::new({}));", escaped_name, init_val));
1181
+ self.rc_cell_storage_define(name.as_ref());
1120
1182
  } else if clone_needed {
1121
1183
  self.writeln(&format!("{} {} = ({}).clone();", mutability, escaped_name, expr_str));
1122
1184
  } else {
@@ -1436,7 +1498,7 @@ impl Codegen {
1436
1498
  // If outer scope already has the var as RefCell, just clone it.
1437
1499
  for outer_var in &outer_vars {
1438
1500
  let var_escaped = Self::escape_ident(outer_var);
1439
- if self.refcell_wrapped_vars.contains(outer_var) {
1501
+ if self.rc_cell_storage_contains(outer_var) {
1440
1502
  self.writeln(&format!("let {}_cell = {}.clone();", var_escaped, var_escaped));
1441
1503
  } else {
1442
1504
  self.writeln(&format!("let {}_cell = std::rc::Rc::new(RefCell::new({}.clone()));", var_escaped, var_escaped));
@@ -1715,7 +1777,12 @@ impl Codegen {
1715
1777
  Expr::Ident { name, .. } => {
1716
1778
  let escaped = Self::escape_ident(name.as_ref());
1717
1779
  if self.refcell_wrapped_vars.contains(name.as_ref()) {
1718
- format!("(*{}.borrow()).clone()", escaped)
1780
+ let var_type = self.type_context.get_type(name.as_ref());
1781
+ if var_type.is_native() {
1782
+ var_type.to_value_expr(&format!("(*{}.borrow())", escaped))
1783
+ } else {
1784
+ format!("(*{}.borrow()).clone()", escaped)
1785
+ }
1719
1786
  } else {
1720
1787
  // Check if this is a typed variable that needs conversion to Value
1721
1788
  let var_type = self.type_context.get_type(name.as_ref());
@@ -1753,7 +1820,15 @@ impl Codegen {
1753
1820
  }
1754
1821
  Expr::Call { callee, args, .. } => {
1755
1822
  // Compile-time embed: Polars.read_csv("<literal path>") when file exists
1756
- if let Some((crate_name, _)) = self.native_module_map.get("tish:polars") {
1823
+ if let Some(init) = self.native_module_init.get("tish:polars") {
1824
+ let crate_name = match init {
1825
+ crate::resolve::NativeModuleInit::Legacy { crate_name, .. } => {
1826
+ crate_name.as_str()
1827
+ }
1828
+ crate::resolve::NativeModuleInit::Generated { shim_crate, .. } => {
1829
+ shim_crate.as_str()
1830
+ }
1831
+ };
1757
1832
  if let (Some(root), Some(CallArg::Expr(first_arg))) =
1758
1833
  (self.project_root.as_ref(), args.first())
1759
1834
  {
@@ -1863,6 +1938,18 @@ impl Codegen {
1863
1938
  obj_expr, search, search, from
1864
1939
  ));
1865
1940
  }
1941
+ "lastIndexOf" => {
1942
+ let search = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
1943
+ let position = if args.len() >= 2 {
1944
+ arg_exprs.get(1).cloned().unwrap_or_else(|| "Value::Null".to_string())
1945
+ } else {
1946
+ "Value::Number(f64::INFINITY)".to_string()
1947
+ };
1948
+ return Ok(format!(
1949
+ "{{ let _obj = ({}).clone(); match &_obj {{ Value::String(_) => tishlang_runtime::string_last_index_of(&_obj, &{}, &{}), _ => Value::Number(-1.0) }} }}",
1950
+ obj_expr, search, position
1951
+ ));
1952
+ }
1866
1953
  "includes" => {
1867
1954
  let search = arg_exprs.first().cloned().unwrap_or_else(|| "Value::Null".to_string());
1868
1955
  let from = arg_exprs.get(1).cloned().unwrap_or_else(|| "Value::Null".to_string());
@@ -2302,37 +2389,47 @@ impl Codegen {
2302
2389
  }
2303
2390
  Expr::Assign { name, value, .. } => {
2304
2391
  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
- }
2392
+ let rust_type = self.type_context.get_type(name.as_ref());
2393
+ let is_ref = self.refcell_wrapped_vars.contains(name.as_ref());
2394
+ // Native fast path: direct assignment (plain or Rc<RefCell<T>> for closure capture)
2395
+ if rust_type.is_native()
2396
+ && matches!(rust_type, RustType::F64 | RustType::Bool | RustType::String)
2397
+ {
2398
+ let (val_code, val_ty) = self.emit_typed_expr(value)?;
2399
+ let native_val = if val_ty == rust_type {
2400
+ val_code
2401
+ } else if val_ty == RustType::Value {
2402
+ rust_type.from_value_expr(&val_code)
2403
+ } else {
2404
+ val_code
2405
+ };
2406
+ let return_val = if is_ref {
2407
+ rust_type.to_value_expr(&format!("(*{}.borrow())", escaped))
2408
+ } else {
2409
+ rust_type.to_value_expr(&escaped)
2410
+ };
2411
+ // Rust evaluates the assignment place before the RHS; RHS must not call
2412
+ // `.borrow()` on the same RefCell while `borrow_mut()` is active.
2413
+ let assign_stmt = if is_ref {
2414
+ format!(
2415
+ "let _assign_tmp = {}; *{}.borrow_mut() = _assign_tmp",
2416
+ native_val, escaped
2417
+ )
2418
+ } else {
2419
+ format!("{} = {}", escaped, native_val)
2420
+ };
2421
+ return Ok(format!("{{ {}; {} }}", assign_stmt, return_val));
2324
2422
  }
2325
2423
  // Fallback: Value path
2326
2424
  let val = self.emit_expr(value)?;
2327
2425
  let needs_outer_clone = self.should_clone(value);
2328
- if self.refcell_wrapped_vars.contains(name.as_ref()) {
2426
+ if is_ref {
2329
2427
  if needs_outer_clone {
2330
2428
  format!("{{ let _v = ({}).clone(); *{}.borrow_mut() = _v.clone(); _v }}", val, escaped)
2331
2429
  } else {
2332
2430
  format!("{{ let _v = {}; *{}.borrow_mut() = _v.clone(); _v }}", val, escaped)
2333
2431
  }
2334
2432
  } else {
2335
- let rust_type = self.type_context.get_type(name.as_ref());
2336
2433
  let assign_rhs = if matches!(rust_type, RustType::Value) {
2337
2434
  "_v.clone()".to_string()
2338
2435
  } else {
@@ -2394,6 +2491,31 @@ impl Codegen {
2394
2491
  let is_refcell = self.refcell_wrapped_vars.contains(name.as_ref());
2395
2492
  let var_type = self.type_context.get_type(name.as_ref());
2396
2493
 
2494
+ // ── native f64 in Rc<RefCell<f64>> (closure-mutated) ───────────
2495
+ if is_refcell && var_type == RustType::F64 {
2496
+ let (rhs_code, rhs_ty) = self.emit_typed_expr(value)?;
2497
+ let rhs_f64 = if rhs_ty == RustType::F64 {
2498
+ rhs_code
2499
+ } else {
2500
+ let rhs_val = if rhs_ty.is_native() {
2501
+ rhs_ty.to_value_expr(&rhs_code)
2502
+ } else {
2503
+ rhs_code
2504
+ };
2505
+ format!("(match &({}) {{ Value::Number(n) => *n, v => panic!(\"compound assign: expected number, got {{:?}}\", v) }})", rhs_val)
2506
+ };
2507
+ let op_str = match op {
2508
+ CompoundOp::Add => "+=",
2509
+ CompoundOp::Sub => "-=",
2510
+ CompoundOp::Mul => "*=",
2511
+ CompoundOp::Div => "/=",
2512
+ CompoundOp::Mod => "%=",
2513
+ };
2514
+ return Ok(format!(
2515
+ "{{ let _op_rhs = {rhs_f64}; *{n}.borrow_mut() {op_str} _op_rhs; Value::Number(*{n}.borrow()) }}"
2516
+ ));
2517
+ }
2518
+
2397
2519
  // ── native f64 fast path: direct arithmetic operators ─────────
2398
2520
  // emit_expr must return a Value expression; wrap the result back.
2399
2521
  if !is_refcell && var_type == RustType::F64 {
@@ -2420,6 +2542,32 @@ impl Codegen {
2420
2542
  return Ok(format!("{{ {} {} {}; Value::Number({}) }}", n, op_str, rhs_f64, n));
2421
2543
  }
2422
2544
 
2545
+ // ── native String += in Rc<RefCell<String>> ───────────────────
2546
+ if is_refcell && var_type == RustType::String && matches!(op, CompoundOp::Add) {
2547
+ let (rhs_code, rhs_ty) = self.emit_typed_expr(value)?;
2548
+ let rhs_str = if rhs_ty == RustType::String {
2549
+ rhs_code
2550
+ } else {
2551
+ let rhs_val = if rhs_ty.is_native() {
2552
+ rhs_ty.to_value_expr(&rhs_code)
2553
+ } else {
2554
+ rhs_code
2555
+ };
2556
+ format!(
2557
+ "match &({}) {{ \
2558
+ Value::String(s) => s.to_string(), \
2559
+ Value::Number(n) => {{ let i = *n as i64; if (*n - i as f64).abs() < f64::EPSILON {{ i.to_string() }} else {{ n.to_string() }} }}, \
2560
+ Value::Bool(b) => b.to_string(), \
2561
+ Value::Null => \"null\".to_string(), \
2562
+ other => format!(\"{{:?}}\", other) }}",
2563
+ rhs_val
2564
+ )
2565
+ };
2566
+ return Ok(format!(
2567
+ "{{ let _push_rhs = {rhs_str}; (*{n}.borrow_mut()).push_str(&_push_rhs); Value::String((*{n}.borrow()).clone().into()) }}"
2568
+ ));
2569
+ }
2570
+
2423
2571
  // ── native String += fast path: push_str ─────────────────────
2424
2572
  if !is_refcell && var_type == RustType::String && matches!(op, CompoundOp::Add) {
2425
2573
  let (rhs_code, rhs_ty) = self.emit_typed_expr(value)?;
@@ -2458,8 +2606,8 @@ impl Codegen {
2458
2606
  };
2459
2607
  if is_refcell {
2460
2608
  format!(
2461
- "{{ let _rhs = ({}).clone(); *{}.borrow_mut() = tishlang_runtime::ops::{}(&*{}.borrow(), &_rhs)?; (*{}.borrow()).clone() }}",
2462
- val, n, op_fn, n, n
2609
+ "{{ let _lhs_v = (*{}.borrow()).clone(); let _rhs = ({}).clone(); let _new = tishlang_runtime::ops::{}(&_lhs_v, &_rhs)?; *{}.borrow_mut() = _new; (*{}.borrow()).clone() }}",
2610
+ n, val, op_fn, n, n
2463
2611
  )
2464
2612
  } else if var_type.is_native() {
2465
2613
  // Wrap native lhs as Value, run ops::, unbox result back to native
@@ -2484,26 +2632,56 @@ impl Codegen {
2484
2632
  let var_type = self.type_context.get_type(name.as_ref());
2485
2633
 
2486
2634
  // ── 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);
2635
+ // (plain binding or Rc<RefCell<T>> when closure-mutated)
2636
+ if var_type.is_native() {
2637
+ let inner = if is_refcell {
2638
+ format!("(*{}.borrow())", n)
2639
+ } else {
2640
+ n.clone()
2641
+ };
2642
+ let n_as_value = var_type.to_value_expr(&inner);
2490
2643
  let val_as_native = var_type.from_value_expr("_v");
2644
+ let ret_expr = if is_refcell {
2645
+ var_type.to_value_expr(&format!("(*{}.borrow())", n))
2646
+ } else {
2647
+ var_type.to_value_expr(&n)
2648
+ };
2491
2649
  let (cond, assign_and_return, else_expr) = match op {
2492
2650
  LogicalAssignOp::AndAnd => (
2493
2651
  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),
2652
+ if is_refcell {
2653
+ format!(
2654
+ "{{ let _v = ({}).clone(); *{}.borrow_mut() = {}; {} }}",
2655
+ val, n, val_as_native, ret_expr
2656
+ )
2657
+ } else {
2658
+ format!(
2659
+ "{{ let _v = ({}).clone(); {} = {}; {} }}",
2660
+ val, n, val_as_native, ret_expr
2661
+ )
2662
+ },
2663
+ ret_expr.clone(),
2496
2664
  ),
2497
2665
  LogicalAssignOp::OrOr => (
2498
2666
  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),
2667
+ if is_refcell {
2668
+ format!(
2669
+ "{{ let _v = ({}).clone(); *{}.borrow_mut() = {}; {} }}",
2670
+ val, n, val_as_native, ret_expr
2671
+ )
2672
+ } else {
2673
+ format!(
2674
+ "{{ let _v = ({}).clone(); {} = {}; {} }}",
2675
+ val, n, val_as_native, ret_expr
2676
+ )
2677
+ },
2678
+ ret_expr.clone(),
2501
2679
  ),
2502
2680
  // Native types (f64, String, bool) are never null — ??= is a no-op
2503
2681
  LogicalAssignOp::Nullish => (
2504
2682
  "false".to_string(),
2505
- var_type.to_value_expr(&n), // unreachable but must type-check
2506
- var_type.to_value_expr(&n),
2683
+ ret_expr.clone(),
2684
+ ret_expr.clone(),
2507
2685
  ),
2508
2686
  };
2509
2687
  return Ok(format!("{{ if {} {{ {} }} else {{ {} }} }}", cond, assign_and_return, else_expr));
@@ -3433,14 +3611,12 @@ impl Codegen {
3433
3611
  let mutable_outer_vars: Vec<String> = outer_vars.iter().filter(|v| assigned_in_body.contains(*v)).cloned().collect();
3434
3612
  let read_only_outer_vars: Vec<String> = outer_vars.iter().filter(|v| !assigned_in_body.contains(*v)).cloned().collect();
3435
3613
 
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
3614
+ // Wrap outer captures in Rc<RefCell<>> and use _ref suffix.
3615
+ // Clone existing Rc only when VarDecl actually emitted `Rc<RefCell<...>>` (see rc_cell_storage_*).
3440
3616
  for outer_param in &outer_params {
3441
3617
  let param_escaped = Self::escape_ident(outer_param);
3442
3618
  let ref_name = format!("{}_ref", param_escaped);
3443
- if already_wrapped.contains(outer_param) {
3619
+ if self.rc_cell_storage_contains(outer_param) {
3444
3620
  code.push_str(&format!(" let {} = {}.clone();\n", ref_name, param_escaped));
3445
3621
  } else {
3446
3622
  code.push_str(&format!(" let {} = std::rc::Rc::new(RefCell::new({}.clone()));\n", ref_name, param_escaped));
@@ -3449,7 +3625,7 @@ impl Codegen {
3449
3625
  for outer_var in &outer_vars {
3450
3626
  let var_escaped = Self::escape_ident(outer_var);
3451
3627
  let ref_name = format!("{}_ref", var_escaped);
3452
- if already_wrapped.contains(outer_var) {
3628
+ if self.rc_cell_storage_contains(outer_var) {
3453
3629
  code.push_str(&format!(" let {} = {}.clone();\n", ref_name, var_escaped));
3454
3630
  } else {
3455
3631
  code.push_str(&format!(" let {} = std::rc::Rc::new(RefCell::new({}.clone()));\n", ref_name, var_escaped));
@@ -3625,7 +3801,11 @@ impl Codegen {
3625
3801
  if let Expr::Ident { name, .. } = expr {
3626
3802
  let var_type = self.type_context.get_type(name.as_ref());
3627
3803
  if &var_type == target_type {
3628
- return Ok(Self::escape_ident(name.as_ref()).into_owned());
3804
+ let esc = Self::escape_ident(name.as_ref()).into_owned();
3805
+ if self.refcell_wrapped_vars.contains(name.as_ref()) {
3806
+ return Ok(format!("(*{}.borrow()).clone()", esc));
3807
+ }
3808
+ return Ok(esc);
3629
3809
  }
3630
3810
  }
3631
3811
 
@@ -3658,8 +3838,12 @@ impl Codegen {
3658
3838
  Expr::Ident { name, .. } => {
3659
3839
  let escaped = Self::escape_ident(name.as_ref());
3660
3840
  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))
3841
+ let var_type = self.type_context.get_type(name.as_ref());
3842
+ if var_type.is_native() {
3843
+ Ok((format!("(*{}.borrow()).clone()", escaped), var_type))
3844
+ } else {
3845
+ Ok((format!("(*{}.borrow()).clone()", escaped), RustType::Value))
3846
+ }
3663
3847
  } else {
3664
3848
  let var_type = self.type_context.get_type(name.as_ref());
3665
3849
  if var_type.is_native() {