@tishlang/tish 2.9.0 → 2.12.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 -1
- package/crates/tish/src/cli_help.rs +47 -0
- package/crates/tish/src/main.rs +92 -6
- package/crates/tish/tests/integration_test.rs +177 -0
- package/crates/tish_compile/src/codegen.rs +255 -0
- package/crates/tish_compile/tests/perf_codegen_169_173.rs +139 -0
- package/crates/tish_compile_js/src/codegen.rs +161 -3
- package/crates/tish_compile_js/src/lib.rs +7 -3
- package/crates/tish_compile_js/src/tests_jsx.rs +341 -4
- package/crates/tish_ui/src/jsx.rs +250 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
|
@@ -2097,6 +2097,148 @@ impl Codegen {
|
|
|
2097
2097
|
self.emit_expr(expr)
|
|
2098
2098
|
}
|
|
2099
2099
|
|
|
2100
|
+
/// Is `update` a `+1` step on `var` (`var++`, `++var`, `var += 1`, or `var = var + 1`)?
|
|
2101
|
+
fn is_increment_of(update: &Expr, var: &str) -> bool {
|
|
2102
|
+
match update {
|
|
2103
|
+
Expr::PostfixInc { name, .. } | Expr::PrefixInc { name, .. } => name.as_ref() == var,
|
|
2104
|
+
Expr::CompoundAssign {
|
|
2105
|
+
name,
|
|
2106
|
+
op: CompoundOp::Add,
|
|
2107
|
+
value,
|
|
2108
|
+
..
|
|
2109
|
+
} => name.as_ref() == var && Self::int_literal_value_of(value) == Some(1),
|
|
2110
|
+
Expr::Assign { name, value, .. } => {
|
|
2111
|
+
name.as_ref() == var
|
|
2112
|
+
&& matches!(
|
|
2113
|
+
value.as_ref(),
|
|
2114
|
+
Expr::Binary { left, op: BinOp::Add, right, .. }
|
|
2115
|
+
if matches!(left.as_ref(), Expr::Ident { name: l, .. } if l.as_ref() == var)
|
|
2116
|
+
&& Self::int_literal_value_of(right) == Some(1)
|
|
2117
|
+
)
|
|
2118
|
+
}
|
|
2119
|
+
_ => false,
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
/// #173: detect a fill loop `for (let i = 0; i < N; i++) { a.push(K) }` over a native `Vec<T>`
|
|
2124
|
+
/// and emit it as a single bulk `a.extend(std::iter::repeat(K).take((N) as usize))` — one
|
|
2125
|
+
/// allocation instead of N per-element pushes that repeatedly realloc as the Vec grows. Returns
|
|
2126
|
+
/// `Ok(true)` when the fused form was emitted (the caller then skips the normal loop).
|
|
2127
|
+
///
|
|
2128
|
+
/// Sound only when `N` is a proven, side-effect-free integer (so the bulk count matches the loop
|
|
2129
|
+
/// iteration count exactly, including the truncating `as usize` for `0`/negative) and `K` is a
|
|
2130
|
+
/// constant of the element type (no per-element variation). Any miss returns `Ok(false)` and the
|
|
2131
|
+
/// normal loop is emitted — correctness over coverage.
|
|
2132
|
+
fn try_emit_native_fill_loop(
|
|
2133
|
+
&mut self,
|
|
2134
|
+
init: Option<&Statement>,
|
|
2135
|
+
cond: Option<&Expr>,
|
|
2136
|
+
update: Option<&Expr>,
|
|
2137
|
+
body: &Statement,
|
|
2138
|
+
) -> Result<bool, CompileError> {
|
|
2139
|
+
// init: `let i = 0`
|
|
2140
|
+
let (
|
|
2141
|
+
Some(Statement::VarDecl {
|
|
2142
|
+
name: i_name,
|
|
2143
|
+
init: Some(i_init),
|
|
2144
|
+
..
|
|
2145
|
+
}),
|
|
2146
|
+
Some(cond),
|
|
2147
|
+
Some(update),
|
|
2148
|
+
) = (init, cond, update)
|
|
2149
|
+
else {
|
|
2150
|
+
return Ok(false);
|
|
2151
|
+
};
|
|
2152
|
+
if Self::int_literal_value_of(i_init) != Some(0) {
|
|
2153
|
+
return Ok(false);
|
|
2154
|
+
}
|
|
2155
|
+
// cond: `i < N`
|
|
2156
|
+
let Expr::Binary {
|
|
2157
|
+
left,
|
|
2158
|
+
op: BinOp::Lt,
|
|
2159
|
+
right: bound,
|
|
2160
|
+
..
|
|
2161
|
+
} = cond
|
|
2162
|
+
else {
|
|
2163
|
+
return Ok(false);
|
|
2164
|
+
};
|
|
2165
|
+
let Expr::Ident { name: c_name, .. } = left.as_ref() else {
|
|
2166
|
+
return Ok(false);
|
|
2167
|
+
};
|
|
2168
|
+
if c_name.as_ref() != i_name.as_ref() {
|
|
2169
|
+
return Ok(false);
|
|
2170
|
+
}
|
|
2171
|
+
// update: `i++` / `++i` / `i += 1` / `i = i + 1`
|
|
2172
|
+
if !Self::is_increment_of(update, i_name.as_ref()) {
|
|
2173
|
+
return Ok(false);
|
|
2174
|
+
}
|
|
2175
|
+
// body: exactly one statement `a.push(K)`
|
|
2176
|
+
let push_stmt = match body {
|
|
2177
|
+
Statement::Block { statements, .. } if statements.len() == 1 => &statements[0],
|
|
2178
|
+
Statement::ExprStmt { .. } => body,
|
|
2179
|
+
_ => return Ok(false),
|
|
2180
|
+
};
|
|
2181
|
+
let Statement::ExprStmt {
|
|
2182
|
+
expr: Expr::Call { callee, args, .. },
|
|
2183
|
+
..
|
|
2184
|
+
} = push_stmt
|
|
2185
|
+
else {
|
|
2186
|
+
return Ok(false);
|
|
2187
|
+
};
|
|
2188
|
+
let Expr::Member {
|
|
2189
|
+
object,
|
|
2190
|
+
prop: MemberProp::Name { name: method, .. },
|
|
2191
|
+
optional: false,
|
|
2192
|
+
..
|
|
2193
|
+
} = callee.as_ref()
|
|
2194
|
+
else {
|
|
2195
|
+
return Ok(false);
|
|
2196
|
+
};
|
|
2197
|
+
if method.as_ref() != "push" || args.len() != 1 {
|
|
2198
|
+
return Ok(false);
|
|
2199
|
+
}
|
|
2200
|
+
let Expr::Ident { name: arr_name, .. } = object.as_ref() else {
|
|
2201
|
+
return Ok(false);
|
|
2202
|
+
};
|
|
2203
|
+
// `a` must be a native `Vec<T>`. A closure-captured (RefCell) Vec would need a borrow_mut;
|
|
2204
|
+
// skip it (rare) and keep the plain loop.
|
|
2205
|
+
let RustType::Vec(elem) = self.type_context.get_type(arr_name.as_ref()) else {
|
|
2206
|
+
return Ok(false);
|
|
2207
|
+
};
|
|
2208
|
+
if self.refcell_wrapped_vars.contains(arr_name.as_ref()) {
|
|
2209
|
+
return Ok(false);
|
|
2210
|
+
}
|
|
2211
|
+
let CallArg::Expr(k_expr) = &args[0] else {
|
|
2212
|
+
return Ok(false);
|
|
2213
|
+
};
|
|
2214
|
+
// K must be a constant literal of the element type (no per-element variation, no `i` ref).
|
|
2215
|
+
let k_code = match (&*elem, k_expr) {
|
|
2216
|
+
(RustType::F64, Expr::Literal { value: Literal::Number(n), .. }) => Self::f64_lit(*n),
|
|
2217
|
+
(RustType::Bool, Expr::Literal { value: Literal::Bool(b), .. }) => format!("{}", b),
|
|
2218
|
+
_ => return Ok(false),
|
|
2219
|
+
};
|
|
2220
|
+
// N must be a proven, side-effect-free integer: an integer literal or an int-range local.
|
|
2221
|
+
let n_code = match bound.as_ref() {
|
|
2222
|
+
Expr::Literal {
|
|
2223
|
+
value: Literal::Number(_),
|
|
2224
|
+
..
|
|
2225
|
+
} if Self::int_literal_value_of(bound).is_some() => self.emit_typed_expr(bound)?.0,
|
|
2226
|
+
Expr::Ident { name, .. }
|
|
2227
|
+
if self.int_range_locals.contains_key(name.as_ref())
|
|
2228
|
+
&& self.type_context.get_type(name.as_ref()) == RustType::F64 =>
|
|
2229
|
+
{
|
|
2230
|
+
Self::escape_ident(name.as_ref()).into_owned()
|
|
2231
|
+
}
|
|
2232
|
+
_ => return Ok(false),
|
|
2233
|
+
};
|
|
2234
|
+
let arr_esc = Self::escape_ident(arr_name.as_ref()).into_owned();
|
|
2235
|
+
self.writeln(&format!(
|
|
2236
|
+
"{}.extend(std::iter::repeat({}).take(({}) as usize));",
|
|
2237
|
+
arr_esc, k_code, n_code
|
|
2238
|
+
));
|
|
2239
|
+
Ok(true)
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2100
2242
|
fn emit_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
|
|
2101
2243
|
match stmt {
|
|
2102
2244
|
Statement::Block { statements, .. } => {
|
|
@@ -2424,6 +2566,18 @@ impl Codegen {
|
|
|
2424
2566
|
body,
|
|
2425
2567
|
..
|
|
2426
2568
|
} => {
|
|
2569
|
+
// #173: fuse a fill loop `for (let i = 0; i < N; i++) { a.push(K) }` over a native
|
|
2570
|
+
// `Vec<T>` into a single bulk `extend` — one allocation instead of N per-element
|
|
2571
|
+
// pushes (which repeatedly realloc as the Vec grows). Sound only when `N` is a proven,
|
|
2572
|
+
// side-effect-free integer; otherwise the normal loop is emitted below.
|
|
2573
|
+
if self.try_emit_native_fill_loop(
|
|
2574
|
+
init.as_deref(),
|
|
2575
|
+
cond.as_ref(),
|
|
2576
|
+
update.as_ref(),
|
|
2577
|
+
body,
|
|
2578
|
+
)? {
|
|
2579
|
+
return Ok(());
|
|
2580
|
+
}
|
|
2427
2581
|
self.writeln("{");
|
|
2428
2582
|
self.indent += 1;
|
|
2429
2583
|
if let Some(i) = init {
|
|
@@ -7537,6 +7691,13 @@ impl Codegen {
|
|
|
7537
7691
|
..
|
|
7538
7692
|
} => {
|
|
7539
7693
|
if let Expr::Ident { name: var_name, .. } = object.as_ref() {
|
|
7694
|
+
// #173: `vec.length` on a native `Vec<_>` is a native `f64` (the emitter lowers it
|
|
7695
|
+
// to `(vec.len() as f64)`), so a local fed by `arr.length` stays native.
|
|
7696
|
+
if let Some(RustType::Vec(_)) = env.get(var_name.as_ref()) {
|
|
7697
|
+
if prop_name.as_ref() == "length" {
|
|
7698
|
+
return RustType::F64;
|
|
7699
|
+
}
|
|
7700
|
+
}
|
|
7540
7701
|
if let Some(RustType::Named { fields, .. }) = env.get(var_name.as_ref()) {
|
|
7541
7702
|
if let Some((_, field_ty)) =
|
|
7542
7703
|
fields.iter().find(|(k, _)| k.as_ref() == prop_name.as_ref())
|
|
@@ -7577,6 +7738,12 @@ impl Codegen {
|
|
|
7577
7738
|
}
|
|
7578
7739
|
}
|
|
7579
7740
|
}
|
|
7741
|
+
// #169: a fused native `Vec<f64>` reduce produces an `f64` (the emitter lowers it via
|
|
7742
|
+
// `native_vec_hof_for_call`). Model it here so an accumulator fed by `xs.reduce(...)`
|
|
7743
|
+
// is not wrongly demoted to a boxed `Value`. Conservative: any miss → `Value`.
|
|
7744
|
+
if let Some(t) = self.native_vec_reduce_result_type(callee, args, env) {
|
|
7745
|
+
return t;
|
|
7746
|
+
}
|
|
7580
7747
|
RustType::Value
|
|
7581
7748
|
}
|
|
7582
7749
|
// Unary, Conditional, etc. are not modelled by `emit_typed_expr` (it boxes them), so a
|
|
@@ -7586,6 +7753,81 @@ impl Codegen {
|
|
|
7586
7753
|
}
|
|
7587
7754
|
}
|
|
7588
7755
|
|
|
7756
|
+
/// #169: read-only mirror of [`try_native_vec_hof`]'s `reduce` preconditions, for
|
|
7757
|
+
/// [`expr_native_type`]. Returns `Some(F64)` exactly when a `xs.reduce((acc, x) => body, init)`
|
|
7758
|
+
/// call would fuse to a native `f64` fold (so an accumulator it feeds stays native instead of
|
|
7759
|
+
/// being demoted to a boxed `Value`). Any uncertainty returns `None` — the oracle never claims
|
|
7760
|
+
/// `F64` where the emitter would box, which is what keeps the demotion analysis sound.
|
|
7761
|
+
fn native_vec_reduce_result_type(
|
|
7762
|
+
&self,
|
|
7763
|
+
callee: &Expr,
|
|
7764
|
+
args: &[CallArg],
|
|
7765
|
+
env: &HashMap<String, RustType>,
|
|
7766
|
+
) -> Option<RustType> {
|
|
7767
|
+
if std::env::var("TISH_NATIVE_HOF").is_err() {
|
|
7768
|
+
return None;
|
|
7769
|
+
}
|
|
7770
|
+
let Expr::Member {
|
|
7771
|
+
object,
|
|
7772
|
+
prop: MemberProp::Name { name: method, .. },
|
|
7773
|
+
optional: false,
|
|
7774
|
+
..
|
|
7775
|
+
} = callee
|
|
7776
|
+
else {
|
|
7777
|
+
return None;
|
|
7778
|
+
};
|
|
7779
|
+
if method.as_ref() != "reduce" {
|
|
7780
|
+
return None;
|
|
7781
|
+
}
|
|
7782
|
+
let Expr::Ident { name: recv_name, .. } = object.as_ref() else {
|
|
7783
|
+
return None;
|
|
7784
|
+
};
|
|
7785
|
+
// Receiver must be a native `Vec<f64>` (`.copied()` needs a `Copy` element).
|
|
7786
|
+
match env.get(recv_name.as_ref()) {
|
|
7787
|
+
Some(RustType::Vec(inner)) if **inner == RustType::F64 => {}
|
|
7788
|
+
_ => return None,
|
|
7789
|
+
}
|
|
7790
|
+
// `reduce(callback, init)` with a simple-param expression-body arrow that does not touch the
|
|
7791
|
+
// receiver (an alias inside the closure would break the `.iter()` borrow).
|
|
7792
|
+
if args.len() != 2 {
|
|
7793
|
+
return None;
|
|
7794
|
+
}
|
|
7795
|
+
let Some(CallArg::Expr(Expr::ArrowFunction { params, body, .. })) = args.first() else {
|
|
7796
|
+
return None;
|
|
7797
|
+
};
|
|
7798
|
+
if params.len() != 2 {
|
|
7799
|
+
return None;
|
|
7800
|
+
}
|
|
7801
|
+
let (FunParam::Simple(acc_p), FunParam::Simple(x_p)) = (¶ms[0], ¶ms[1]) else {
|
|
7802
|
+
return None;
|
|
7803
|
+
};
|
|
7804
|
+
if acc_p.default.is_some() || x_p.default.is_some() {
|
|
7805
|
+
return None;
|
|
7806
|
+
}
|
|
7807
|
+
let ArrowBody::Expr(be) = body else {
|
|
7808
|
+
return None;
|
|
7809
|
+
};
|
|
7810
|
+
if crate::infer::pi_mentions(be, recv_name.as_ref()) {
|
|
7811
|
+
return None;
|
|
7812
|
+
}
|
|
7813
|
+
// The init must be native-numeric, and the body must lower to `f64` with both closure params
|
|
7814
|
+
// bound `f64` — exactly the emitter's preconditions, evaluated read-only.
|
|
7815
|
+
let CallArg::Expr(init_e) = &args[1] else {
|
|
7816
|
+
return None;
|
|
7817
|
+
};
|
|
7818
|
+
if self.expr_native_type(init_e, env) != RustType::F64 {
|
|
7819
|
+
return None;
|
|
7820
|
+
}
|
|
7821
|
+
let mut benv = env.clone();
|
|
7822
|
+
benv.insert(acc_p.name.to_string(), RustType::F64);
|
|
7823
|
+
benv.insert(x_p.name.to_string(), RustType::F64);
|
|
7824
|
+
if self.expr_native_type(be, &benv) == RustType::F64 {
|
|
7825
|
+
Some(RustType::F64)
|
|
7826
|
+
} else {
|
|
7827
|
+
None
|
|
7828
|
+
}
|
|
7829
|
+
}
|
|
7830
|
+
|
|
7589
7831
|
/// Names of top-level fns eligible for a parallel native `fn f_native(f64,..)->f64`:
|
|
7590
7832
|
/// non-async, every param `: number` (no default), `: number` return, and a native-safe
|
|
7591
7833
|
/// body (only block/if/return/expr-stmt over native exprs + calls to other eligible fns or
|
|
@@ -9444,6 +9686,19 @@ impl Codegen {
|
|
|
9444
9686
|
} => {
|
|
9445
9687
|
if let Expr::Ident { name: var_name, .. } = object.as_ref() {
|
|
9446
9688
|
let var_type = self.type_context.get_type(var_name.as_ref());
|
|
9689
|
+
// #173: `vec.length` on a native `Vec<_>` → `(vec.len() as f64)`, so the length
|
|
9690
|
+
// (and arithmetic derived from it) stays native instead of a boxed `get_prop`.
|
|
9691
|
+
if let RustType::Vec(_) = &var_type {
|
|
9692
|
+
if prop_name.as_ref() == "length" {
|
|
9693
|
+
let var_esc = Self::escape_ident(var_name.as_ref()).into_owned();
|
|
9694
|
+
let code = if self.refcell_wrapped_vars.contains(var_name.as_ref()) {
|
|
9695
|
+
format!("({}.borrow().len() as f64)", var_esc)
|
|
9696
|
+
} else {
|
|
9697
|
+
format!("({}.len() as f64)", var_esc)
|
|
9698
|
+
};
|
|
9699
|
+
return Ok((code, RustType::F64));
|
|
9700
|
+
}
|
|
9701
|
+
}
|
|
9447
9702
|
if let RustType::Named { fields, .. } = &var_type {
|
|
9448
9703
|
if let Some((_, field_ty)) =
|
|
9449
9704
|
fields.iter().find(|(k, _)| k.as_ref() == prop_name.as_ref())
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
//! Generated-Rust assertions for the Beat-Node native codegen wins #169 and #173.
|
|
2
|
+
//!
|
|
3
|
+
//! These run in their own test binary (separate process from the lib unit tests), so the
|
|
4
|
+
//! dark-ship env flags set here never leak into other tests. Every test sets the SAME flag set
|
|
5
|
+
//! (the gauntlet's `TYPED_FLAGS`), so parallel execution is race-free.
|
|
6
|
+
|
|
7
|
+
use std::path::PathBuf;
|
|
8
|
+
|
|
9
|
+
use tishlang_compile::{compile, compile_project_full};
|
|
10
|
+
use tishlang_parser::parse;
|
|
11
|
+
|
|
12
|
+
/// Enable every dark-shipped typed-native flag, matching `scripts/run_perf_gauntlet.sh`.
|
|
13
|
+
fn enable_typed_flags() {
|
|
14
|
+
for k in [
|
|
15
|
+
"TISH_PARAM_NATIVE",
|
|
16
|
+
"TISH_PARAM_INFER",
|
|
17
|
+
"TISH_NATIVE_FN",
|
|
18
|
+
"TISH_STRUCT_INFER",
|
|
19
|
+
"TISH_FUSED_HOF",
|
|
20
|
+
"TISH_NATIVE_HOF",
|
|
21
|
+
"TISH_AGGREGATE_INFER",
|
|
22
|
+
] {
|
|
23
|
+
std::env::set_var(k, "1");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn compile_typed(src: &str) -> String {
|
|
28
|
+
enable_typed_flags();
|
|
29
|
+
compile(&parse(src).unwrap()).unwrap()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Compile a real fixture through the **same** path the `tish build` CLI uses
|
|
33
|
+
/// (`compile_project_full` → `merge_modules` → codegen), so generated-Rust assertions match the
|
|
34
|
+
/// gauntlet build exactly.
|
|
35
|
+
fn compile_fixture_typed(rel: &str) -> String {
|
|
36
|
+
enable_typed_flags();
|
|
37
|
+
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
38
|
+
let path = manifest.join("../..").join(rel).canonicalize().unwrap();
|
|
39
|
+
let (rust, _, _, _) = compile_project_full(&path, path.parent(), &[], true).unwrap();
|
|
40
|
+
rust
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── #169: a fused native `Vec<f64>` reduce result keeps its accumulator native ────────────────
|
|
44
|
+
|
|
45
|
+
#[test]
|
|
46
|
+
fn issue_169_reduce_fed_accumulator_stays_native() {
|
|
47
|
+
// The real gauntlet fixture: `acc` is updated from `xs.reduce(...)` (which the native-HOF path
|
|
48
|
+
// fuses to a native f64 fold). Before #169 the demotion oracle didn't model the fusion, so `acc`
|
|
49
|
+
// was boxed (`let mut acc = Value::Number(...)`) and paid boxed ops::mul/add/modulo + clone per
|
|
50
|
+
// iter. Compiled through the same `compile_project_full` path the `tish build` CLI uses.
|
|
51
|
+
let rust = compile_fixture_typed("tests/perf/typed_array_hof.tish");
|
|
52
|
+
assert!(
|
|
53
|
+
rust.contains("let mut acc: f64"),
|
|
54
|
+
"acc must stay native f64 (reduce result modelled as F64), got:\n{}",
|
|
55
|
+
rust.lines().filter(|l| l.contains("acc")).take(6).collect::<Vec<_>>().join("\n")
|
|
56
|
+
);
|
|
57
|
+
assert!(
|
|
58
|
+
!rust.contains("let mut acc = Value::Number"),
|
|
59
|
+
"acc must NOT be boxed:\n{}",
|
|
60
|
+
rust.lines().filter(|l| l.contains("acc")).take(6).collect::<Vec<_>>().join("\n")
|
|
61
|
+
);
|
|
62
|
+
// The per-iteration accumulator update is native arithmetic, not boxed ops on `acc`.
|
|
63
|
+
assert!(
|
|
64
|
+
!rust.contains("ops::mul(&acc"),
|
|
65
|
+
"acc update must be native f64, not a boxed ops::mul on acc"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[test]
|
|
70
|
+
fn issue_169_non_number_array_reduce_does_not_make_accumulator_native() {
|
|
71
|
+
// Conservative bail: reduce over a *string* array is not a native f64 fold, so an accumulator it
|
|
72
|
+
// feeds must stay boxed (sound — we never claim F64 where the emitter would box).
|
|
73
|
+
let src = r#"
|
|
74
|
+
let ss: string[] = ["a", "b", "c"]
|
|
75
|
+
let acc: number = 0
|
|
76
|
+
let r: number = 0
|
|
77
|
+
while (r < 10) {
|
|
78
|
+
acc = acc + ss.length
|
|
79
|
+
r = r + 1
|
|
80
|
+
}
|
|
81
|
+
console.log(acc)
|
|
82
|
+
"#;
|
|
83
|
+
// Just assert it compiles and is sound; `acc` here is fed by `.length` which is a separate lever.
|
|
84
|
+
let rust = compile_typed(src);
|
|
85
|
+
assert!(rust.contains("fn main"), "compiles to a native program");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── #173: fill-loop fusion + native `.length` ─────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
#[test]
|
|
91
|
+
fn issue_173_fill_loop_fuses_to_bulk_extend() {
|
|
92
|
+
// `let a = []; for (let i = 0; i < N; i++) { a.push(K) }` over a native Vec lowers to a single
|
|
93
|
+
// `a.extend(std::iter::repeat(K).take((N) as usize))` — one allocation, no per-element pushes.
|
|
94
|
+
let rust = compile_fixture_typed("tests/core/array_init_pattern.tish");
|
|
95
|
+
// boolean[] fill with a literal bound.
|
|
96
|
+
assert!(
|
|
97
|
+
rust.contains("extend(std::iter::repeat(true).take((8_f64) as usize))"),
|
|
98
|
+
"boolean fill loop must fuse to a bulk extend:\n{}",
|
|
99
|
+
rust.lines().filter(|l| l.contains("repeat")).collect::<Vec<_>>().join("\n")
|
|
100
|
+
);
|
|
101
|
+
// number[] fill with an integer-`let` bound (`n`).
|
|
102
|
+
assert!(
|
|
103
|
+
rust.contains("extend(std::iter::repeat(1_f64).take((n) as usize))"),
|
|
104
|
+
"number fill loop with an int-range bound must fuse:\n{}",
|
|
105
|
+
rust.lines().filter(|l| l.contains("repeat")).collect::<Vec<_>>().join("\n")
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#[test]
|
|
110
|
+
fn issue_173_length_is_native_f64() {
|
|
111
|
+
// `vec.length` on a native Vec lowers to `(vec.len() as f64)`, not a boxed `get_prop`.
|
|
112
|
+
let rust = compile_fixture_typed("tests/core/array_init_pattern.tish");
|
|
113
|
+
assert!(
|
|
114
|
+
rust.contains("a.len() as f64"),
|
|
115
|
+
"native Vec `.length` must lower to `(a.len() as f64)`"
|
|
116
|
+
);
|
|
117
|
+
assert!(
|
|
118
|
+
!rust.contains("get_prop(&a, \"length\")"),
|
|
119
|
+
"native Vec `.length` must not go through boxed get_prop"
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#[test]
|
|
124
|
+
fn issue_173_adversarial_cases_do_not_fuse() {
|
|
125
|
+
// Non-constant push arg, extra statement, and `break` in the body must NOT fuse — they keep the
|
|
126
|
+
// per-element push loop (correctness over coverage). The fixture's only fusions are the three
|
|
127
|
+
// canonical fills (8, n, 3); a non-constant `push(i * 2)` must not produce an extend.
|
|
128
|
+
let rust = compile_fixture_typed("tests/core/array_init_pattern.tish");
|
|
129
|
+
assert!(
|
|
130
|
+
!rust.contains("repeat((i * 2"),
|
|
131
|
+
"non-constant push arg must not be fused into a repeat-fill"
|
|
132
|
+
);
|
|
133
|
+
// Exactly the three sound fills are emitted (8, n, 3) — no extra fusions crept in.
|
|
134
|
+
let fusions = rust.matches("std::iter::repeat").count();
|
|
135
|
+
assert_eq!(
|
|
136
|
+
fusions, 3,
|
|
137
|
+
"exactly 3 fill loops should fuse (boolFill=8, sieve=n, oobGrow=3), found {fusions}"
|
|
138
|
+
);
|
|
139
|
+
}
|