@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.
- 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_lint/Cargo.toml +1 -0
- package/crates/tish_lint/src/bin/tish-lint.rs +141 -23
- package/crates/tish_lint/src/lib.rs +3 -1
- package/crates/tish_lsp/README.md +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 -8
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
580
|
-
|
|
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
|
|
715
|
-
if
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
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
|
|
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
|
|
2462
|
-
|
|
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
|
-
|
|
2488
|
-
|
|
2489
|
-
let
|
|
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
|
-
|
|
2495
|
-
|
|
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
|
-
|
|
2500
|
-
|
|
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
|
-
|
|
2506
|
-
|
|
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
|
-
//
|
|
3437
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3662
|
-
|
|
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"),
|