Skip to content

Commit 554986b

Browse files
author
Ralph Küpper
committed
fix(codegen): a declared numeric type is not a proof that the value is a number
Perry does not enforce annotations at runtime (CLAUDE.md, Known Limitations), but codegen answered `is_numeric_expr` = true on the strength of one and then emitted bare f64 arithmetic on whatever the slot actually held. That is worse than a NaN, because arithmetic on a NaN-BOXED value is not a no-op that yields NaN: `fadd`/`fmul` propagate the input NaN's payload, so a NaN-boxed string comes back out of the instruction STILL TAGGED AS THAT STRING. `typeof (v * 2)` answered "string", and `v + 1` looked as though the `+ 1` had evaporated. Three divergences from Node, all silent: #7773 shape 1 `o.x + 1` gave NaN (the number-context read's cold arm coerces unconditionally). Node concatenates: `s1`. #7773 shape 2 through a refined local (`const v = o.x`) there was no coerce at all, so the string passed straight through. #7776 a heterogeneous element stored via `as any`, then summed. New predicate `numeric_proof_is_declared_only` separates "an annotation said so" from a real proof. It is deliberately narrower than `expr_may_return_boxed_value_from_raw_f64_fallback`, which answers "is there a raw-f64 tier worth trying" and stays true for reads that end up with no boxed fallback: every arm carrying a guard, a closed store universe or scalar replacement answers false, so element-shape and class-field loop facts, `Ptr<Shape>` numeric fields, POD records, scalar replacement and typed arrays all keep their bare loads. Two consumers: * `+` with a declared-only operand lowers through `lower_declared_only_numeric_add`: an inline NaN-box tag test, `fadd` on the fast arm, `js_dynamic_string_or_number_add` on the cold one. The spec's `+` dispatches on the runtime value, so this is the operator that needs the dispatch rather than a coerce. * every other arithmetic operator is a plain ToNumber, so the existing residual `js_number_coerce` rule is enough — it just could not see a refined LOCAL before. `expr/mod.rs::lower_numeric_binary_value` is a second arithmetic tier that bypasses `binary::lower` entirely and emits bare `fadd`/`fmul` with no residual coerce at all; it is the path both refined-local shapes took, and it now hands declared-only operands down to `binary::lower` the same way its two existing Mod cases do. Two things the first attempt got wrong, both now pinned by the test: * ONE diamond per `+` TREE, not one per node. Per-node diamonds make the outer add of `s += o.x + 1` consume a phi, and LLVM cannot prove a phi over (`fadd`, runtime call) is a canonical double — the outer test never folded and the hot loop lost its `fadd` to an unconditional call. Fusing took that shape from +38% to +8.6%. Both arms rebuild the ORIGINAL tree shape, because `+` is not associative across strings: `1 + (2 + "x")` is `"12x"` and `(1 + 2) + "x"` is `"3x"`. * every leaf is tested except those `expr_produces_canonical_raw_f64` vouches for. Testing only the declared-only leaves skips the ACCUMULATOR, and `let s = 0; s += r.x + r.y` holds a string the moment this lowering's own cold arm concatenates — that summed `16zw1113151719` down to `16zw`, the original bug one level up. Measured on the quiet M1 mini (load 1.68, 7 alternating runs, same runtime for both arms so only codegen differs): element-shape clone 218 -> 217 ms -0.5% (untouched, as intended) this.v + 1 in method 70 -> 76 ms +8.6% s += p.x + p.y 196 -> 263 ms +34.2% The cost falls only on reads the compiler could prove nothing about, which already pay an inline header precheck or a `js_typed_feedback_class_field_get_guard` call for their shape check. It is a real cost and the alternative is silently wrong arithmetic. test-files/test_gap_declared_numeric_field_holds_string_7773.ts covers both reported shapes plus array elements, inherited fields, chained adds and the accumulator, and asserts the other direction for VALUE — honest arithmetic, an honest guard failure and a typed array must all still answer as numbers.
1 parent 1ee158d commit 554986b

12 files changed

Lines changed: 561 additions & 5 deletions

File tree

crates/perry-codegen/src/codegen/closure.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -978,6 +978,7 @@ pub(super) fn compile_closure(
978978
temp_roots: crate::rooting::TempRootPool::default(),
979979
shadow_slot_map,
980980
persistent_shadow_slots: std::collections::HashSet::new(),
981+
declared_only_numeric_locals: std::collections::HashSet::new(),
981982
shadow_slot_clears_after_stmt,
982983
arena_state_slot: None,
983984
class_keys_slots: HashMap::new(),

crates/perry-codegen/src/codegen/entry.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,7 @@ pub(super) fn compile_module_entry(
816816
temp_roots: crate::rooting::TempRootPool::default(),
817817
shadow_slot_map: main_shadow_slot_map,
818818
persistent_shadow_slots: std::collections::HashSet::new(),
819+
declared_only_numeric_locals: std::collections::HashSet::new(),
819820
shadow_slot_clears_after_stmt: main_shadow_slot_clears_after_stmt,
820821
arena_state_slot: None,
821822
class_keys_slots: HashMap::new(),
@@ -1485,6 +1486,7 @@ pub(super) fn compile_module_entry(
14851486
temp_roots: crate::rooting::TempRootPool::default(),
14861487
shadow_slot_map: init_shadow_slot_map,
14871488
persistent_shadow_slots: std::collections::HashSet::new(),
1489+
declared_only_numeric_locals: std::collections::HashSet::new(),
14881490
shadow_slot_clears_after_stmt: init_shadow_slot_clears_after_stmt,
14891491
arena_state_slot: None,
14901492
class_keys_slots: HashMap::new(),

crates/perry-codegen/src/codegen/function.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,7 @@ pub(super) fn compile_function(
770770
unsigned_i32_locals: native_facts.unsigned_i32_locals(),
771771
shadow_slot_map,
772772
persistent_shadow_slots: std::collections::HashSet::new(),
773+
declared_only_numeric_locals: std::collections::HashSet::new(),
773774
shadow_slot_clears_after_stmt,
774775
shadow_slots_bound: bound_param_slots,
775776
temp_roots: crate::rooting::TempRootPool::default(),

crates/perry-codegen/src/codegen/method.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,7 @@ pub(super) fn compile_method(
509509
temp_roots: crate::rooting::TempRootPool::default(),
510510
shadow_slot_map,
511511
persistent_shadow_slots: std::collections::HashSet::new(),
512+
declared_only_numeric_locals: std::collections::HashSet::new(),
512513
shadow_slot_clears_after_stmt,
513514
arena_state_slot: None,
514515
class_keys_slots: HashMap::new(),
@@ -1572,6 +1573,7 @@ pub(super) fn compile_static_method(
15721573
temp_roots: crate::rooting::TempRootPool::default(),
15731574
shadow_slot_map,
15741575
persistent_shadow_slots: std::collections::HashSet::new(),
1576+
declared_only_numeric_locals: std::collections::HashSet::new(),
15751577
shadow_slot_clears_after_stmt,
15761578
arena_state_slot: None,
15771579
class_keys_slots: HashMap::new(),

crates/perry-codegen/src/expr/binary.rs

Lines changed: 181 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::native_value::{
1818
use crate::type_analysis::{
1919
add_operands_have_pod_materialization_hazard,
2020
expr_may_return_boxed_value_from_raw_f64_fallback, is_bigint_expr, is_bool_expr,
21-
is_numeric_expr,
21+
is_numeric_expr, numeric_proof_is_declared_only,
2222
};
2323
use crate::types::{DOUBLE, I1, I128, I32, I64};
2424

@@ -51,6 +51,166 @@ fn lower_rooted_dynamic_binary(
5151
})
5252
}
5353

54+
/// `+` where both operands are statically numeric but at least one of them is
55+
/// numeric only because a DECLARED type said so (#7773, #7776).
56+
///
57+
/// Nothing enforces annotations at runtime, so a `x: number` slot reached
58+
/// through `as any` really can hold a string — and then the spec says `+` is
59+
/// string concatenation, which is what Node does. Trusting the annotation cost
60+
/// two different wrong answers, both silent:
61+
///
62+
/// * `o.x + 1` produced `NaN`, because the number-context read's cold arm
63+
/// `js_number_coerce`s unconditionally; Node prints `s1`.
64+
/// * through a refined local the add did not even coerce — `fadd` on a
65+
/// NaN-BOXED value propagates the input payload on both AArch64 and x86-64,
66+
/// so the string came back out of the add still a string, and the `+ 1`
67+
/// looked like it had evaporated.
68+
///
69+
/// So re-check at runtime instead of assuming. The fast arm keeps the inline
70+
/// `fadd`; only a value that is not a canonical double reaches the dynamic
71+
/// helper, which is the one that implements the spec's `+`.
72+
///
73+
/// **The whole `+` TREE becomes one diamond, not one per node.** That is a
74+
/// correctness-neutral but performance-critical detail, and doing it the
75+
/// obvious way first is what showed why. Per-node diamonds make the outer add
76+
/// of `s += o.x + 1` consume a PHI, and LLVM cannot prove a phi over
77+
/// (`fadd`, runtime call) is a canonical double — so the outer test never
78+
/// folded, its cold arm stayed live in the loop, and `Acc.run`'s hot loop lost
79+
/// its `fadd` to an unconditional call. Measured on the bench mini that shape
80+
/// went 86 ms -> 119 ms. Fusing the tree removes the phi entirely: one test
81+
/// over the tree's violable LEAVES, one branch, then either all-`fadd` or
82+
/// all-`js_dynamic_string_or_number_add`.
83+
///
84+
/// Associativity is preserved rather than assumed away: both arms rebuild the
85+
/// ORIGINAL tree shape. `1 + (2 + "x")` is `"12x"` and `(1 + 2) + "x"` is
86+
/// `"3x"`, so a flattened re-association would be a wrong answer — the leaves
87+
/// are collected in evaluation order for rooting, but the arms are rebuilt
88+
/// node-for-node.
89+
///
90+
/// Every leaf is tested EXCEPT those that `expr_produces_canonical_raw_f64`
91+
/// vouches for (literals, `Math.*`, an explicit coerce, non-`+` arithmetic).
92+
/// Testing only the declared-only leaves is not enough, and the accumulator is
93+
/// the counter-example: `let s = 0; s += r.x + r.y` types `s` as `Number`, but
94+
/// the moment this very lowering's cold arm concatenates, `s` HOLDS A STRING
95+
/// while its static type still says otherwise. Skipping it summed
96+
/// `16zw1113151719` down to `16zw` — the fast arm `fadd`ed a NaN-boxed string
97+
/// and passed it through unchanged, which is the original bug reintroduced one
98+
/// level up. `expr_produces_canonical_raw_f64` declines to vouch for a
99+
/// `LocalGet` precisely because a local is a slot somebody can store into.
100+
///
101+
/// The residual cost lands where it is already small: every read that reaches
102+
/// here is one the compiler could NOT prove, so it pays an inline header
103+
/// precheck or a `js_typed_feedback_class_field_get_guard` call for its shape
104+
/// check regardless. The proven tiers (element-shape / class-field loop facts,
105+
/// `Ptr<Shape>` numeric fields, scalar replacement, POD records, typed arrays)
106+
/// never get here at all — `numeric_proof_is_declared_only` answers `false`.
107+
fn lower_declared_only_numeric_add(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
108+
let mut leaves = Vec::new();
109+
add_tree_leaves(expr, &mut leaves);
110+
let needs_test: Vec<bool> = leaves
111+
.iter()
112+
.map(|leaf| !crate::type_analysis::expr_produces_canonical_raw_f64(ctx, leaf))
113+
.collect();
114+
115+
with_operands_rooted(ctx, &leaves, |ctx, values| {
116+
let mut cond: Option<String> = None;
117+
for (value, is_tested) in values.iter().zip(needs_test.iter()) {
118+
if !is_tested {
119+
continue;
120+
}
121+
let is_num = crate::stmt::emit_js_value_is_number(ctx, value);
122+
cond = Some(match cond {
123+
Some(prev) => ctx.block().and(I1, &prev, &is_num),
124+
None => is_num,
125+
});
126+
}
127+
// The caller only routes here when a leaf is declared-only, and every
128+
// such leaf is a field / element / local read — none of which
129+
// `expr_produces_canonical_raw_f64` vouches for. So there is always at
130+
// least one test; an empty condition would mean the two predicates had
131+
// drifted apart, which is worth a hard error rather than a silent
132+
// unguarded `fadd`.
133+
let Some(all_num) = cond else {
134+
anyhow::bail!(
135+
"declared-only `+` tree has no testable leaf: \
136+
numeric_proof_is_declared_only and expr_produces_canonical_raw_f64 disagree"
137+
);
138+
};
139+
140+
let fast_idx = ctx.new_block("declared_add.numeric");
141+
let slow_idx = ctx.new_block("declared_add.dynamic");
142+
let merge_idx = ctx.new_block("declared_add.merge");
143+
let fast_label = ctx.block_label(fast_idx);
144+
let slow_label = ctx.block_label(slow_idx);
145+
let merge_label = ctx.block_label(merge_idx);
146+
ctx.block().cond_br(&all_num, &fast_label, &slow_label);
147+
148+
ctx.current_block = fast_idx;
149+
let fast_val = rebuild_add_tree(ctx, expr, values, &mut 0, true);
150+
let fast_end = ctx.block().label.clone();
151+
ctx.block().br(&merge_label);
152+
153+
ctx.current_block = slow_idx;
154+
let slow_val = rebuild_add_tree(ctx, expr, values, &mut 0, false);
155+
let slow_end = ctx.block().label.clone();
156+
ctx.block().br(&merge_label);
157+
158+
ctx.current_block = merge_idx;
159+
Ok(ctx
160+
.block()
161+
.phi(DOUBLE, &[(&fast_val, &fast_end), (&slow_val, &slow_end)]))
162+
})
163+
}
164+
165+
/// The `+` tree's operand leaves, in evaluation order — a left-to-right walk,
166+
/// so `with_operands_rooted` lowers them in the order JS evaluates them.
167+
fn add_tree_leaves<'a>(expr: &'a Expr, out: &mut Vec<&'a Expr>) {
168+
if let Expr::Binary {
169+
op: BinaryOp::Add,
170+
left,
171+
right,
172+
} = expr
173+
{
174+
add_tree_leaves(left, out);
175+
add_tree_leaves(right, out);
176+
} else {
177+
out.push(expr);
178+
}
179+
}
180+
181+
/// Rebuild the `+` tree over already-lowered leaf values, node for node, so the
182+
/// original associativity survives. `fast` picks the inline `fadd`; otherwise
183+
/// every node goes through the spec-`+` helper.
184+
fn rebuild_add_tree(
185+
ctx: &mut FnCtx<'_>,
186+
expr: &Expr,
187+
values: &[String],
188+
next_leaf: &mut usize,
189+
fast: bool,
190+
) -> String {
191+
if let Expr::Binary {
192+
op: BinaryOp::Add,
193+
left,
194+
right,
195+
} = expr
196+
{
197+
let l = rebuild_add_tree(ctx, left, values, next_leaf, fast);
198+
let r = rebuild_add_tree(ctx, right, values, next_leaf, fast);
199+
return if fast {
200+
ctx.block().fadd(&l, &r)
201+
} else {
202+
ctx.block().call(
203+
DOUBLE,
204+
"js_dynamic_string_or_number_add",
205+
&[(DOUBLE, &l), (DOUBLE, &r)],
206+
)
207+
};
208+
}
209+
let value = values[*next_leaf].clone();
210+
*next_leaf += 1;
211+
value
212+
}
213+
54214
fn lower_arithmetic_operand(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<(String, bool)> {
55215
// #6884: a statically typed numeric TypedArray read is Number|undefined,
56216
// not an unconditional raw f64. In arithmetic context the OOB `undefined`
@@ -145,7 +305,17 @@ fn lower_arithmetic_operand(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<(String,
145305
fn operand_needs_residual_coerce(ctx: &FnCtx<'_>, expr: &Expr, fallback_coerced: bool) -> bool {
146306
!fallback_coerced
147307
&& (!is_numeric_expr(ctx, expr)
148-
|| expr_may_return_boxed_value_from_raw_f64_fallback(ctx, expr))
308+
|| expr_may_return_boxed_value_from_raw_f64_fallback(ctx, expr)
309+
// #7773: a local REFINED to `Number` from a declared field/element
310+
// type is `is_numeric_expr`, but the hazard predicate above only
311+
// knows how to look at reads, so `const v = o.x; v * 2` emitted a
312+
// bare `fmul`. Arithmetic on a NaN-box preserves the payload, so
313+
// that multiply returned the string unchanged — `typeof (v * 2)`
314+
// answered `"string"`. Every non-`+` arithmetic operator is a plain
315+
// `ToNumber` on its operands, so a coerce is the whole fix here;
316+
// `+` needs the concat dispatch and gets it from
317+
// `lower_declared_only_numeric_add`.
318+
|| matches!(expr, Expr::LocalGet(_)) && numeric_proof_is_declared_only(ctx, expr))
149319
}
150320

151321
/// Lower an operand in number context: route through
@@ -519,6 +689,15 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
519689
right,
520690
);
521691
}
692+
// Both sides are statically numeric — but "statically" can mean
693+
// "an annotation said so", and annotations are not enforced
694+
// (#7773, #7776). Re-check the tag at runtime rather than
695+
// emitting a bare `fadd` on a value that may be NaN-boxed.
696+
if numeric_proof_is_declared_only(ctx, left)
697+
|| numeric_proof_is_declared_only(ctx, right)
698+
{
699+
return lower_declared_only_numeric_add(ctx, expr);
700+
}
522701
}
523702
// BigInt arithmetic fast path. NaN-tagged bigints compare
524703
// unordered under `fadd`/`fsub`/`fmul`/`fdiv`/`frem` (the

crates/perry-codegen/src/expr/mod.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,23 @@ pub(crate) struct FnCtx<'a> {
744744
/// protected temporary this function lowers.
745745
pub temp_roots: crate::rooting::TempRootPool,
746746

747+
/// #7773: LocalIds whose `Number`/`Int32` type was REFINED from a read
748+
/// whose own numeric answer is only a declared type — `const v = o.x` on a
749+
/// `x: number` field, or `const e = arr[i]` on a `number[]`.
750+
///
751+
/// The refinement is load-bearing (an un-annotated `const` is `Any` in the
752+
/// HIR, so without it every ordinary field read loses the numeric fast
753+
/// path), but it copies an annotation rather than proving anything. The
754+
/// local then reads as `is_numeric_expr`, which licenses a bare `fadd` /
755+
/// `fmul` on whatever the slot holds — and arithmetic on a NaN-boxed value
756+
/// PRESERVES ITS PAYLOAD, so a string laundered in through `as any` came
757+
/// back out of a multiply still tagged as a string (`typeof (v * 2)` was
758+
/// `"string"`).
759+
///
760+
/// Consumed by `type_analysis::numeric_proof_is_declared_only`, which turns
761+
/// the trust into a four-instruction runtime tag test instead.
762+
pub declared_only_numeric_locals: std::collections::HashSet<u32>,
763+
747764
/// Cached pointer to this function's `InlineArenaState` slot —
748765
/// allocated lazily on the first `new ClassName()` site that uses
749766
/// the inline bump-allocator path. The slot lives in the function
@@ -2393,6 +2410,26 @@ fn lower_numeric_binary_value(
23932410
return Ok(None);
23942411
}
23952412

2413+
// #7773: `is_numeric_expr` answering `true` is not always a PROOF — for a
2414+
// class-field read, an array element, or a local refined from one, it is
2415+
// just the declared type repeated back, and nothing enforces declared types
2416+
// at runtime. This tier emits a bare `fadd`/`fmul` with no residual coerce
2417+
// at all, and arithmetic on a NaN-BOXED value propagates the payload
2418+
// instead of producing NaN — so a string laundered into a `x: number` slot
2419+
// came back out of `v * 2` still a string (`typeof` said `"string"`).
2420+
//
2421+
// Hand those to `binary::lower`, which has both remedies: the runtime tag
2422+
// test that keeps `+` on the spec's string-concat dispatch, and the
2423+
// residual `js_number_coerce` that gives every other operator its
2424+
// `ToNumber`. Same hand-off shape as the two Mod cases below, and for the
2425+
// same reason — it must run before operand lowering so an `Ok(None)` emits
2426+
// no dead loads or duplicate records.
2427+
if crate::type_analysis::numeric_proof_is_declared_only(ctx, left)
2428+
|| crate::type_analysis::numeric_proof_is_declared_only(ctx, right)
2429+
{
2430+
return Ok(None);
2431+
}
2432+
23962433
// Hand this proven shape to `binary::lower`, which owns the existing
23972434
// integer remainder and negative-zero repair. This must run before operand
23982435
// lowering so returning `None` emits no dead loads or duplicate records.

crates/perry-codegen/src/stmt/let_stmt.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,27 @@ pub(crate) fn lower_let(
290290
ty.clone()
291291
};
292292

293+
// #7773: the refinement above copies a DECLARED type — `const v = o.x` on a
294+
// `x: number` field answers `Number` because the annotation says so, not
295+
// because anything proved it. Nothing enforces annotations at runtime, so
296+
// record the local as violable; `numeric_proof_is_declared_only` then makes
297+
// arithmetic on it re-check the tag instead of trusting the type outright.
298+
//
299+
// Only the Any → numeric direction matters. A local the user DECLARED
300+
// `number` is equally unenforced, but it is also the shape every honest
301+
// program is made of; the refined case is the one where codegen invented
302+
// the numeric claim itself, and it is the one both reported shapes need.
303+
if matches!(ty, perry_hir::types::Type::Any)
304+
&& matches!(
305+
refined_ty,
306+
perry_hir::types::Type::Number | perry_hir::types::Type::Int32
307+
)
308+
{
309+
if init.is_some_and(|e| crate::type_analysis::numeric_proof_is_declared_only(ctx, e)) {
310+
ctx.declared_only_numeric_locals.insert(id);
311+
}
312+
}
313+
293314
// Track closure func_id → local_id mapping so the closure
294315
// call site in lower_call can look up rest param info.
295316
if let Some(perry_hir::Expr::Closure {

crates/perry-codegen/src/stmt/loops.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4569,7 +4569,7 @@ fn dynamic_bound_private_counter_is_safe(
45694569
advanced_by_increment && !stmts_mutate_local(body, counter_id)
45704570
}
45714571

4572-
pub(super) fn emit_js_value_is_number(ctx: &mut FnCtx<'_>, value: &str) -> String {
4572+
pub(crate) fn emit_js_value_is_number(ctx: &mut FnCtx<'_>, value: &str) -> String {
45734573
let n_bits = ctx.block().bitcast_double_to_i64(value);
45744574
let tag = ctx.block().and(
45754575
I64,

crates/perry-codegen/src/stmt/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ mod unused_expr;
3131

3232
pub(crate) use if_stmt::lower_if;
3333
pub(crate) use let_stmt::lower_let;
34-
pub(crate) use loops::{lower_do_while, lower_for, lower_while};
34+
pub(crate) use loops::{emit_js_value_is_number, lower_do_while, lower_for, lower_while};
3535
pub(crate) use switch_stmt::lower_switch;
3636
pub(crate) use try_stmt::lower_try;
3737

crates/perry-codegen/src/type_analysis.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ pub(crate) use numeric::{
3838
pub(crate) use pod::{
3939
add_operands_have_pod_materialization_hazard,
4040
expr_may_return_boxed_value_from_raw_f64_fallback, expression_has_numeric_length,
41-
is_fixed_width_buffer_numeric_read, is_numeric_typed_array_class, pod_record_field_is_numeric,
41+
is_fixed_width_buffer_numeric_read, is_numeric_typed_array_class,
42+
numeric_proof_is_declared_only, pod_record_field_is_numeric,
4243
scalar_replaced_array_element_is_raw_f64, scalar_replaced_field_is_raw_f64,
4344
scalar_replaced_field_raw_f64_store_state,
4445
};

0 commit comments

Comments
 (0)