@tishlang/tish-format 3.7.0 → 3.8.6
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-format +0 -0
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish_compile/src/codegen.rs +555 -23
- package/crates/tish_compile/src/lib.rs +8 -2
- package/crates/tish_compile/tests/regr_655_gba_stack_guard.rs +41 -0
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_native/src/build.rs +287 -22
- package/crates/tish_runtime_gba/src/lib.rs +51 -2
- package/crates/tish_test/Cargo.toml +1 -1
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
|
@@ -1166,6 +1166,12 @@ pub(crate) struct Codegen {
|
|
|
1166
1166
|
/// free `fn f_native(f64,..)->f64` (all params `: number`, returns `number`, native-safe
|
|
1167
1167
|
/// body). Direct calls to these route to the native fn, bypassing the boxed `value_call`.
|
|
1168
1168
|
native_fns: std::collections::HashSet<String>,
|
|
1169
|
+
/// Declared param types of each `native_fns` member, so a call site knows which argument slots
|
|
1170
|
+
/// are `&TishStruct_…` (passed by reference) rather than a coerced numeric.
|
|
1171
|
+
native_fn_param_types: std::collections::HashMap<String, Vec<RustType>>,
|
|
1172
|
+
/// Names ever used AS A VALUE (see [`Self::collect_value_used_expr`]). A `native_fns` member
|
|
1173
|
+
/// absent from this set needs no boxed `Value::native` wrapper in `run()`.
|
|
1174
|
+
value_used_names: std::collections::HashSet<String>,
|
|
1169
1175
|
/// Set while emitting the body of an i32-ABI native fn, so `return` coerces to `i32`.
|
|
1170
1176
|
native_fn_abi_i32: bool,
|
|
1171
1177
|
/// The subset of `native_fns` emitted with an `fn(i32, ..) -> i32` signature instead of the
|
|
@@ -1447,6 +1453,8 @@ impl Codegen {
|
|
|
1447
1453
|
collection_instance_locals: std::collections::HashSet::new(),
|
|
1448
1454
|
hoisted_string_chars: std::collections::HashMap::new(),
|
|
1449
1455
|
native_fns: std::collections::HashSet::new(),
|
|
1456
|
+
value_used_names: std::collections::HashSet::new(),
|
|
1457
|
+
native_fn_param_types: std::collections::HashMap::new(),
|
|
1450
1458
|
native_fns_i32: std::collections::HashSet::new(),
|
|
1451
1459
|
native_fn_abi_i32: false,
|
|
1452
1460
|
native_fn_body_emit: false,
|
|
@@ -3319,11 +3327,88 @@ impl Codegen {
|
|
|
3319
3327
|
let mut global_names: std::collections::HashSet<String> =
|
|
3320
3328
|
self.native_numeric_globals.keys().cloned().collect();
|
|
3321
3329
|
global_names.extend(self.module_const_f64_arrays.keys().cloned());
|
|
3330
|
+
// Publish the all-numeric externs before the proof runs, so a body calling into an
|
|
3331
|
+
// extension crate is not rejected for the call alone. `is_numeric` covers f64, i32 and
|
|
3332
|
+
// the narrow ints; anything else (string, array, struct) would need a boxing boundary
|
|
3333
|
+
// inside a lowered fn, which defeats the purpose.
|
|
3334
|
+
let numeric_externs: std::collections::HashSet<String> = self
|
|
3335
|
+
.extern_fns
|
|
3336
|
+
.iter()
|
|
3337
|
+
.filter(|(_, sigs)| {
|
|
3338
|
+
// ⚠️ NOT `is_numeric()` ALONE — that matches F64 only (types.rs), while every GBA
|
|
3339
|
+
// `declare fn f(row: i32): i32` is `I32`. Filtering on it rejected all 64 declares
|
|
3340
|
+
// in ffta-hud (probed: numeric_externs=0) and made this whole relaxation inert.
|
|
3341
|
+
// Same predicate the rest of the proof uses: f64, i32, or a narrow storage int.
|
|
3342
|
+
//
|
|
3343
|
+
// Widening this REQUIRES the f64 return coercion in `emit_native_fn_body` — an
|
|
3344
|
+
// i32 extern returned from an f64-ABI fn is otherwise emitted raw (E0308).
|
|
3345
|
+
let ok = |t: &RustType| t.is_numeric() || t.is_integer_scalar();
|
|
3346
|
+
!sigs.is_empty()
|
|
3347
|
+
&& sigs.iter().all(|s| ok(&s.ret) && s.params.iter().all(ok))
|
|
3348
|
+
})
|
|
3349
|
+
.map(|(n, _)| n.clone())
|
|
3350
|
+
.collect();
|
|
3351
|
+
if std::env::var("TISH_PROBE_EXTERNS").as_deref() == Ok("1") {
|
|
3352
|
+
let mut names: Vec<&String> = numeric_externs.iter().collect();
|
|
3353
|
+
names.sort();
|
|
3354
|
+
eprintln!(
|
|
3355
|
+
"[probe] extern_fns={} numeric_externs={} sample={:?}",
|
|
3356
|
+
self.extern_fns.len(),
|
|
3357
|
+
numeric_externs.len(),
|
|
3358
|
+
names.iter().take(8).collect::<Vec<_>>()
|
|
3359
|
+
);
|
|
3360
|
+
}
|
|
3361
|
+
Self::numeric_externs_with(|s| {
|
|
3362
|
+
s.clear();
|
|
3363
|
+
// `TISH_NO_EXTERN_LOWER=1` publishes an EMPTY set, which reverts this build to the
|
|
3364
|
+
// pre-extern-admission behaviour without rebuilding the compiler. It exists so a ROM
|
|
3365
|
+
// can be A/B'd against one binary: two 5-minute game builds instead of two toolchain
|
|
3366
|
+
// rebuilds plus two game builds, and no edit to a shared checkout between them.
|
|
3367
|
+
if std::env::var("TISH_NO_EXTERN_LOWER").as_deref() != Ok("1") {
|
|
3368
|
+
s.extend(numeric_externs.iter().cloned());
|
|
3369
|
+
}
|
|
3370
|
+
});
|
|
3322
3371
|
self.native_fns = Self::collect_native_fns(
|
|
3323
3372
|
&program.statements,
|
|
3324
3373
|
&global_names,
|
|
3325
3374
|
&native_vec_names,
|
|
3375
|
+
&self.type_aliases,
|
|
3326
3376
|
);
|
|
3377
|
+
self.native_fn_param_types = program
|
|
3378
|
+
.statements
|
|
3379
|
+
.iter()
|
|
3380
|
+
.filter_map(|st| match st {
|
|
3381
|
+
Statement::FunDecl { name, params, .. }
|
|
3382
|
+
if self.native_fns.contains(name.as_ref()) =>
|
|
3383
|
+
{
|
|
3384
|
+
let tys = params
|
|
3385
|
+
.iter()
|
|
3386
|
+
.filter_map(|p| match p {
|
|
3387
|
+
FunParam::Simple(tp) => Some(
|
|
3388
|
+
tp.type_ann
|
|
3389
|
+
.as_ref()
|
|
3390
|
+
.map(|a| {
|
|
3391
|
+
RustType::from_annotation_with_aliases(
|
|
3392
|
+
a,
|
|
3393
|
+
&self.type_aliases,
|
|
3394
|
+
)
|
|
3395
|
+
})
|
|
3396
|
+
.unwrap_or(RustType::F64),
|
|
3397
|
+
),
|
|
3398
|
+
_ => None,
|
|
3399
|
+
})
|
|
3400
|
+
.collect();
|
|
3401
|
+
Some((name.to_string(), tys))
|
|
3402
|
+
}
|
|
3403
|
+
_ => None,
|
|
3404
|
+
})
|
|
3405
|
+
.collect();
|
|
3406
|
+
// Which names are ever used as a VALUE rather than only called. Drives the
|
|
3407
|
+
// boxed-wrapper elision at `Statement::FunDecl` — see `collect_value_used_expr`.
|
|
3408
|
+
self.value_used_names.clear();
|
|
3409
|
+
for s in &program.statements {
|
|
3410
|
+
Self::collect_value_used_stmt(s, &mut self.value_used_names);
|
|
3411
|
+
}
|
|
3327
3412
|
// Which of the survivors carry the all-`i32` signature. Computed AFTER the fixpoint,
|
|
3328
3413
|
// so a function the proof rejected never reaches the i32 ABI either.
|
|
3329
3414
|
self.native_fns_i32 = program
|
|
@@ -3332,7 +3417,23 @@ impl Codegen {
|
|
|
3332
3417
|
.filter_map(|s| match s {
|
|
3333
3418
|
Statement::FunDecl { name, params, return_type, .. }
|
|
3334
3419
|
if self.native_fns.contains(name.as_ref())
|
|
3335
|
-
&& Self::fn_sig_all_i32(params, return_type)
|
|
3420
|
+
&& (Self::fn_sig_all_i32(params, return_type)
|
|
3421
|
+
// A struct-param fn: its struct slot is typed on its own, so
|
|
3422
|
+
// `fn_sig_all_i32` can never hold. Take the i32 return ABI when the
|
|
3423
|
+
// return is `: i32` and every NON-struct param is `: i32` too —
|
|
3424
|
+
// otherwise the body computes i32 while the signature says f64.
|
|
3425
|
+
// A struct param qualifies on ONE numeric field, matching the
|
|
3426
|
+
// candidacy gate. If this still demanded all-numeric, a mixed-struct
|
|
3427
|
+
// fn would lower its body to i32 arithmetic and then be handed the
|
|
3428
|
+
// f64 ABI — computing one thing and declaring another.
|
|
3429
|
+
|| (return_type.as_ref().is_some_and(Self::ann_is_i32)
|
|
3430
|
+
&& params.iter().any(|p| matches!(p, FunParam::Simple(tp)
|
|
3431
|
+
if tp.type_ann.as_ref().is_some_and(|a|
|
|
3432
|
+
!Self::struct_numeric_fields(a, &self.type_aliases).is_empty())))
|
|
3433
|
+
&& params.iter().all(|p| matches!(p, FunParam::Simple(tp)
|
|
3434
|
+
if tp.type_ann.as_ref().is_some_and(|a|
|
|
3435
|
+
Self::ann_is_i32(a)
|
|
3436
|
+
|| !Self::struct_numeric_fields(a, &self.type_aliases).is_empty()))))) =>
|
|
3336
3437
|
{
|
|
3337
3438
|
Some(name.to_string())
|
|
3338
3439
|
}
|
|
@@ -3347,9 +3448,11 @@ impl Codegen {
|
|
|
3347
3448
|
Self::collect_native_lcg_fns(&program.statements, &self.native_fns);
|
|
3348
3449
|
// #381 — fns on a call-graph cycle get guarded rotation copies (default ON;
|
|
3349
3450
|
// TISH_NATIVE_RECUR_GUARD=0 restores the plain unguarded emission).
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3451
|
+
// #655: GBA was excluded because the no_std runtime had no stack-pressure probe.
|
|
3452
|
+
// `tishlang_runtime_gba` now derives a real floor from the link map (`__iwram_end`),
|
|
3453
|
+
// and it matters MORE here than on the host: 32 KB of IWRAM, no MMU, no guard page,
|
|
3454
|
+
// so an unguarded overflow silently overwrites agb's live data instead of faulting.
|
|
3455
|
+
self.native_recur_fns = if crate::native_recur_guard_enabled() {
|
|
3353
3456
|
Self::compute_native_recur_fns(&program.statements, &self.native_fns)
|
|
3354
3457
|
} else {
|
|
3355
3458
|
std::collections::HashMap::new()
|
|
@@ -4234,6 +4337,10 @@ impl Codegen {
|
|
|
4234
4337
|
val_code
|
|
4235
4338
|
} else if val_ty == RustType::Value {
|
|
4236
4339
|
RustType::F64.from_value_expr(&val_code)
|
|
4340
|
+
} else if val_ty.is_integer_scalar() {
|
|
4341
|
+
// An i32-typed value into an f64-backed global. Widening is exact; emitted
|
|
4342
|
+
// raw it is E0308.
|
|
4343
|
+
format!("(({}) as f64)", val_code)
|
|
4237
4344
|
} else {
|
|
4238
4345
|
val_code
|
|
4239
4346
|
};
|
|
@@ -4350,6 +4457,17 @@ impl Codegen {
|
|
|
4350
4457
|
// `fixed` local must be lifted to `Fixed` — the RHS emitter reports numeric
|
|
4351
4458
|
// literals as F64, so without this a `fixed` reassignment mistypes (E0308).
|
|
4352
4459
|
format!("tishlang_runtime::Fixed::from_raw((({}) * 256.0) as i32)", val_code)
|
|
4460
|
+
} else if rust_type == RustType::F64 && val_ty.is_integer_scalar() {
|
|
4461
|
+
// ⚠️ AN i32 VALUE INTO AN f64 LOCAL, IN STATEMENT POSITION. The COMPARISON
|
|
4462
|
+
// emitter already widens (`n > ((MAX_UNITS) as f64)`), so
|
|
4463
|
+
//
|
|
4464
|
+
// if (n > MAX_UNITS) { n = MAX_UNITS } packages/clansave
|
|
4465
|
+
//
|
|
4466
|
+
// compiled its test and rejected its assignment: `n = MAX_UNITS;`, expected
|
|
4467
|
+
// f64 found i32. Needs a native local AND an `: i32`-annotated global on the
|
|
4468
|
+
// right, which is why it stayed hidden until a game imported clansave beside
|
|
4469
|
+
// the rest of its packages. Widening is exact.
|
|
4470
|
+
format!("(({}) as f64)", val_code)
|
|
4353
4471
|
} else {
|
|
4354
4472
|
val_code
|
|
4355
4473
|
};
|
|
@@ -7037,6 +7155,34 @@ impl Codegen {
|
|
|
7037
7155
|
{
|
|
7038
7156
|
return Ok(());
|
|
7039
7157
|
}
|
|
7158
|
+
// Same idea, wider: this fn already has a native lowering (`<name>_native`) that
|
|
7159
|
+
// every direct call routes to, and nothing in the program uses the NAME as a value
|
|
7160
|
+
// — so the boxed `Value::native` wrapper is dead weight. It is not free weight: the
|
|
7161
|
+
// wrapper and its cell are permanent slots in the enclosing frame, and for module
|
|
7162
|
+
// fns that frame is `run()`, which on GBA holds every one of them at once against a
|
|
7163
|
+
// 29 KB IWRAM stack. Skipping it is what makes "type your functions" actually pay.
|
|
7164
|
+
// ⚠️ AND ONLY WHEN EVERY CALL SITE CAN ACTUALLY BE DIRECT. A fn taking a STRUCT
|
|
7165
|
+
// still falls back to `value_call` when the argument is a plain boxed local --
|
|
7166
|
+
// `area(r)` in tests/core/struct_return does exactly that while `area(makeRect(..))`
|
|
7167
|
+
// lowers, so one call site needed the wrapper the other made look dead (E0425).
|
|
7168
|
+
// Restricting the elision to all-scalar parameter lists keeps the win for the
|
|
7169
|
+
// numeric fns it was written for and stops guessing about the rest.
|
|
7170
|
+
let all_scalar_params = self
|
|
7171
|
+
.native_fn_param_types
|
|
7172
|
+
.get(name.as_ref())
|
|
7173
|
+
.map(|ps| {
|
|
7174
|
+
ps.iter().all(|p| {
|
|
7175
|
+
matches!(p, RustType::F64 | RustType::I32 | RustType::Bool)
|
|
7176
|
+
|| p.is_narrow_int()
|
|
7177
|
+
})
|
|
7178
|
+
})
|
|
7179
|
+
.unwrap_or(false);
|
|
7180
|
+
if self.native_fns.contains(name.as_ref())
|
|
7181
|
+
&& !self.value_used_names.contains(name.as_ref())
|
|
7182
|
+
&& all_scalar_params
|
|
7183
|
+
{
|
|
7184
|
+
return Ok(());
|
|
7185
|
+
}
|
|
7040
7186
|
// Use Rc<RefCell<>> pattern to allow recursive function calls
|
|
7041
7187
|
// The function can reference itself through the cell
|
|
7042
7188
|
let name_raw = name.as_ref();
|
|
@@ -9391,12 +9537,28 @@ impl Codegen {
|
|
|
9391
9537
|
let i32_abi = self.native_fns_i32.contains(fname.as_ref());
|
|
9392
9538
|
let want =
|
|
9393
9539
|
if i32_abi { RustType::I32 } else { RustType::F64 };
|
|
9540
|
+
let ptys = self.native_fn_param_types.get(fname.as_ref()).cloned();
|
|
9394
9541
|
let mut argc: Vec<String> = Vec::with_capacity(args.len());
|
|
9395
9542
|
let mut ok = true;
|
|
9396
|
-
for a in args {
|
|
9543
|
+
for (i, a) in args.iter().enumerate() {
|
|
9397
9544
|
if let CallArg::Expr(e) = a {
|
|
9545
|
+
// A struct slot takes a shared reference to the caller's struct;
|
|
9546
|
+
// coercing it to a number would be nonsense (and would not compile).
|
|
9547
|
+
let is_struct = matches!(
|
|
9548
|
+
ptys.as_ref().and_then(|v| v.get(i)),
|
|
9549
|
+
Some(RustType::Named { .. })
|
|
9550
|
+
);
|
|
9398
9551
|
let (ac, at) = self.emit_typed_expr(e)?;
|
|
9399
|
-
|
|
9552
|
+
if is_struct {
|
|
9553
|
+
if matches!(at, RustType::Named { .. }) {
|
|
9554
|
+
argc.push(format!("&{}", ac));
|
|
9555
|
+
} else {
|
|
9556
|
+
ok = false;
|
|
9557
|
+
break;
|
|
9558
|
+
}
|
|
9559
|
+
} else {
|
|
9560
|
+
argc.push(self.coerce_native_arg(&ac, at, want.clone()));
|
|
9561
|
+
}
|
|
9400
9562
|
} else {
|
|
9401
9563
|
ok = false;
|
|
9402
9564
|
break;
|
|
@@ -9835,6 +9997,10 @@ impl Codegen {
|
|
|
9835
9997
|
val_code
|
|
9836
9998
|
} else if val_ty == RustType::Value {
|
|
9837
9999
|
RustType::F64.from_value_expr(&val_code)
|
|
10000
|
+
} else if val_ty.is_integer_scalar() {
|
|
10001
|
+
// An i32-typed value into an f64-backed global. Widening is exact; emitted
|
|
10002
|
+
// raw it is E0308.
|
|
10003
|
+
format!("(({}) as f64)", val_code)
|
|
9838
10004
|
} else {
|
|
9839
10005
|
val_code
|
|
9840
10006
|
};
|
|
@@ -9857,6 +10023,12 @@ impl Codegen {
|
|
|
9857
10023
|
val_code
|
|
9858
10024
|
} else if val_ty == RustType::Value {
|
|
9859
10025
|
rust_type.from_value_expr(&val_code)
|
|
10026
|
+
} else if rust_type == RustType::F64 && val_ty.is_integer_scalar() {
|
|
10027
|
+
// ⚠️ An `: i32` global assigned into an f64-typed local. `let n = roster.len()`
|
|
10028
|
+
// infers f64, and `n = MAX_UNITS` (a `let MAX_UNITS: i32` in packages/clansave)
|
|
10029
|
+
// emitted `n = MAX_UNITS;` raw -- E0308, expected f64 found i32. Needs BOTH a
|
|
10030
|
+
// native local and an i32-annotated global, which is why so little hit it.
|
|
10031
|
+
format!("(({}) as f64)", val_code)
|
|
9860
10032
|
} else {
|
|
9861
10033
|
val_code
|
|
9862
10034
|
};
|
|
@@ -10367,6 +10539,126 @@ impl Codegen {
|
|
|
10367
10539
|
idents
|
|
10368
10540
|
}
|
|
10369
10541
|
|
|
10542
|
+
/// Names used AS A VALUE — i.e. everywhere except the callee slot of a direct `f(..)` call.
|
|
10543
|
+
/// A module fn that is only ever called directly needs no boxed `Value::native` wrapper: the
|
|
10544
|
+
/// native lowering (`f_native`) already serves every call site, and the wrapper is a permanent
|
|
10545
|
+
/// slot in `run()`'s stack frame. On GBA `run()` holds every module fn at once — 605 of them in
|
|
10546
|
+
/// `examples/ffta` — against a 29 KB IWRAM stack, so dropping the unused wrappers is the
|
|
10547
|
+
/// difference between a game fitting and overwriting the audio mixer.
|
|
10548
|
+
///
|
|
10549
|
+
/// ⚠️ CONSERVATIVE BY CONSTRUCTION. Anything this does not structurally recurse through falls
|
|
10550
|
+
/// back to [`Self::collect_expr_idents`], which marks every name inside as value-used — that
|
|
10551
|
+
/// only forfeits the optimisation, it never removes a wrapper something needs. And if it ever
|
|
10552
|
+
/// were wrong, the generated Rust names a binding that does not exist: a loud `E0425` at build
|
|
10553
|
+
/// time, not a silently wrong ROM.
|
|
10554
|
+
fn collect_value_used_expr(expr: &Expr, idents: &mut HashSet<String>) {
|
|
10555
|
+
match expr {
|
|
10556
|
+
Expr::Call { callee, args, .. } => {
|
|
10557
|
+
// The callee slot of a direct call is NOT a value use -- UNLESS the call cannot be
|
|
10558
|
+
// emitted as a direct one. A SPREAD call always goes through `value_call`, which
|
|
10559
|
+
// needs the boxed binding by name, so eliding the wrapper leaves `let _callee =
|
|
10560
|
+
// (sum3).clone()` referring to nothing (E0425, tests/core/spread).
|
|
10561
|
+
let spreads = args.iter().any(|a| matches!(a, CallArg::Spread(_)));
|
|
10562
|
+
if !matches!(&**callee, Expr::Ident { .. }) || spreads {
|
|
10563
|
+
Self::collect_value_used_expr(callee, idents);
|
|
10564
|
+
}
|
|
10565
|
+
for arg in args {
|
|
10566
|
+
match arg {
|
|
10567
|
+
CallArg::Expr(e) | CallArg::Spread(e) => {
|
|
10568
|
+
Self::collect_value_used_expr(e, idents)
|
|
10569
|
+
}
|
|
10570
|
+
}
|
|
10571
|
+
}
|
|
10572
|
+
}
|
|
10573
|
+
Expr::Binary { left, right, .. } => {
|
|
10574
|
+
Self::collect_value_used_expr(left, idents);
|
|
10575
|
+
Self::collect_value_used_expr(right, idents);
|
|
10576
|
+
}
|
|
10577
|
+
Expr::Unary { operand, .. } => Self::collect_value_used_expr(operand, idents),
|
|
10578
|
+
Expr::Conditional {
|
|
10579
|
+
cond,
|
|
10580
|
+
then_branch,
|
|
10581
|
+
else_branch,
|
|
10582
|
+
..
|
|
10583
|
+
} => {
|
|
10584
|
+
Self::collect_value_used_expr(cond, idents);
|
|
10585
|
+
Self::collect_value_used_expr(then_branch, idents);
|
|
10586
|
+
Self::collect_value_used_expr(else_branch, idents);
|
|
10587
|
+
}
|
|
10588
|
+
Expr::Member { object, prop, .. } => {
|
|
10589
|
+
Self::collect_value_used_expr(object, idents);
|
|
10590
|
+
if let MemberProp::Expr(e) = prop {
|
|
10591
|
+
Self::collect_value_used_expr(e, idents);
|
|
10592
|
+
}
|
|
10593
|
+
}
|
|
10594
|
+
Expr::Index { object, index, .. } => {
|
|
10595
|
+
Self::collect_value_used_expr(object, idents);
|
|
10596
|
+
Self::collect_value_used_expr(index, idents);
|
|
10597
|
+
}
|
|
10598
|
+
other => Self::collect_expr_idents(other, idents),
|
|
10599
|
+
}
|
|
10600
|
+
}
|
|
10601
|
+
|
|
10602
|
+
/// Statement-level driver for [`Self::collect_value_used_expr`]. Mirrors the shape of
|
|
10603
|
+
/// [`Self::collect_stmt_idents`]; unknown statement forms fall back to it (conservative).
|
|
10604
|
+
fn collect_value_used_stmt(stmt: &Statement, idents: &mut HashSet<String>) {
|
|
10605
|
+
match stmt {
|
|
10606
|
+
Statement::ExprStmt { expr, .. } => Self::collect_value_used_expr(expr, idents),
|
|
10607
|
+
Statement::VarDecl { init, .. } => {
|
|
10608
|
+
if let Some(e) = init {
|
|
10609
|
+
Self::collect_value_used_expr(e, idents);
|
|
10610
|
+
}
|
|
10611
|
+
}
|
|
10612
|
+
Statement::Return { value, .. } => {
|
|
10613
|
+
if let Some(e) = value {
|
|
10614
|
+
Self::collect_value_used_expr(e, idents);
|
|
10615
|
+
}
|
|
10616
|
+
}
|
|
10617
|
+
Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
|
|
10618
|
+
for s in statements {
|
|
10619
|
+
Self::collect_value_used_stmt(s, idents);
|
|
10620
|
+
}
|
|
10621
|
+
}
|
|
10622
|
+
Statement::If {
|
|
10623
|
+
cond,
|
|
10624
|
+
then_branch,
|
|
10625
|
+
else_branch,
|
|
10626
|
+
..
|
|
10627
|
+
} => {
|
|
10628
|
+
Self::collect_value_used_expr(cond, idents);
|
|
10629
|
+
Self::collect_value_used_stmt(then_branch, idents);
|
|
10630
|
+
if let Some(s) = else_branch {
|
|
10631
|
+
Self::collect_value_used_stmt(s, idents);
|
|
10632
|
+
}
|
|
10633
|
+
}
|
|
10634
|
+
Statement::While { cond, body, .. } => {
|
|
10635
|
+
Self::collect_value_used_expr(cond, idents);
|
|
10636
|
+
Self::collect_value_used_stmt(body, idents);
|
|
10637
|
+
}
|
|
10638
|
+
Statement::FunDecl { body, .. } => Self::collect_value_used_stmt(body, idents),
|
|
10639
|
+
// ⚠️ An EXPORTED fn must keep its boxed wrapper: the module namespace hands it out as
|
|
10640
|
+
// a `Value`, and an importer may do anything with it. Mark the exported name itself as
|
|
10641
|
+
// value-used, then walk the declaration normally.
|
|
10642
|
+
Statement::Export { declaration, .. } => match &**declaration {
|
|
10643
|
+
tishlang_ast::ExportDeclaration::Named(inner) => {
|
|
10644
|
+
if let Statement::FunDecl { name, .. } = &**inner {
|
|
10645
|
+
idents.insert(name.to_string());
|
|
10646
|
+
}
|
|
10647
|
+
Self::collect_value_used_stmt(inner, idents);
|
|
10648
|
+
}
|
|
10649
|
+
tishlang_ast::ExportDeclaration::Default(e) => Self::collect_value_used_expr(e, idents),
|
|
10650
|
+
tishlang_ast::ExportDeclaration::ReExport { specifiers, .. } => {
|
|
10651
|
+
for sp in specifiers {
|
|
10652
|
+
if let tishlang_ast::ImportSpecifier::Named { name, .. } = sp {
|
|
10653
|
+
idents.insert(name.to_string());
|
|
10654
|
+
}
|
|
10655
|
+
}
|
|
10656
|
+
}
|
|
10657
|
+
},
|
|
10658
|
+
other => Self::collect_stmt_idents(other, idents),
|
|
10659
|
+
}
|
|
10660
|
+
}
|
|
10661
|
+
|
|
10370
10662
|
fn collect_expr_idents(expr: &Expr, idents: &mut HashSet<String>) {
|
|
10371
10663
|
match expr {
|
|
10372
10664
|
Expr::Ident { name, .. } => {
|
|
@@ -19222,6 +19514,7 @@ impl Codegen {
|
|
|
19222
19514
|
statements: &[Statement],
|
|
19223
19515
|
globals: &std::collections::HashSet<String>,
|
|
19224
19516
|
native_vec_names: &std::collections::HashSet<String>,
|
|
19517
|
+
aliases: &std::collections::HashMap<String, RustType>,
|
|
19225
19518
|
) -> std::collections::HashSet<String> {
|
|
19226
19519
|
use std::collections::HashSet;
|
|
19227
19520
|
let mut cand: HashSet<String> = HashSet::new();
|
|
@@ -19240,7 +19533,14 @@ impl Codegen {
|
|
|
19240
19533
|
let params_ok = params.iter().all(|p| {
|
|
19241
19534
|
matches!(p, FunParam::Simple(tp)
|
|
19242
19535
|
if tp.default.is_none()
|
|
19243
|
-
&& tp.type_ann.as_ref().map_or(true,
|
|
19536
|
+
&& tp.type_ann.as_ref().map_or(true, |a| {
|
|
19537
|
+
Self::ann_is_number(a)
|
|
19538
|
+
// At least one numeric field is enough to be a CANDIDATE. The
|
|
19539
|
+
// fixpoint below still has to prove the body, and a read of a
|
|
19540
|
+
// non-numeric field fails that proof — so admitting a mixed
|
|
19541
|
+
// record here cannot lower anything that was not already sound.
|
|
19542
|
+
|| !Self::struct_numeric_fields(a, aliases).is_empty()
|
|
19543
|
+
}))
|
|
19244
19544
|
});
|
|
19245
19545
|
// Return: an annotated `: number`, OR unannotated with all-numeric returns
|
|
19246
19546
|
// (verified in the fixpoint via `returns_numeric`), so the native `-> f64` holds.
|
|
@@ -19253,7 +19553,19 @@ impl Codegen {
|
|
|
19253
19553
|
// still has to prove the body native-safe and every return numeric — the annotation
|
|
19254
19554
|
// never decides that on its own (see `liar` in the issue).
|
|
19255
19555
|
let i32_ok = Self::fn_sig_all_i32(params, return_type);
|
|
19256
|
-
|
|
19556
|
+
// A struct-param fn can never satisfy `fn_sig_all_i32` (its struct slot is not an
|
|
19557
|
+
// `i32`), so an `: i32` return would reject it even though the return is perfectly
|
|
19558
|
+
// native. Accept an i32 return for those; the f64 ABI carries it, and the existing
|
|
19559
|
+
// all-i32 fast path is untouched for everyone else.
|
|
19560
|
+
let has_struct_param = params.iter().any(|p| {
|
|
19561
|
+
matches!(p, FunParam::Simple(tp)
|
|
19562
|
+
if tp.type_ann.as_ref()
|
|
19563
|
+
.is_some_and(|a| !Self::struct_numeric_fields(a, aliases).is_empty()))
|
|
19564
|
+
});
|
|
19565
|
+
let ret_ok = ret_ok
|
|
19566
|
+
|| i32_ok
|
|
19567
|
+
|| (has_struct_param && return_type.as_ref().is_some_and(Self::ann_is_i32));
|
|
19568
|
+
let params_ok = params_ok || i32_ok;
|
|
19257
19569
|
// #320: 0-param numeric fns (e.g. k_nucleotide's `nextBase()` — mutates a numeric
|
|
19258
19570
|
// global, returns number) are eligible too; the fixpoint below still proves the body
|
|
19259
19571
|
// native-safe + all-numeric-returns, so `fn name_native() -> f64` is sound.
|
|
@@ -19307,6 +19619,25 @@ impl Codegen {
|
|
|
19307
19619
|
}
|
|
19308
19620
|
let pnames: HashSet<String> =
|
|
19309
19621
|
params.iter().flat_map(|p| p.bound_names()).map(|n| n.to_string()).collect();
|
|
19622
|
+
// Each struct param maps to the fields that can lower — its NUMERIC ones. A param
|
|
19623
|
+
// with no numeric field is left out entirely, so it reads exactly as "not a struct
|
|
19624
|
+
// param" and nothing about it is native.
|
|
19625
|
+
let sparams: std::collections::HashMap<String, HashSet<String>> = params
|
|
19626
|
+
.iter()
|
|
19627
|
+
.filter_map(|p| match p {
|
|
19628
|
+
FunParam::Simple(tp) => tp.type_ann.as_ref().and_then(|a| {
|
|
19629
|
+
let fs = Self::struct_numeric_fields(a, aliases);
|
|
19630
|
+
(!fs.is_empty()).then(|| (tp.name.to_string(), fs))
|
|
19631
|
+
}),
|
|
19632
|
+
_ => None,
|
|
19633
|
+
})
|
|
19634
|
+
.collect();
|
|
19635
|
+
Self::struct_params_with(|m| {
|
|
19636
|
+
m.clear();
|
|
19637
|
+
for (k, v) in sparams.iter() {
|
|
19638
|
+
m.insert(k.clone(), v.clone());
|
|
19639
|
+
}
|
|
19640
|
+
});
|
|
19310
19641
|
let nums = Self::native_fn_numeric_locals(
|
|
19311
19642
|
body,
|
|
19312
19643
|
&pnames,
|
|
@@ -19314,9 +19645,20 @@ impl Codegen {
|
|
|
19314
19645
|
&cand,
|
|
19315
19646
|
native_vec_names,
|
|
19316
19647
|
);
|
|
19317
|
-
|
|
19318
|
-
|
|
19319
|
-
|
|
19648
|
+
let safe = Self::native_safe_stmt(body, &pnames, &cand, globals, &nums, native_vec_names);
|
|
19649
|
+
let rets = Self::returns_numeric(body, &pnames, &cand, globals, &nums);
|
|
19650
|
+
let ok = safe && rets;
|
|
19651
|
+
if !ok && std::env::var("TISH_PROBE_LOWERING").as_deref() == Ok("1") {
|
|
19652
|
+
// Which HALF rejected it. "body not native-safe" and "a return is not numeric"
|
|
19653
|
+
// are entirely different fixes, and the fixpoint silently collapsed both into a
|
|
19654
|
+
// candidate quietly disappearing.
|
|
19655
|
+
eprintln!(
|
|
19656
|
+
"[probe] {} rejected: body_safe={} returns_numeric={}",
|
|
19657
|
+
name, safe, rets
|
|
19658
|
+
);
|
|
19659
|
+
}
|
|
19660
|
+
Self::struct_params_with(|s| s.clear());
|
|
19661
|
+
if !ok {
|
|
19320
19662
|
remove.push(name.to_string());
|
|
19321
19663
|
}
|
|
19322
19664
|
}
|
|
@@ -19512,6 +19854,114 @@ impl Codegen {
|
|
|
19512
19854
|
}
|
|
19513
19855
|
|
|
19514
19856
|
/// Numeric locals in an M5-eligible fn body (params + fixpoint literal/copy seeds).
|
|
19857
|
+
/// The current function's params that are annotated with a type alias whose fields are ALL
|
|
19858
|
+
/// numeric, so `p.field` is a machine-word load. Set for the duration of one function's
|
|
19859
|
+
/// native-safety proof (`collect_native_fns`) and read by the proof's leaf predicates.
|
|
19860
|
+
///
|
|
19861
|
+
/// A side-channel rather than a threaded argument on purpose: the proof is five mutually
|
|
19862
|
+
/// recursive static helpers, and widening all of their signatures to carry a set that only two
|
|
19863
|
+
/// leaves consult is a large diff for no added safety. Codegen is single-threaded; the set is
|
|
19864
|
+
/// cleared after each function.
|
|
19865
|
+
///
|
|
19866
|
+
/// ⚠️ THE FIELD SET IS WHAT MAKES THIS SOUND, AND IT REPLACED A BARE NAME SET FOR THAT REASON.
|
|
19867
|
+
/// While the map held only names, admitting a struct with a `string` field would have made
|
|
19868
|
+
/// `p.name` "numeric" here and emitted Rust that does not compile — so the gate demanded that
|
|
19869
|
+
/// EVERY field be numeric, and one string disqualified the whole function. Carrying the numeric
|
|
19870
|
+
/// fields per param removes that ceiling: `p.x` proves native and `p.name` does not, so a mixed
|
|
19871
|
+
/// record lowers its arithmetic and a function that touches the string simply fails the proof
|
|
19872
|
+
/// and stays boxed. Never widen this to a bare name test again.
|
|
19873
|
+
fn struct_params_with<R>(
|
|
19874
|
+
f: impl FnOnce(&mut std::collections::HashMap<String, HashSet<String>>) -> R,
|
|
19875
|
+
) -> R {
|
|
19876
|
+
thread_local! {
|
|
19877
|
+
static STRUCT_PARAMS:
|
|
19878
|
+
std::cell::RefCell<std::collections::HashMap<String, HashSet<String>>> =
|
|
19879
|
+
std::cell::RefCell::new(std::collections::HashMap::new());
|
|
19880
|
+
}
|
|
19881
|
+
STRUCT_PARAMS.with(|c| f(&mut c.borrow_mut()))
|
|
19882
|
+
}
|
|
19883
|
+
|
|
19884
|
+
/// Declared externs whose signature is numbers in, a number out.
|
|
19885
|
+
///
|
|
19886
|
+
/// A `declare fn` already resolves to a native Rust fn — the emitter calls
|
|
19887
|
+
/// `<crate>::<symbol>_typed(..)` directly (see [`Self::extern_sig_for_arity`]). But the proof
|
|
19888
|
+
/// only accepted calls to `cand` (the tish fns being lowered), so ANY function calling into an
|
|
19889
|
+
/// extension crate was rejected and stayed boxed. On a game that is nearly everything: measured
|
|
19890
|
+
/// on `examples/ffta-hud`, a fn doing `u.col * 100 + u.row` lowered while the one beside it
|
|
19891
|
+
/// calling `tac_add_unit(u.col, u.row, …)` did not, purely because of the call.
|
|
19892
|
+
///
|
|
19893
|
+
/// Only ALL-NUMERIC signatures qualify. An extern taking or returning a string, an array or a
|
|
19894
|
+
/// struct is not callable from a lowered body without a boxing boundary, which is the thing
|
|
19895
|
+
/// being removed.
|
|
19896
|
+
///
|
|
19897
|
+
/// A thread-local for the same reason [`Self::struct_params_with`] is one: the proof is a set of
|
|
19898
|
+
/// associated fns with no `&self`, and threading a new parameter through every one of them would
|
|
19899
|
+
/// touch far more code than the fact being communicated.
|
|
19900
|
+
fn numeric_externs_with<R>(f: impl FnOnce(&mut HashSet<String>) -> R) -> R {
|
|
19901
|
+
thread_local! {
|
|
19902
|
+
static NUMERIC_EXTERNS: std::cell::RefCell<HashSet<String>> =
|
|
19903
|
+
std::cell::RefCell::new(HashSet::new());
|
|
19904
|
+
}
|
|
19905
|
+
NUMERIC_EXTERNS.with(|c| f(&mut c.borrow_mut()))
|
|
19906
|
+
}
|
|
19907
|
+
|
|
19908
|
+
/// `p.field` where `p` is one of [`Self::struct_params_with`]'s params AND `field` is one of its
|
|
19909
|
+
/// NUMERIC fields. Both halves matter: the name alone would admit `p.name` on a mixed record.
|
|
19910
|
+
fn is_struct_param_field(e: &Expr) -> bool {
|
|
19911
|
+
let Expr::Member { object, prop, .. } = e else {
|
|
19912
|
+
return false;
|
|
19913
|
+
};
|
|
19914
|
+
let MemberProp::Name { name: fname, .. } = prop else {
|
|
19915
|
+
return false;
|
|
19916
|
+
};
|
|
19917
|
+
let Expr::Ident { name, .. } = object.as_ref() else {
|
|
19918
|
+
return false;
|
|
19919
|
+
};
|
|
19920
|
+
Self::struct_params_with(|m| {
|
|
19921
|
+
m.get(name.as_ref()).is_some_and(|fs| fs.contains(fname.as_ref()))
|
|
19922
|
+
})
|
|
19923
|
+
}
|
|
19924
|
+
|
|
19925
|
+
/// Every field of this annotation's aliased struct is a native NUMBER (the invariant above).
|
|
19926
|
+
fn ann_is_all_numeric_struct(
|
|
19927
|
+
ann: &TypeAnnotation,
|
|
19928
|
+
aliases: &std::collections::HashMap<String, RustType>,
|
|
19929
|
+
) -> bool {
|
|
19930
|
+
matches!(
|
|
19931
|
+
RustType::from_annotation_with_aliases(ann, aliases),
|
|
19932
|
+
RustType::Named { ref fields, .. }
|
|
19933
|
+
if !fields.is_empty()
|
|
19934
|
+
&& fields.iter().all(|(_, t)| {
|
|
19935
|
+
matches!(t, RustType::F64 | RustType::I32) || t.is_narrow_int()
|
|
19936
|
+
})
|
|
19937
|
+
)
|
|
19938
|
+
}
|
|
19939
|
+
|
|
19940
|
+
/// The NUMERIC fields of this annotation's aliased struct, by name.
|
|
19941
|
+
///
|
|
19942
|
+
/// [`Self::ann_is_all_numeric_struct`] asks a whole-struct question, and that is the ceiling on
|
|
19943
|
+
/// what can lower: half of a game's record shapes carry a string, a null or an array beside
|
|
19944
|
+
/// their numbers, and one such field disqualifies the whole function even when it only ever
|
|
19945
|
+
/// touches the numeric ones. Answering per FIELD lets `m.x` lower while `m.name` does not — a
|
|
19946
|
+
/// function that reads the string fails the proof exactly as it does today, so nothing that
|
|
19947
|
+
/// lowers now stops lowering.
|
|
19948
|
+
///
|
|
19949
|
+
/// An empty set means nothing here can lower: not a struct, or a struct with no numeric field.
|
|
19950
|
+
/// Callers treat empty as "not a struct param", which keeps the conservative default.
|
|
19951
|
+
fn struct_numeric_fields(
|
|
19952
|
+
ann: &TypeAnnotation,
|
|
19953
|
+
aliases: &std::collections::HashMap<String, RustType>,
|
|
19954
|
+
) -> HashSet<String> {
|
|
19955
|
+
match RustType::from_annotation_with_aliases(ann, aliases) {
|
|
19956
|
+
RustType::Named { ref fields, .. } => fields
|
|
19957
|
+
.iter()
|
|
19958
|
+
.filter(|(_, t)| matches!(t, RustType::F64 | RustType::I32) || t.is_narrow_int())
|
|
19959
|
+
.map(|(n, _)| n.to_string())
|
|
19960
|
+
.collect(),
|
|
19961
|
+
_ => HashSet::new(),
|
|
19962
|
+
}
|
|
19963
|
+
}
|
|
19964
|
+
|
|
19515
19965
|
fn native_fn_numeric_locals(
|
|
19516
19966
|
body: &Statement,
|
|
19517
19967
|
params: &HashSet<String>,
|
|
@@ -19545,6 +19995,10 @@ impl Codegen {
|
|
|
19545
19995
|
..
|
|
19546
19996
|
} => {
|
|
19547
19997
|
let numeric = type_ann.as_ref().is_some_and(Self::ann_is_number)
|
|
19998
|
+
// `: i32` is a number too. Without this, `let j: i32 = …` was never seeded as a
|
|
19999
|
+
// numeric local, so every later use of `j` failed the safety proof and the whole
|
|
20000
|
+
// function stayed boxed — for an annotation the author added to make it FASTER.
|
|
20001
|
+
|| type_ann.as_ref().is_some_and(Self::ann_is_i32)
|
|
19548
20002
|
|| matches!(
|
|
19549
20003
|
init,
|
|
19550
20004
|
Some(Expr::Literal {
|
|
@@ -19564,7 +20018,13 @@ impl Codegen {
|
|
|
19564
20018
|
}) if matches!(
|
|
19565
20019
|
callee.as_ref(),
|
|
19566
20020
|
Expr::Ident { name: fnname, .. }
|
|
19567
|
-
if cand.contains(fnname.as_ref())
|
|
20021
|
+
if cand.contains(fnname.as_ref())
|
|
20022
|
+
|| native_vec_names.contains(fnname.as_ref())
|
|
20023
|
+
// A numbers-in-number-out extern yields a number, same as the
|
|
20024
|
+
// safety proof and `numeric_shaped` already accept. Every
|
|
20025
|
+
// analysis has to agree or a local initialised from an extern is
|
|
20026
|
+
// numeric in one and unknown in another.
|
|
20027
|
+
|| Self::numeric_externs_with(|s| s.contains(fnname.as_ref()))
|
|
19568
20028
|
)
|
|
19569
20029
|
);
|
|
19570
20030
|
if numeric {
|
|
@@ -19676,6 +20136,13 @@ impl Codegen {
|
|
|
19676
20136
|
nums: &HashSet<String>,
|
|
19677
20137
|
native_vec_names: &std::collections::HashSet<String>,
|
|
19678
20138
|
) -> bool {
|
|
20139
|
+
// A field read off an all-numeric struct param is a plain numeric load — `p.x` on a
|
|
20140
|
+
// `&TishStruct_P` is a machine word, not a boxed property lookup. Without this the proof
|
|
20141
|
+
// rejects every object-taking fn, which is why typing a game's functions never produced a
|
|
20142
|
+
// native lowering. See `STRUCT_PARAMS`.
|
|
20143
|
+
if Self::is_struct_param_field(expr) {
|
|
20144
|
+
return true;
|
|
20145
|
+
}
|
|
19679
20146
|
match expr {
|
|
19680
20147
|
Expr::Literal { value, .. } => matches!(value, Literal::Number(_) | Literal::Bool(_)),
|
|
19681
20148
|
Expr::Ident { name, .. } => {
|
|
@@ -19750,7 +20217,13 @@ impl Codegen {
|
|
|
19750
20217
|
}
|
|
19751
20218
|
match callee.as_ref() {
|
|
19752
20219
|
Expr::Ident { name, .. } => {
|
|
19753
|
-
cand.contains(name.as_ref())
|
|
20220
|
+
cand.contains(name.as_ref())
|
|
20221
|
+
|| native_vec_names.contains(name.as_ref())
|
|
20222
|
+
// A numbers-in-number-out extern lowers to a direct
|
|
20223
|
+
// `<crate>::<symbol>_typed(..)` call, so it is native-safe. Without this
|
|
20224
|
+
// any fn touching an extension crate stayed boxed — which in a game is
|
|
20225
|
+
// most of them.
|
|
20226
|
+
|| Self::numeric_externs_with(|s| s.contains(name.as_ref()))
|
|
19754
20227
|
}
|
|
19755
20228
|
Expr::Member { object, prop: MemberProp::Name { name: m, .. }, .. } => {
|
|
19756
20229
|
matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
|
|
@@ -19810,6 +20283,10 @@ impl Codegen {
|
|
|
19810
20283
|
globals: &std::collections::HashSet<String>,
|
|
19811
20284
|
nums: &HashSet<String>,
|
|
19812
20285
|
) -> bool {
|
|
20286
|
+
// `p.field` on an all-numeric struct param IS a number — see `struct_params_with`.
|
|
20287
|
+
if Self::is_struct_param_field(e) {
|
|
20288
|
+
return true;
|
|
20289
|
+
}
|
|
19813
20290
|
match e {
|
|
19814
20291
|
Expr::Literal { value: Literal::Number(_), .. } => true,
|
|
19815
20292
|
Expr::Ident { name, .. } => {
|
|
@@ -19855,7 +20332,12 @@ impl Codegen {
|
|
|
19855
20332
|
&& Self::numeric_shaped(else_branch, params, cand, globals, nums)
|
|
19856
20333
|
}
|
|
19857
20334
|
Expr::Call { callee, .. } => match callee.as_ref() {
|
|
19858
|
-
|
|
20335
|
+
// Same set as the safety proof: a numbers-in-number-out extern RETURNS a number, so
|
|
20336
|
+
// `return tac_unit_hp(id)` is a numeric return and the fn keeps its native ABI.
|
|
20337
|
+
Expr::Ident { name, .. } => {
|
|
20338
|
+
cand.contains(name.as_ref())
|
|
20339
|
+
|| Self::numeric_externs_with(|s| s.contains(name.as_ref()))
|
|
20340
|
+
}
|
|
19859
20341
|
Expr::Member { object, prop: MemberProp::Name { name: m, .. }, .. } => {
|
|
19860
20342
|
matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
|
|
19861
20343
|
&& matches!(
|
|
@@ -20055,12 +20537,28 @@ impl Codegen {
|
|
|
20055
20537
|
}
|
|
20056
20538
|
let i32_abi = self.native_fns_i32.contains(name.as_ref());
|
|
20057
20539
|
let abi = if i32_abi { "i32" } else { "f64" };
|
|
20540
|
+
// Per-parameter type. A struct param lowers to `&TishStruct_<alias>` — one pointer,
|
|
20541
|
+
// no boxing, and `p.field` becomes a machine-word load. Numeric params keep the
|
|
20542
|
+
// single f64/i32 ABI. `mut` only on the numerics: a shared reference is not rebound.
|
|
20058
20543
|
let plist: Vec<String> = params
|
|
20059
20544
|
.iter()
|
|
20060
20545
|
.filter_map(|p| match p {
|
|
20061
|
-
FunParam::Simple(tp) =>
|
|
20062
|
-
|
|
20063
|
-
|
|
20546
|
+
FunParam::Simple(tp) => Some(
|
|
20547
|
+
match tp.type_ann.as_ref().map(|a| {
|
|
20548
|
+
RustType::from_annotation_with_aliases(a, &self.type_aliases)
|
|
20549
|
+
}) {
|
|
20550
|
+
Some(RustType::Named { name: sname, .. }) => format!(
|
|
20551
|
+
"{}: &{}",
|
|
20552
|
+
Self::escape_ident(tp.name.as_ref()),
|
|
20553
|
+
crate::types::named_struct_ident(&sname)
|
|
20554
|
+
),
|
|
20555
|
+
_ => format!(
|
|
20556
|
+
"mut {}: {}",
|
|
20557
|
+
Self::escape_ident(tp.name.as_ref()),
|
|
20558
|
+
abi
|
|
20559
|
+
),
|
|
20560
|
+
},
|
|
20561
|
+
),
|
|
20064
20562
|
_ => None,
|
|
20065
20563
|
})
|
|
20066
20564
|
.collect();
|
|
@@ -20079,10 +20577,13 @@ impl Codegen {
|
|
|
20079
20577
|
self.type_context.push_scope();
|
|
20080
20578
|
for p in params {
|
|
20081
20579
|
if let FunParam::Simple(tp) = p {
|
|
20082
|
-
|
|
20083
|
-
|
|
20084
|
-
|
|
20085
|
-
|
|
20580
|
+
let ty = match tp.type_ann.as_ref().map(|a| {
|
|
20581
|
+
RustType::from_annotation_with_aliases(a, &self.type_aliases)
|
|
20582
|
+
}) {
|
|
20583
|
+
Some(named @ RustType::Named { .. }) => named,
|
|
20584
|
+
_ => if i32_abi { RustType::I32 } else { RustType::F64 },
|
|
20585
|
+
};
|
|
20586
|
+
self.type_context.define(tp.name.as_ref(), ty);
|
|
20086
20587
|
}
|
|
20087
20588
|
}
|
|
20088
20589
|
let copy_suffix = if copy_idx == 0 { String::new() } else { format!("_r{}", copy_idx) };
|
|
@@ -20281,6 +20782,20 @@ impl Codegen {
|
|
|
20281
20782
|
// An f64-typed expression returned from an `i32` fn takes the same ToInt32 the
|
|
20282
20783
|
// boxed path would have applied at the consumer.
|
|
20283
20784
|
format!("tishlang_runtime::to_int32({})", code)
|
|
20785
|
+
} else if want == RustType::F64 && ty.is_integer_scalar() {
|
|
20786
|
+
// An INTEGER-typed expression returned from an `f64`-ABI fn. Widening is exact
|
|
20787
|
+
// (i32 and every narrow width fit in f64), and without it the body computes an
|
|
20788
|
+
// integer while the signature declares f64:
|
|
20789
|
+
//
|
|
20790
|
+
// fn uiKeys_native() -> f64 { return tish_agb::keys_edge_typed(); }
|
|
20791
|
+
// ^ i32, expected f64 (E0308)
|
|
20792
|
+
//
|
|
20793
|
+
// This was unreachable while the only integer-returning callees were fns the
|
|
20794
|
+
// proof had already typed; it became reachable when calls to i32 externs were
|
|
20795
|
+
// admitted as native-safe, because an extern reports its own declared return
|
|
20796
|
+
// type. `Fixed` is deliberately NOT covered — it is fixed-point, so `as f64`
|
|
20797
|
+
// would reinterpret the scale rather than convert it.
|
|
20798
|
+
format!("(({}) as f64)", code)
|
|
20284
20799
|
} else {
|
|
20285
20800
|
code
|
|
20286
20801
|
};
|
|
@@ -26125,10 +26640,27 @@ impl Codegen {
|
|
|
26125
26640
|
};
|
|
26126
26641
|
let mut argc: Vec<String> = Vec::with_capacity(args.len());
|
|
26127
26642
|
let mut ok = true;
|
|
26128
|
-
for a in args {
|
|
26643
|
+
for (idx, a) in args.iter().enumerate() {
|
|
26129
26644
|
if let CallArg::Expr(e) = a {
|
|
26645
|
+
// A struct slot takes `&caller_struct`; coercing it to a number
|
|
26646
|
+
// would not compile. Mirrors the boxed-context call site.
|
|
26647
|
+
let is_struct = matches!(
|
|
26648
|
+
self.native_fn_param_types
|
|
26649
|
+
.get(fname.as_ref())
|
|
26650
|
+
.and_then(|v| v.get(idx)),
|
|
26651
|
+
Some(RustType::Named { .. })
|
|
26652
|
+
);
|
|
26130
26653
|
let (ac, at) = self.emit_typed_expr(e)?;
|
|
26131
|
-
|
|
26654
|
+
if is_struct {
|
|
26655
|
+
if matches!(at, RustType::Named { .. }) {
|
|
26656
|
+
argc.push(format!("&{}", ac));
|
|
26657
|
+
} else {
|
|
26658
|
+
ok = false;
|
|
26659
|
+
break;
|
|
26660
|
+
}
|
|
26661
|
+
} else {
|
|
26662
|
+
argc.push(self.coerce_native_arg(&ac, at, want.clone()));
|
|
26663
|
+
}
|
|
26132
26664
|
} else {
|
|
26133
26665
|
ok = false;
|
|
26134
26666
|
break;
|