@tishlang/tish-lint 3.3.3 → 3.7.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/Cargo.toml +1 -0
- package/bin/tish-lint +0 -0
- package/crates/tish/Cargo.toml +4 -1
- package/crates/tish/src/cli_help.rs +84 -0
- package/crates/tish/src/main.rs +93 -1
- package/crates/tish/tests/scope_merge.rs +90 -0
- package/crates/tish/tests/test_runner.rs +673 -0
- package/crates/tish_bytecode/src/compiler.rs +25 -4
- package/crates/tish_compile/src/check.rs +178 -0
- package/crates/tish_compile/src/codegen.rs +383 -63
- package/crates/tish_compile/src/lib.rs +33 -1
- package/crates/tish_compile/src/resolve.rs +121 -19
- package/crates/tish_compile/src/types.rs +35 -1
- package/crates/tish_compile/tests/call_arity_641.rs +104 -0
- package/crates/tish_core/src/lib.rs +103 -11
- package/crates/tish_eval/src/eval.rs +16 -0
- package/crates/tish_eval/src/value_convert.rs +10 -0
- package/crates/tish_lint/Cargo.toml +1 -1
- package/crates/tish_test/Cargo.toml +47 -0
- package/crates/tish_test/src/assert.rs +435 -0
- package/crates/tish_test/src/config.rs +85 -0
- package/crates/tish_test/src/coverage.rs +237 -0
- package/crates/tish_test/src/deep_equal.rs +261 -0
- package/crates/tish_test/src/discovery.rs +70 -0
- package/crates/tish_test/src/expect.rs +977 -0
- package/crates/tish_test/src/instrument.rs +457 -0
- package/crates/tish_test/src/isolation.rs +14 -0
- package/crates/tish_test/src/lib.rs +22 -0
- package/crates/tish_test/src/load.rs +152 -0
- package/crates/tish_test/src/mocks.rs +270 -0
- package/crates/tish_test/src/module.rs +318 -0
- package/crates/tish_test/src/registry.rs +235 -0
- package/crates/tish_test/src/report.rs +312 -0
- package/crates/tish_test/src/runner.rs +797 -0
- package/crates/tish_test/src/snapshots.rs +197 -0
- package/crates/tish_vm/src/vm.rs +17 -14
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lint +0 -0
- package/platform/darwin-x64/tish-lint +0 -0
- package/platform/linux-arm64/tish-lint +0 -0
- package/platform/linux-x64/tish-lint +0 -0
- package/platform/win32-x64/tish-lint.exe +0 -0
|
@@ -705,6 +705,28 @@ pub fn compile_with_native_modules(
|
|
|
705
705
|
/// as warnings; `TISH_CHECK=error` also fails the build. Unset/`0` → no-op (default builds are
|
|
706
706
|
/// unaffected). The checker is gradual (see `check.rs`): it never flags code it can't prove wrong.
|
|
707
707
|
fn run_type_check(program: &Program) -> Result<(), CompileError> {
|
|
708
|
+
// #641: call arity is checked UNCONDITIONALLY, ahead of the opt-in gradual checker. It cannot
|
|
709
|
+
// sit behind `TISH_CHECK` — the case this exists for built clean and shipped, and a diagnostic
|
|
710
|
+
// nobody enables would not have caught it. It is also not "gradual": an omitted argument bound
|
|
711
|
+
// to a typed parameter arrives as `Value::Null` and cannot satisfy the typed prologue, so it is
|
|
712
|
+
// provably wrong on every backend rather than an unproven annotation.
|
|
713
|
+
//
|
|
714
|
+
// `TISH_ARITY=0` is the escape hatch, for bisecting a build against pre-#641 behaviour.
|
|
715
|
+
if std::env::var("TISH_ARITY").as_deref() != Ok("0") {
|
|
716
|
+
let arity = crate::check::check_call_arity(program);
|
|
717
|
+
if let Some(first) = arity.first() {
|
|
718
|
+
for d in &arity {
|
|
719
|
+
eprintln!(
|
|
720
|
+
"tish error: {}:{}: {}",
|
|
721
|
+
d.span.start.0, d.span.start.1, d.message
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
return Err(CompileError::new(
|
|
725
|
+
format!("call arity: {} error(s)", arity.len()),
|
|
726
|
+
Some(first.span),
|
|
727
|
+
));
|
|
728
|
+
}
|
|
729
|
+
}
|
|
708
730
|
let mode = std::env::var("TISH_CHECK").unwrap_or_default();
|
|
709
731
|
if mode.is_empty() || mode == "0" {
|
|
710
732
|
return Ok(());
|
|
@@ -1067,7 +1089,11 @@ pub(crate) struct Codegen {
|
|
|
1067
1089
|
native_ns_locals: std::collections::HashMap<String, String>,
|
|
1068
1090
|
/// Typed native imports (`declare fun` + a native import): tish name -> its typed signature.
|
|
1069
1091
|
/// A matching call emits a DIRECT `<crate>::name_typed(..)` instead of a boxed `value_call`.
|
|
1070
|
-
|
|
1092
|
+
/// #615: MULTIPLE signatures per name, dispatched on EXACT arity. An extern with optional
|
|
1093
|
+
/// trailing args (`ui_text` 4-10, `ui_rect` 5 or 6) could previously be typed for one arity
|
|
1094
|
+
/// only; every other call site stayed boxed. Additive by construction: an arity with no
|
|
1095
|
+
/// declaration finds no match and keeps the boxed path bit-identically.
|
|
1096
|
+
extern_fns: std::collections::HashMap<String, Vec<ExternSig>>,
|
|
1071
1097
|
/// User tish functions whose declared return type is a native STRUCT (`function f(): Iface`).
|
|
1072
1098
|
/// A call in a typed context coerces the boxed result to the native struct (a `Value::Struct`
|
|
1073
1099
|
/// downcast on GBA), so `f().field` reads are native instead of `get_prop`. Name -> `RustType::Named`.
|
|
@@ -1234,7 +1260,22 @@ pub(crate) struct Codegen {
|
|
|
1234
1260
|
/// once in `emit_program` (after type aliases + `native_fns`), consulted at `VarDecl` to force
|
|
1235
1261
|
/// `RustType::Value`. This is the rust-AOT analogue of the VM array-JIT bailing to the
|
|
1236
1262
|
/// interpreter on a non-numeric element. See `collect_demoted_numeric_locals`.
|
|
1237
|
-
|
|
1263
|
+
///
|
|
1264
|
+
/// #614: keyed by `(declaring scope, name)`, NOT by bare name. The scope is the top-level fn
|
|
1265
|
+
/// whose body declares the local, or `""` for a module-level binding. Flat keying meant one
|
|
1266
|
+
/// boxed `i` anywhere in the bundle demoted every `i` in every module — with 44 of tish-gba's
|
|
1267
|
+
/// 70 packages untyped, a legacy package silently de-optimized a fully-modernized one that
|
|
1268
|
+
/// merely shared a loop-variable name (~40-60 cycles per boxed use against 2-4 native).
|
|
1269
|
+
///
|
|
1270
|
+
/// Module-level bindings stay program-wide (scope `""`) ON PURPOSE: a module `let` can be
|
|
1271
|
+
/// reassigned from inside any fn, so its demotion is genuinely a whole-program property.
|
|
1272
|
+
/// Only FUNCTION-LOCAL names are scoped, and for those every reassignment necessarily lives
|
|
1273
|
+
/// in the declaring fn (or a closure nested in it) — exactly the region analyzed.
|
|
1274
|
+
demoted_numeric_locals: std::collections::HashSet<(String, String)>,
|
|
1275
|
+
/// #614: the top-level fn currently being emitted (`""` at module level), used to key lookups
|
|
1276
|
+
/// into `demoted_numeric_locals`. A stack because fn emission nests; the FIRST entry is the
|
|
1277
|
+
/// top-level fn, which is the granularity the collector analyzes.
|
|
1278
|
+
demote_scope_stack: Vec<String>,
|
|
1238
1279
|
/// Top-level fns whose EVERY return is provably numeric → the native type a call to one yields.
|
|
1239
1280
|
///
|
|
1240
1281
|
/// Without it a call is opaque, so a local assigned from one demotes to a boxed `Value` and
|
|
@@ -1434,6 +1475,7 @@ impl Codegen {
|
|
|
1434
1475
|
aggregate_array_locals: std::collections::HashSet::new(),
|
|
1435
1476
|
agg_cur_ret: None,
|
|
1436
1477
|
demoted_numeric_locals: std::collections::HashSet::new(),
|
|
1478
|
+
demote_scope_stack: Vec::new(),
|
|
1437
1479
|
user_fn_ret: std::collections::HashMap::new(),
|
|
1438
1480
|
int_range_locals: std::collections::HashMap::new(),
|
|
1439
1481
|
diag_coord_indices: std::collections::HashSet::new(),
|
|
@@ -2989,13 +3031,35 @@ impl Codegen {
|
|
|
2989
3031
|
.as_ref()
|
|
2990
3032
|
.map(RustType::from_annotation)
|
|
2991
3033
|
.unwrap_or(RustType::Unit);
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
);
|
|
3034
|
+
// #615: one name may carry several arities. A REDECLARATION of an arity already seen
|
|
3035
|
+
// replaces it (last wins), matching the previous single-map behaviour for the
|
|
3036
|
+
// single-arity case exactly.
|
|
3037
|
+
let sig = ExternSig { crate_name, symbol: export_name.clone(), params: ptys, ret };
|
|
3038
|
+
let slot = self.extern_fns.entry(name.to_string()).or_default();
|
|
3039
|
+
if let Some(existing) = slot.iter_mut().find(|e| e.params.len() == sig.params.len()) {
|
|
3040
|
+
*existing = sig;
|
|
3041
|
+
} else {
|
|
3042
|
+
slot.push(sig);
|
|
3043
|
+
}
|
|
2996
3044
|
}
|
|
2997
3045
|
}
|
|
2998
3046
|
|
|
3047
|
+
/// #615: the declared signature for `name` at EXACTLY `argc` arguments, plus the Rust symbol
|
|
3048
|
+
/// suffix to call it by. A name with a single declaration keeps the historical `<symbol>_typed`
|
|
3049
|
+
/// spelling, so every existing extension crate links unchanged; only a name that declares more
|
|
3050
|
+
/// than one arity uses `<symbol>_typed_<arity>`, which is the new surface a crate opts into by
|
|
3051
|
+
/// declaring the extra arities in the first place.
|
|
3052
|
+
fn extern_sig_for_arity(&self, name: &str, argc: usize) -> Option<(ExternSig, String)> {
|
|
3053
|
+
let sigs = self.extern_fns.get(name)?;
|
|
3054
|
+
let sig = sigs.iter().find(|s| s.params.len() == argc)?.clone();
|
|
3055
|
+
let suffix = if sigs.len() > 1 {
|
|
3056
|
+
format!("_typed_{}", argc)
|
|
3057
|
+
} else {
|
|
3058
|
+
"_typed".to_string()
|
|
3059
|
+
};
|
|
3060
|
+
Some((sig, suffix))
|
|
3061
|
+
}
|
|
3062
|
+
|
|
2999
3063
|
/// Record every top-level `function f(...): Iface` whose return annotation resolves — via the type
|
|
3000
3064
|
/// aliases — to a native STRUCT (`RustType::Named`, or a `ShapeUnion`). A later call to `f` in a
|
|
3001
3065
|
/// typed position then coerces the boxed result to the native struct (a `Value::Struct` downcast on
|
|
@@ -5937,9 +6001,26 @@ impl Codegen {
|
|
|
5937
6001
|
// `s = s + arr[i]`, JS string concat) must stay a boxed `Value` — a native-f64
|
|
5938
6002
|
// store would panic at the `from_value_expr(F64)` coercion. See
|
|
5939
6003
|
// `demoted_numeric_locals`.
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
6004
|
+
// #614: look up (declaring scope, name). A module-level binding was decided
|
|
6005
|
+
// program-wide under scope `""`; a function-local under its top-level fn. Both are
|
|
6006
|
+
// checked because this same emission path serves module code and fn bodies, and a
|
|
6007
|
+
// fn-local name may legitimately shadow a module one.
|
|
6008
|
+
// #614: look up (declaring scope, name). A fn-local is keyed under its top-level
|
|
6009
|
+
// fn; a module binding under `""`. Both are consulted because this same emission
|
|
6010
|
+
// path serves module code and fn bodies.
|
|
6011
|
+
// #614: key by the scope this declaration belongs to. No module fallback — the
|
|
6012
|
+
// statement being emitted IS a declaration, so the variable is by definition owned
|
|
6013
|
+
// by the current scope: inside a fn it is that fn's local (`scope` = the top-level
|
|
6014
|
+
// fn), at module level `scope` is `""`, which is the module key itself. Consulting
|
|
6015
|
+
// the module verdict for a fn-local is exactly the false positive this fixes.
|
|
6016
|
+
if rust_type == RustType::F64 {
|
|
6017
|
+
let scope = self.demote_scope_stack.first().cloned().unwrap_or_default();
|
|
6018
|
+
if self
|
|
6019
|
+
.demoted_numeric_locals
|
|
6020
|
+
.contains(&(scope, name.to_string()))
|
|
6021
|
+
{
|
|
6022
|
+
rust_type = RustType::Value;
|
|
6023
|
+
}
|
|
5943
6024
|
}
|
|
5944
6025
|
|
|
5945
6026
|
// Native-vec call initializer: `let cum = cumulative(probs)` → `Vec<f64>`.
|
|
@@ -7100,6 +7181,45 @@ impl Codegen {
|
|
|
7100
7181
|
sibling_escaped, sibling_escaped
|
|
7101
7182
|
));
|
|
7102
7183
|
}
|
|
7184
|
+
// #563: a fn from an OUTER function scope is not a sibling, so the `_ref`/`_cell`
|
|
7185
|
+
// path above never fires for it — but the enclosing frame already materialized it
|
|
7186
|
+
// as a plain `Value` local (module fns and imports arrive that way, via the
|
|
7187
|
+
// `let f = (*f_ref.borrow()).clone();` prelude). A `move` closure therefore MOVES
|
|
7188
|
+
// it, and the SECOND nested fn that names it fails:
|
|
7189
|
+
//
|
|
7190
|
+
// error[E0382]: use of moved value: `uiRowText`
|
|
7191
|
+
//
|
|
7192
|
+
// One nested fn compiles fine, which is why this reads as "nested functions are
|
|
7193
|
+
// broken" rather than "the second one is". Clone it in, exactly as the builtin list
|
|
7194
|
+
// below does for `console`/`Math` — same hazard, same remedy; that list is just a
|
|
7195
|
+
// hardcoded enumeration of the names that happened to hit it first.
|
|
7196
|
+
//
|
|
7197
|
+
// Scope-stack order matters: `.last()` is the current (sibling) scope and is
|
|
7198
|
+
// already handled, and scope 0 is module level. Anything in between is an
|
|
7199
|
+
// enclosing function's scope — those are the locals at risk.
|
|
7200
|
+
let outer_scope_fns: Vec<String> = {
|
|
7201
|
+
let depth = self.function_scope_stack.len();
|
|
7202
|
+
let mut seen = std::collections::HashSet::new();
|
|
7203
|
+
let mut out = Vec::new();
|
|
7204
|
+
for scope in self.function_scope_stack.iter().take(depth.saturating_sub(1)) {
|
|
7205
|
+
for f in scope {
|
|
7206
|
+
if f.as_str() != name_raw
|
|
7207
|
+
&& referenced.contains(f.as_str())
|
|
7208
|
+
&& !sibling_fns.iter().any(|s| s == f)
|
|
7209
|
+
&& !outer_vars.iter().any(|v| v == f)
|
|
7210
|
+
&& !outer_params.iter().any(|p| p == f)
|
|
7211
|
+
&& seen.insert(f.clone())
|
|
7212
|
+
{
|
|
7213
|
+
out.push(f.clone());
|
|
7214
|
+
}
|
|
7215
|
+
}
|
|
7216
|
+
}
|
|
7217
|
+
out
|
|
7218
|
+
};
|
|
7219
|
+
for f in &outer_scope_fns {
|
|
7220
|
+
let escaped = Self::escape_ident(f);
|
|
7221
|
+
self.writeln(&format!("let {} = {}.clone();", escaped, escaped));
|
|
7222
|
+
}
|
|
7103
7223
|
// Clone outer parameters so they can be captured by the move closure
|
|
7104
7224
|
for outer_param in &outer_params {
|
|
7105
7225
|
let param_escaped = Self::escape_ident(outer_param);
|
|
@@ -7454,9 +7574,12 @@ impl Codegen {
|
|
|
7454
7574
|
for v in &body_cell_vars {
|
|
7455
7575
|
self.refcell_wrapped_vars.insert(v.clone());
|
|
7456
7576
|
}
|
|
7577
|
+
// #614: this body's locals were analyzed under THIS fn's scope.
|
|
7578
|
+
self.demote_scope_stack.push(name_raw.to_string());
|
|
7457
7579
|
for s in statements {
|
|
7458
7580
|
self.emit_statement(s)?;
|
|
7459
7581
|
}
|
|
7582
|
+
self.demote_scope_stack.pop();
|
|
7460
7583
|
for v in &body_cell_vars {
|
|
7461
7584
|
self.refcell_wrapped_vars.remove(v);
|
|
7462
7585
|
}
|
|
@@ -7468,7 +7591,9 @@ impl Codegen {
|
|
|
7468
7591
|
self.outer_vars_stack.push(Vec::new());
|
|
7469
7592
|
self.rc_cell_storage_scopes
|
|
7470
7593
|
.push(std::collections::HashSet::new());
|
|
7594
|
+
self.demote_scope_stack.push(name_raw.to_string()); // #614
|
|
7471
7595
|
self.emit_statement(body)?;
|
|
7596
|
+
self.demote_scope_stack.pop();
|
|
7472
7597
|
self.function_scope_stack.pop();
|
|
7473
7598
|
self.outer_vars_stack.pop();
|
|
7474
7599
|
self.rc_cell_storage_scopes.pop();
|
|
@@ -9229,10 +9354,10 @@ impl Codegen {
|
|
|
9229
9354
|
// `value_call` dispatch through the namespace object. Falls through to the boxed path
|
|
9230
9355
|
// when the arg count/kind doesn't match (a dynamic/spread call stays correct).
|
|
9231
9356
|
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
9232
|
-
if let Some(sig
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
{
|
|
9357
|
+
if let Some((sig, suffix)) =
|
|
9358
|
+
self.extern_sig_for_arity(fname.as_ref(), args.len())
|
|
9359
|
+
{
|
|
9360
|
+
if args.iter().all(|a| matches!(a, CallArg::Expr(_))) {
|
|
9236
9361
|
let mut argc: Vec<String> = Vec::with_capacity(args.len());
|
|
9237
9362
|
for (a, ty) in args.iter().zip(sig.params.iter()) {
|
|
9238
9363
|
if let CallArg::Expr(e) = a {
|
|
@@ -9240,9 +9365,10 @@ impl Codegen {
|
|
|
9240
9365
|
}
|
|
9241
9366
|
}
|
|
9242
9367
|
let call = format!(
|
|
9243
|
-
"{}::{}
|
|
9368
|
+
"{}::{}{}({})",
|
|
9244
9369
|
sig.crate_name,
|
|
9245
9370
|
sig.symbol,
|
|
9371
|
+
suffix,
|
|
9246
9372
|
argc.join(", ")
|
|
9247
9373
|
);
|
|
9248
9374
|
// This is a Value-producing context: a `void` extern yields `Null`; a
|
|
@@ -12575,9 +12701,16 @@ impl Codegen {
|
|
|
12575
12701
|
/// function that mixes the two. A function whose whole signature is `i32` has one convention
|
|
12576
12702
|
/// and keeps the coercion at the boundary, where the caller does it once on an argument that is
|
|
12577
12703
|
/// usually already an integer.
|
|
12704
|
+
///
|
|
12705
|
+
/// #647: ZERO PARAMETERS QUALIFY. The rule above is about keeping ONE coercion convention
|
|
12706
|
+
/// across a signature — and a signature with no parameters has nothing to be inconsistent
|
|
12707
|
+
/// about, so the reason the rule exists cannot apply to it. Excluding it cost every niladic
|
|
12708
|
+
/// accessor (`rCols()`, `rPocket()`, `atkLines()`) a boxed `value_call`: measured at 55 ticks
|
|
12709
|
+
/// against ~0 for the identical body taking one argument. The body proof still runs, so a
|
|
12710
|
+
/// zero-arg fn that touches something unlowerable is rejected on its body, as before — this
|
|
12711
|
+
/// only stops rejecting it on its (empty) parameter list.
|
|
12578
12712
|
fn fn_sig_all_i32(params: &[FunParam], return_type: &Option<TypeAnnotation>) -> bool {
|
|
12579
12713
|
return_type.as_ref().is_some_and(Self::ann_is_i32)
|
|
12580
|
-
&& !params.is_empty()
|
|
12581
12714
|
&& params.iter().all(|p| {
|
|
12582
12715
|
matches!(p, FunParam::Simple(tp)
|
|
12583
12716
|
if tp.default.is_none()
|
|
@@ -12842,23 +12975,105 @@ impl Codegen {
|
|
|
12842
12975
|
}
|
|
12843
12976
|
}
|
|
12844
12977
|
|
|
12845
|
-
fn collect_demoted_numeric_locals(&self, stmts: &[Statement]) -> HashSet<String> {
|
|
12846
|
-
//
|
|
12978
|
+
fn collect_demoted_numeric_locals(&self, stmts: &[Statement]) -> HashSet<(String, String)> {
|
|
12979
|
+
// #614: the analysis below is UNCHANGED — same env, same reassignment set, same fixpoint.
|
|
12980
|
+
// What changed is the KEY. Previously a bare name, so one boxed `i` anywhere in the bundle
|
|
12981
|
+
// demoted every `i` in every module (~40-60 cycles per boxed use against 2-4 native); with
|
|
12982
|
+
// 44 of tish-gba's 70 packages untyped, a legacy package silently de-optimized a fully
|
|
12983
|
+
// modernized one that merely shared a loop-variable name.
|
|
12984
|
+
//
|
|
12985
|
+
// Scope granularity is the TOP-LEVEL FN, or `""` for module level. Each fn is analyzed from
|
|
12986
|
+
// its own `FunDecl` statement — not just its body — so params (including a REST param, whose
|
|
12987
|
+
// `Vec<f64>` element type is what keeps `total` native in `typed_assign_conversion`) are
|
|
12988
|
+
// collected exactly as the flat pass collected them. Nested fns fold into their enclosing
|
|
12989
|
+
// top-level fn: still conservative, but bounded to one function instead of the whole bundle.
|
|
12990
|
+
let mut out: HashSet<(String, String)> = HashSet::new();
|
|
12991
|
+
|
|
12992
|
+
// Module-level names keep the program-wide verdict: a module `let` is reassignable from
|
|
12993
|
+
// inside any fn, so whether it can go non-numeric is genuinely a whole-program property.
|
|
12994
|
+
let module_names: HashSet<String> = stmts
|
|
12995
|
+
.iter()
|
|
12996
|
+
.filter_map(|s| match s {
|
|
12997
|
+
Statement::VarDecl { name, .. } => Some(name.to_string()),
|
|
12998
|
+
_ => None,
|
|
12999
|
+
})
|
|
13000
|
+
.collect();
|
|
13001
|
+
|
|
12847
13002
|
let mut env: HashMap<String, RustType> = HashMap::new();
|
|
12848
13003
|
Self::collect_annotated_types(stmts, &self.type_aliases, &mut env);
|
|
12849
|
-
// 2. Every reassignment `(name, rhs)` anywhere in the program (incl. nested exprs/closures).
|
|
12850
13004
|
let mut reassigns: Vec<(String, &Expr)> = Vec::new();
|
|
12851
13005
|
Self::collect_reassignments_stmts(stmts, &mut reassigns);
|
|
12852
|
-
|
|
13006
|
+
for name in self.demote_fixpoint(&mut env, &reassigns, &module_names) {
|
|
13007
|
+
out.insert((String::new(), name));
|
|
13008
|
+
}
|
|
13009
|
+
|
|
13010
|
+
// A fn's env must START from what MODULE scope makes visible to it. Without that, a body
|
|
13011
|
+
// reading a module binding — `sum = (sum + codes[j]) % M`, where `codes` is a hoisted const
|
|
13012
|
+
// f64 array — cannot type the RHS, wrongly demotes `sum` to `Value`, and the emitted code
|
|
13013
|
+
// then adds an `f64` static to a `Value`: `error[E0369]: cannot add f64 to Value`. The flat
|
|
13014
|
+
// pass had this information for free; scoping the KEY must not scope away the VISIBILITY.
|
|
13015
|
+
//
|
|
13016
|
+
// Module-only, and carrying the module verdicts just settled above: a demoted module binding
|
|
13017
|
+
// reads as `Value` inside a fn too, which is exactly what its declaration will emit.
|
|
13018
|
+
let mut module_env: HashMap<String, RustType> = HashMap::new();
|
|
13019
|
+
for s in stmts {
|
|
13020
|
+
if !matches!(s, Statement::FunDecl { .. }) {
|
|
13021
|
+
Self::collect_annotated_types(std::slice::from_ref(s), &self.type_aliases, &mut module_env);
|
|
13022
|
+
}
|
|
13023
|
+
}
|
|
13024
|
+
for (scope, n) in &out {
|
|
13025
|
+
if scope.is_empty() {
|
|
13026
|
+
module_env.insert(n.clone(), RustType::Value);
|
|
13027
|
+
}
|
|
13028
|
+
}
|
|
13029
|
+
|
|
13030
|
+
for s in stmts {
|
|
13031
|
+
let Statement::FunDecl { name, .. } = s else {
|
|
13032
|
+
continue;
|
|
13033
|
+
};
|
|
13034
|
+
let scope = name.to_string();
|
|
13035
|
+
let one = std::slice::from_ref(s);
|
|
13036
|
+
// Module bindings first, then this fn's own declarations OVERLAY them — so a local that
|
|
13037
|
+
// shadows a module name takes its own type, not the module one.
|
|
13038
|
+
// What this fn itself declares — computed separately from the visibility env, because
|
|
13039
|
+
// only these names may take a verdict under this scope. INCLUDES a name that shadows a
|
|
13040
|
+
// module binding: a shadowing local is a different variable and gets its own verdict;
|
|
13041
|
+
// leaving it out drops it back onto the module's verdict, which is the bug being fixed.
|
|
13042
|
+
let mut own: HashMap<String, RustType> = HashMap::new();
|
|
13043
|
+
Self::collect_annotated_types(one, &self.type_aliases, &mut own);
|
|
13044
|
+
let local_names: HashSet<String> = own.keys().cloned().collect();
|
|
13045
|
+
let mut fenv: HashMap<String, RustType> = module_env.clone();
|
|
13046
|
+
fenv.extend(own);
|
|
13047
|
+
if local_names.is_empty() {
|
|
13048
|
+
continue;
|
|
13049
|
+
}
|
|
13050
|
+
let mut freassigns: Vec<(String, &Expr)> = Vec::new();
|
|
13051
|
+
Self::collect_reassignments_stmts(one, &mut freassigns);
|
|
13052
|
+
for n in self.demote_fixpoint(&mut fenv, &freassigns, &local_names) {
|
|
13053
|
+
out.insert((scope.clone(), n));
|
|
13054
|
+
}
|
|
13055
|
+
}
|
|
13056
|
+
out
|
|
13057
|
+
}
|
|
13058
|
+
|
|
13059
|
+
/// #614: the demotion fixpoint, factored out so the module scope and every function scope run
|
|
13060
|
+
/// the IDENTICAL rule. Only names in `considered` may demote; `env` is updated in place so a
|
|
13061
|
+
/// demoted name's later reads see `Value` — that feedback is what makes it a fixpoint.
|
|
13062
|
+
fn demote_fixpoint(
|
|
13063
|
+
&self,
|
|
13064
|
+
env: &mut HashMap<String, RustType>,
|
|
13065
|
+
reassigns: &[(String, &Expr)],
|
|
13066
|
+
considered: &HashSet<String>,
|
|
13067
|
+
) -> HashSet<String> {
|
|
12853
13068
|
let mut demoted: HashSet<String> = HashSet::new();
|
|
12854
13069
|
loop {
|
|
12855
13070
|
let mut changed = false;
|
|
12856
|
-
for (name, rhs) in
|
|
12857
|
-
if demoted.contains(name) {
|
|
13071
|
+
for (name, rhs) in reassigns {
|
|
13072
|
+
if demoted.contains(name) || !considered.contains(name) {
|
|
12858
13073
|
continue;
|
|
12859
13074
|
}
|
|
12860
13075
|
if env.get(name) == Some(&RustType::F64)
|
|
12861
|
-
&& self.expr_native_type(rhs,
|
|
13076
|
+
&& self.expr_native_type(rhs, env) != RustType::F64
|
|
12862
13077
|
{
|
|
12863
13078
|
demoted.insert(name.clone());
|
|
12864
13079
|
env.insert(name.clone(), RustType::Value);
|
|
@@ -13070,8 +13285,28 @@ impl Codegen {
|
|
|
13070
13285
|
}
|
|
13071
13286
|
|
|
13072
13287
|
fn index_in_bounds(&self, index: &Expr, arr: &str) -> bool {
|
|
13073
|
-
|
|
13074
|
-
|
|
13288
|
+
// #645: a PROMOTED module-const array knows its own length unconditionally — it is a
|
|
13289
|
+
// `const G_X: [i32; N]`, fixed at emit time. `vec_fixed_len` is per-scope inference and is
|
|
13290
|
+
// NOT populated for it in every body, so the SAME expression proved in bounds inside a
|
|
13291
|
+
// function's native twin and failed inside its boxed closure — the twin got a direct
|
|
13292
|
+
// integer read while the closure, which is the copy a boxed call actually runs, kept the
|
|
13293
|
+
// bounds-checked `f64`/NaN form. Falling back to the static's own length makes the proof
|
|
13294
|
+
// scope-independent, which is what a compile-time constant deserves.
|
|
13295
|
+
let owned_len;
|
|
13296
|
+
let len = match self.vec_fixed_len.get(arr) {
|
|
13297
|
+
Some(l) => l,
|
|
13298
|
+
None => match self
|
|
13299
|
+
.module_const_f64_arrays
|
|
13300
|
+
.get(arr)
|
|
13301
|
+
.map(|v| v.len())
|
|
13302
|
+
.filter(|n| *n > 0)
|
|
13303
|
+
{
|
|
13304
|
+
Some(n) => {
|
|
13305
|
+
owned_len = BoundKey::Const(n as i64);
|
|
13306
|
+
&owned_len
|
|
13307
|
+
}
|
|
13308
|
+
None => return false,
|
|
13309
|
+
},
|
|
13075
13310
|
};
|
|
13076
13311
|
let Expr::Ident { name: idx, .. } = index else {
|
|
13077
13312
|
if let Expr::Literal {
|
|
@@ -13087,6 +13322,32 @@ impl Codegen {
|
|
|
13087
13322
|
return true;
|
|
13088
13323
|
}
|
|
13089
13324
|
}
|
|
13325
|
+
// #645: a MASK bounds its result exactly. For any i32 `x` and a non-negative literal
|
|
13326
|
+
// mask `M`, `x & M` lies in `[0, M]` — the AND clears every bit above M's highest,
|
|
13327
|
+
// including the sign bit, so a negative `x` cannot escape the range either. That makes
|
|
13328
|
+
// `A[k & 63]` provably in bounds for a 64-element `A` without knowing anything about
|
|
13329
|
+
// `k`.
|
|
13330
|
+
//
|
|
13331
|
+
// Worth the special case because it is the idiomatic way to index a power-of-two table,
|
|
13332
|
+
// and without it such a read misses the proven-in-bounds path and falls to the
|
|
13333
|
+
// bounds-checked form — which returns `f64` (it must be able to answer NaN), dragging
|
|
13334
|
+
// an otherwise all-integer expression through an f64 round trip on EVERY element.
|
|
13335
|
+
if let Expr::Binary { op: BinOp::BitAnd, left, right, .. } = index {
|
|
13336
|
+
let mask_of = |e: &Expr| -> Option<i64> {
|
|
13337
|
+
if let Expr::Literal { value: Literal::Number(n), .. } = e {
|
|
13338
|
+
Self::int_literal_value(*n).filter(|v| *v >= 0)
|
|
13339
|
+
} else {
|
|
13340
|
+
None
|
|
13341
|
+
}
|
|
13342
|
+
};
|
|
13343
|
+
if let Some(m) = mask_of(right).or_else(|| mask_of(left)) {
|
|
13344
|
+
if let BoundKey::Const(len) = len {
|
|
13345
|
+
if m < *len {
|
|
13346
|
+
return true;
|
|
13347
|
+
}
|
|
13348
|
+
}
|
|
13349
|
+
}
|
|
13350
|
+
}
|
|
13090
13351
|
return self.sub_index_in_bounds(index, arr);
|
|
13091
13352
|
};
|
|
13092
13353
|
let idx = idx.as_ref();
|
|
@@ -15557,7 +15818,7 @@ impl Codegen {
|
|
|
15557
15818
|
/// statements (blocks, if, loops, switch, try, return/throw). `f` is responsible for recursing
|
|
15558
15819
|
/// into each expression's own subtree. Does NOT descend into nested fn-decl bodies (a different
|
|
15559
15820
|
/// lexical scope; a captured loop var would be RefCell-bailed before reaching here).
|
|
15560
|
-
fn for_each_stmt_expr(s: &Statement, f: &mut dyn FnMut(&Expr)) {
|
|
15821
|
+
pub(crate) fn for_each_stmt_expr(s: &Statement, f: &mut dyn FnMut(&Expr)) {
|
|
15561
15822
|
match s {
|
|
15562
15823
|
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
15563
15824
|
for st in statements {
|
|
@@ -20178,16 +20439,27 @@ impl Codegen {
|
|
|
20178
20439
|
matches!(op, UnaryOp::Neg | UnaryOp::Pos)
|
|
20179
20440
|
&& Self::inline_body_numeric(operand, params, self_name)
|
|
20180
20441
|
}
|
|
20181
|
-
|
|
20182
|
-
|
|
20183
|
-
|
|
20184
|
-
|
|
20185
|
-
|
|
20186
|
-
|
|
20187
|
-
|
|
20188
|
-
|
|
20189
|
-
|
|
20190
|
-
|
|
20442
|
+
// #637: A TERNARY IS NEVER INLINED. The substitution that rewrites a parameter to its
|
|
20443
|
+
// `_inl<N>_<param>` temporary reaches the condition but NOT the branches, so an inlined
|
|
20444
|
+
// ternary emits branches naming parameters that do not exist in the generated scope:
|
|
20445
|
+
//
|
|
20446
|
+
// if Value::Bool((_inl0_a > _inl0_b)).is_truthy() { a } else { b }
|
|
20447
|
+
// ^^^^^^^ substituted ^ ^ not substituted
|
|
20448
|
+
// error[E0425]: cannot find value `a` in this scope
|
|
20449
|
+
//
|
|
20450
|
+
// The set this arm used to admit was exactly the broken one. `inline_body_numeric`
|
|
20451
|
+
// accepts a bare param, so `flag ? 10 : 20` was inlined — and since the inliner binds
|
|
20452
|
+
// every param as `f64`, the emitter then asked an `f64` for `.is_truthy()`
|
|
20453
|
+
// (`error[E0614]: type f64 cannot be dereferenced`). A comparison cond such as
|
|
20454
|
+
// `a > b ? a : b` was never admitted here, because `Gt` is not in the arithmetic op list
|
|
20455
|
+
// above — which is why that shape compiled fine and only truthiness tests broke.
|
|
20456
|
+
//
|
|
20457
|
+
// So refusing outright REMOVES no working inline: every ternary this arm ever accepted
|
|
20458
|
+
// produced code that did not build from the second call site onward. Widening it instead
|
|
20459
|
+
// (admitting comparison conds) is what surfaced the branch-substitution gap, and fixing
|
|
20460
|
+
// that belongs with the substitution machinery, not here. Ternaries fall back to the
|
|
20461
|
+
// ordinary call: correct, and merely un-inlined.
|
|
20462
|
+
Expr::Conditional { .. } => false,
|
|
20191
20463
|
Expr::Call { callee, args, .. } => {
|
|
20192
20464
|
// Only 1-arg `Math.<intrinsic>` — never another fn (incl. self), to keep it a pure
|
|
20193
20465
|
// expression with no dispatch and no recursion.
|
|
@@ -21366,7 +21638,23 @@ impl Codegen {
|
|
|
21366
21638
|
//
|
|
21367
21639
|
// This does not close #594's other half — a lowered fn that can actually SEE module state,
|
|
21368
21640
|
// which needs those bindings emitted as statics on GBA too. It stops the miscompile.
|
|
21369
|
-
|
|
21641
|
+
// #633: THE SAME HOLE EXISTS OFF-GBA. The scoping above reasoned that off-target "the
|
|
21642
|
+
// statics DO exist" — true only for what those passes actually cover: numeric arrays and
|
|
21643
|
+
// numeric globals. A module binding of any OTHER shape (an array of ARRAYS, a string, an
|
|
21644
|
+
// object) gets no top-level home on ANY target, stays a `let` in `run()`, and is just as
|
|
21645
|
+
// unreachable from a free `fn name_nv` on host as on GBA:
|
|
21646
|
+
//
|
|
21647
|
+
// let COLS = [[1,2,3],[4,5]] // array of arrays → no static
|
|
21648
|
+
// function widths(){ let o=[]; …COLS[i].length…; return o }
|
|
21649
|
+
// widths(); widths() // ≥2 call sites → not inlined
|
|
21650
|
+
//
|
|
21651
|
+
// compiles to `error[E0425]: cannot find value COLS in this scope`. One call site hides it
|
|
21652
|
+
// (the fn is inlined and never lowered), which is why this surfaced as a cross-module
|
|
21653
|
+
// ordering bug in a large program rather than as the two-line case it actually is.
|
|
21654
|
+
//
|
|
21655
|
+
// The carve-out is what differs per target, not the reachability rule — so run the rejection
|
|
21656
|
+
// everywhere and let the per-target `remove` list below decide what still has a home.
|
|
21657
|
+
{
|
|
21370
21658
|
let mut module_bindings = Self::collect_module_level_binding_names(&program.statements);
|
|
21371
21659
|
// …EXCEPT the ones that now have a top-level home. Since #594 a module const numeric
|
|
21372
21660
|
// array is a `const` and a mutable numeric global is a `SingleCore` static, both
|
|
@@ -25342,6 +25630,21 @@ impl Codegen {
|
|
|
25342
25630
|
// in (-2^53, 2^53) and `c` a positive integer literal → `(x as i64) % c` instead
|
|
25343
25631
|
// of `fmod`. Bit-identical (x is exactly an integer in f64; Rust `%` and `fmod`
|
|
25344
25632
|
// both truncate toward zero), and far faster — the fmod in LCG/hash recurrences.
|
|
25633
|
+
// #606: a mixed `fixed`/`f64` pair types as `Fixed` (or `Bool` for a
|
|
25634
|
+
// comparison), so the f64 side must be LIFTED before the operator — agb's
|
|
25635
|
+
// `Num<i32,8>` has no `f64` overload. `coerce_native_arg` owns the scaling, so
|
|
25636
|
+
// a value crossing here reads identically to one crossing a Value boundary or
|
|
25637
|
+
// the literal fold.
|
|
25638
|
+
let (l, r) = if lt == RustType::Fixed && rt == RustType::F64 {
|
|
25639
|
+
let rc = self.coerce_native_arg(&r, RustType::F64, RustType::Fixed);
|
|
25640
|
+
(l, rc)
|
|
25641
|
+
} else if lt == RustType::F64 && rt == RustType::Fixed {
|
|
25642
|
+
let lc = self.coerce_native_arg(&l, RustType::F64, RustType::Fixed);
|
|
25643
|
+
(lc, r)
|
|
25644
|
+
} else {
|
|
25645
|
+
(l, r)
|
|
25646
|
+
};
|
|
25647
|
+
|
|
25345
25648
|
if matches!(op, BinOp::Mod) && result_ty == RustType::F64 {
|
|
25346
25649
|
if let Some(c) = Self::int_literal_value_of(right).filter(|&c| c > 0) {
|
|
25347
25650
|
if self.int_range(left, &self.int_range_locals).is_some() {
|
|
@@ -25510,20 +25813,15 @@ impl Codegen {
|
|
|
25510
25813
|
name.as_ref(),
|
|
25511
25814
|
) {
|
|
25512
25815
|
let in_bounds = self.index_in_bounds(index, name.as_ref());
|
|
25513
|
-
|
|
25514
|
-
|
|
25515
|
-
|
|
25516
|
-
|
|
25517
|
-
|
|
25518
|
-
|
|
25519
|
-
|
|
25520
|
-
|
|
25521
|
-
|
|
25522
|
-
format!(
|
|
25523
|
-
"{{ let _i = &{}; if let Value::Number(n) = _i {{ *n as usize }} else {{ panic!(\"array index must be a number\") }} }}",
|
|
25524
|
-
iv
|
|
25525
|
-
)
|
|
25526
|
-
};
|
|
25816
|
+
// #645: use the SHARED index lowering. This site used to accept only
|
|
25817
|
+
// `F64` natively and push everything else — including a perfectly good
|
|
25818
|
+
// integer expression — through `Value::Number(..)` and back, or through
|
|
25819
|
+
// an `i32 -> f64 -> usize` cast. On a target with no FPU that cast is a
|
|
25820
|
+
// soft-float call PER ELEMENT, which is most of what made a promoted
|
|
25821
|
+
// array's read expensive. `emit_index_usize` already has the int-domain
|
|
25822
|
+
// shortcut (`(int).max(0) as usize`, where `.max(0)` reproduces f64's
|
|
25823
|
+
// saturate-to-zero for a negative index).
|
|
25824
|
+
let idx_usize = self.emit_index_usize(index)?;
|
|
25527
25825
|
let is_int = self.module_const_int_statics.contains(&static_name);
|
|
25528
25826
|
// The proven-in-bounds read on an integer array is the whole point: a
|
|
25529
25827
|
// plain word load, reported as I32 so the consumer stays in the integer
|
|
@@ -25786,9 +26084,10 @@ impl Codegen {
|
|
|
25786
26084
|
// dropping to `ops::*`. Only a native-returning extern is taken here; a `void`/`Value`
|
|
25787
26085
|
// one falls through to the boxed path (this is a value-position expression).
|
|
25788
26086
|
if let Expr::Ident { name: fname, .. } = callee.as_ref() {
|
|
25789
|
-
if let Some(sig
|
|
26087
|
+
if let Some((sig, suffix)) =
|
|
26088
|
+
self.extern_sig_for_arity(fname.as_ref(), args.len())
|
|
26089
|
+
{
|
|
25790
26090
|
if sig.ret.is_native()
|
|
25791
|
-
&& args.len() == sig.params.len()
|
|
25792
26091
|
&& args.iter().all(|a| matches!(a, CallArg::Expr(_)))
|
|
25793
26092
|
{
|
|
25794
26093
|
let mut argc: Vec<String> = Vec::with_capacity(args.len());
|
|
@@ -25798,9 +26097,10 @@ impl Codegen {
|
|
|
25798
26097
|
}
|
|
25799
26098
|
}
|
|
25800
26099
|
let call = format!(
|
|
25801
|
-
"{}::{}
|
|
26100
|
+
"{}::{}{}({})",
|
|
25802
26101
|
sig.crate_name,
|
|
25803
26102
|
sig.symbol,
|
|
26103
|
+
suffix,
|
|
25804
26104
|
argc.join(", ")
|
|
25805
26105
|
);
|
|
25806
26106
|
return Ok((call, sig.ret));
|
|
@@ -26737,7 +27037,12 @@ impl Codegen {
|
|
|
26737
27037
|
let RustType::Vec(inner) = self.type_context.get_type(recv_name.as_ref()) else {
|
|
26738
27038
|
return Ok(None);
|
|
26739
27039
|
};
|
|
26740
|
-
|
|
27040
|
+
// #609: the element-type gate exists in TWO places — here and at the top of
|
|
27041
|
+
// `try_native_vec_hof`. Widening only the inner one changes nothing, because this one
|
|
27042
|
+
// returns first: the array is boxed whole into a `Value::Array` and handed to
|
|
27043
|
+
// `tishlang_runtime::array_map`, which is the per-element `value_call` path this issue is
|
|
27044
|
+
// about. Both must agree.
|
|
27045
|
+
if *inner != RustType::F64 && *inner != RustType::I32 {
|
|
26741
27046
|
return Ok(None);
|
|
26742
27047
|
}
|
|
26743
27048
|
let recv_code = Self::escape_ident(recv_name.as_ref()).into_owned();
|
|
@@ -26766,8 +27071,14 @@ impl Codegen {
|
|
|
26766
27071
|
method: &str,
|
|
26767
27072
|
args: &[CallArg],
|
|
26768
27073
|
) -> Result<Option<(String, RustType)>, CompileError> {
|
|
26769
|
-
//
|
|
26770
|
-
|
|
27074
|
+
// `.copied()` needs a `Copy` element, which both of these are.
|
|
27075
|
+
//
|
|
27076
|
+
// #609: `i32` joins `f64` here. On GBA every idiomatic array is `i32[]`, so restricting the
|
|
27077
|
+
// fast path to `Vec<f64>` meant `.map/.filter/.some/.every` fell back to one boxed
|
|
27078
|
+
// `value_call` PER ELEMENT on exactly the arrays the target guidance tells you to write.
|
|
27079
|
+
//
|
|
27080
|
+
// `reduce` is deliberately NOT extended — see its arm below.
|
|
27081
|
+
if *elem != RustType::F64 && *elem != RustType::I32 {
|
|
26771
27082
|
return Ok(None);
|
|
26772
27083
|
}
|
|
26773
27084
|
let Some(CallArg::Expr(Expr::ArrowFunction { params, body, .. })) = args.first() else {
|
|
@@ -26799,7 +27110,12 @@ impl Codegen {
|
|
|
26799
27110
|
res
|
|
26800
27111
|
};
|
|
26801
27112
|
match method {
|
|
26802
|
-
|
|
27113
|
+
// #609 stops short here on purpose. The accumulator is typed `f64` (or `i64` for the
|
|
27114
|
+
// integer-range form); with an `i32` element the body would mix an `i32` binding into
|
|
27115
|
+
// that accumulator — `acc = acc + x` either fails to compile or silently changes
|
|
27116
|
+
// overflow behaviour from f64's exact-integer range to i32 wrapping. That is a results
|
|
27117
|
+
// question, not a speed one, so it wants its own change with its own tests.
|
|
27118
|
+
"reduce" if *elem == RustType::F64 => {
|
|
26803
27119
|
if args.len() != 2 || params.len() != 2 {
|
|
26804
27120
|
return Ok(None);
|
|
26805
27121
|
}
|
|
@@ -26893,7 +27209,7 @@ impl Codegen {
|
|
|
26893
27209
|
let Some(x) = simple_name(¶ms[0]) else {
|
|
26894
27210
|
return Ok(None);
|
|
26895
27211
|
};
|
|
26896
|
-
let (body_code, body_ty) = emit_with(self, &[(&x,
|
|
27212
|
+
let (body_code, body_ty) = emit_with(self, &[(&x, elem.clone())])?;
|
|
26897
27213
|
if !body_ty.is_native() {
|
|
26898
27214
|
return Ok(None);
|
|
26899
27215
|
}
|
|
@@ -26913,17 +27229,21 @@ impl Codegen {
|
|
|
26913
27229
|
let Some(x) = simple_name(¶ms[0]) else {
|
|
26914
27230
|
return Ok(None);
|
|
26915
27231
|
};
|
|
26916
|
-
let (body_code, body_ty) = emit_with(self, &[(&x,
|
|
27232
|
+
let (body_code, body_ty) = emit_with(self, &[(&x, elem.clone())])?;
|
|
26917
27233
|
if body_ty != RustType::Bool {
|
|
26918
27234
|
return Ok(None);
|
|
26919
27235
|
}
|
|
26920
27236
|
let x_esc = Self::escape_ident(x.as_ref()).into_owned();
|
|
27237
|
+
// A filter PRESERVES the element type — collecting to `Vec<f64>` from an `i32`
|
|
27238
|
+
// receiver would not compile, and silently widening would change what the caller
|
|
27239
|
+
// gets back.
|
|
26921
27240
|
Ok(Some((
|
|
26922
27241
|
format!(
|
|
26923
|
-
"{recv}.iter().copied().filter(|&{x}| {body}).collect::<Vec<
|
|
26924
|
-
recv = recv, x = x_esc, body = body_code
|
|
27242
|
+
"{recv}.iter().copied().filter(|&{x}| {body}).collect::<Vec<{ety}>>()",
|
|
27243
|
+
recv = recv, x = x_esc, body = body_code,
|
|
27244
|
+
ety = elem.to_rust_type_str()
|
|
26925
27245
|
),
|
|
26926
|
-
RustType::Vec(Box::new(
|
|
27246
|
+
RustType::Vec(Box::new(elem.clone())),
|
|
26927
27247
|
)))
|
|
26928
27248
|
}
|
|
26929
27249
|
"some" | "every" => {
|
|
@@ -26933,7 +27253,7 @@ impl Codegen {
|
|
|
26933
27253
|
let Some(x) = simple_name(¶ms[0]) else {
|
|
26934
27254
|
return Ok(None);
|
|
26935
27255
|
};
|
|
26936
|
-
let (body_code, body_ty) = emit_with(self, &[(&x,
|
|
27256
|
+
let (body_code, body_ty) = emit_with(self, &[(&x, elem.clone())])?;
|
|
26937
27257
|
if body_ty != RustType::Bool {
|
|
26938
27258
|
return Ok(None);
|
|
26939
27259
|
}
|