fix(ir): unify float→int narrowing — integral folds, non-integral errors [F0.11]
Issue 0095: a typed local/param/field silently TRUNCATED a float initializer to an integer annotation (`y : s64 = 1.5` → 1) with no diagnostic. Agra ruled the UNIFIED rule (Option B): an implicit float→int in a typed binding behaves like the array-dimension rule — - an INTEGRAL compile-time float FOLDS to its int (`4.0` → 4, `-2.0` → -2); - a NON-integral float is a COMPILE ERROR (`1.5`, `4.5`); - explicit `xx` / `cast(T)` ALWAYS truncates (the escape hatch). Applied consistently to typed local / param-default / field-default, typed module CONST, and array dim — all reusing the single `program_index.floatToIntExact` / `evalConstIntExpr` facility (no second integral check). - `Builder.constFloatInfo` reads a compile-time `const_float` back from its Ref (value + span). - `coerceToType` is now the IMPLICIT path: its `.float_to_int` arm folds an integral const-float to `constInt`, else emits the narrowing diagnostic. `coerceExplicit` is the raw truncating path; `xx` (lowerXX) and `cast(T)` route through it so the escape still truncates. - Field-default lowering (struct-literal pad, named-field default, buildDefaultValue) now coerces the default to the field type at the IR level (was silently bit-coerced by emitStructInit). - Const path: `typedConstInitFits` accepts an integral float (literal or a `M + 2.0`-style expression folding via `evalComptimeInt`); `emitModuleConst` / `constExprValue` / `globalInitValue` fold an integral float to its int and reject a non-integral one — relaxing F0.7's blanket float rejection. Tests: examples/0168 (positive: local/field/param/const fold, xx/cast truncate), examples/1146 (negative: local/param/field error), integral-float const cases added to examples/0162; non-integral const cases in 1143 stay errors. specs.md + readme.md document the unified rule, cross-referencing the array-dim rule. issues/0095 marked RESOLVED.
This commit is contained in:
@@ -8,6 +8,9 @@
|
|||||||
// - integer EXPRESSION → integer (`KE : s64 : M + 2`) — usable as a count too
|
// - integer EXPRESSION → integer (`KE : s64 : M + 2`) — usable as a count too
|
||||||
// - integer EXPRESSION → float (`WE : f32 : M + 2`)
|
// - integer EXPRESSION → float (`WE : f32 : M + 2`)
|
||||||
// - MIXED int+float EXPRESSION → float (`MF : f64 : M + 0.5`, both operand orders)
|
// - MIXED int+float EXPRESSION → float (`MF : f64 : M + 0.5`, both operand orders)
|
||||||
|
// - INTEGRAL float literal → integer (`KF : s64 : 4.0` → 4) — folds under the
|
||||||
|
// unified narrowing rule (F0.11), usable as a count too
|
||||||
|
// - INTEGRAL float EXPRESSION → integer (`KFE : s64 : M + 2.0` → 4)
|
||||||
//
|
//
|
||||||
// Companion to the negative example 1143: the issue-0088 fix rejects a typed
|
// Companion to the negative example 1143: the issue-0088 fix rejects a typed
|
||||||
// const whose initializer mismatches its annotation, and these correctly-typed
|
// const whose initializer mismatches its annotation, and these correctly-typed
|
||||||
@@ -31,6 +34,8 @@ KE : s64 : M + 2;
|
|||||||
WE : f32 : M + 2;
|
WE : f32 : M + 2;
|
||||||
MF : f64 : M + 0.5;
|
MF : f64 : M + 0.5;
|
||||||
MFR : f64 : 0.5 + M;
|
MFR : f64 : 0.5 + M;
|
||||||
|
KF : s64 : 4.0; // integral float literal → folds to 4
|
||||||
|
KFE : s64 : M + 2.0; // integral float expression → folds to 4
|
||||||
|
|
||||||
main :: () {
|
main :: () {
|
||||||
// Integer const: prints AND drives an array dimension (len 4).
|
// Integer const: prints AND drives an array dimension (len 4).
|
||||||
@@ -55,4 +60,9 @@ main :: () {
|
|||||||
// Mixed int+float const-EXPRESSION folds to the promoted float (2.5),
|
// Mixed int+float const-EXPRESSION folds to the promoted float (2.5),
|
||||||
// operand-order-independent.
|
// operand-order-independent.
|
||||||
print("MF={} MFR={}\n", MF, MFR);
|
print("MF={} MFR={}\n", MF, MFR);
|
||||||
|
|
||||||
|
// Integral float const (literal + expression): folds to its integer under
|
||||||
|
// the unified narrowing rule; `KF` also drives an array dimension (len 4).
|
||||||
|
cc : [KF]s64 = ---;
|
||||||
|
print("KF={} len={} KFE={}\n", KF, cc.len, KFE);
|
||||||
}
|
}
|
||||||
|
|||||||
46
examples/0168-types-integral-float-to-int.sx
Normal file
46
examples/0168-types-integral-float-to-int.sx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// Unified float→int narrowing rule (F0.11), POSITIVE side: an INTEGRAL float
|
||||||
|
// flowing into an integer-typed binding FOLDS to its integer — the same
|
||||||
|
// `floatToIntExact` rule an array dimension / `$K: Count` already uses — across
|
||||||
|
// a typed LOCAL, a struct FIELD default, a typed module CONST, and a function
|
||||||
|
// PARAM default. The escape hatch (`xx` / `cast`) still TRUNCATES any float,
|
||||||
|
// integral or not.
|
||||||
|
//
|
||||||
|
// Companion to the negative example 1146 (non-integral floats error).
|
||||||
|
// Regression (issue 0095): a typed local/param/field silently truncated a float
|
||||||
|
// initializer (`y : s64 = 1.5` → 1) with no diagnostic; the rule now folds an
|
||||||
|
// integral float and rejects a non-integral one.
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
Box :: struct {
|
||||||
|
n : s64 = 4.0; // integral float field default → folds to 4
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefault :: (x : s64 = 6.0) -> s64 { return x; } // param default → 6
|
||||||
|
|
||||||
|
K : s64 : 8.0; // integral float module const → folds to 8
|
||||||
|
|
||||||
|
main :: () {
|
||||||
|
// Typed local: integral float folds.
|
||||||
|
z : s64 = 4.0;
|
||||||
|
print("local={}\n", z);
|
||||||
|
|
||||||
|
// Negative integral float folds to its (negative) integer.
|
||||||
|
neg : s64 = -2.0;
|
||||||
|
print("neg={}\n", neg);
|
||||||
|
|
||||||
|
// Struct field default folds.
|
||||||
|
b := Box.{};
|
||||||
|
print("field={}\n", b.n);
|
||||||
|
|
||||||
|
// Param default folds.
|
||||||
|
print("param={}\n", withDefault());
|
||||||
|
|
||||||
|
// Module const folds (and can drive an array dimension: len 8).
|
||||||
|
a : [K]s64 = ---;
|
||||||
|
print("const={} len={}\n", K, a.len);
|
||||||
|
|
||||||
|
// Explicit escape: `xx` / `cast` always truncate, integral or not.
|
||||||
|
e : s64 = xx 4.9;
|
||||||
|
c : s64 = cast(s64) 1.5;
|
||||||
|
print("xx={} cast={}\n", e, c);
|
||||||
|
}
|
||||||
26
examples/1146-diagnostics-nonintegral-float-to-int.sx
Normal file
26
examples/1146-diagnostics-nonintegral-float-to-int.sx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Unified float→int narrowing rule (F0.11), NEGATIVE side: a NON-INTEGRAL float
|
||||||
|
// implicitly narrowing to an integer-typed binding is a COMPILE ERROR — not a
|
||||||
|
// silent truncation. The rule fires at a typed LOCAL initializer, a function
|
||||||
|
// PARAM default, and a struct FIELD default; each emits a narrowing diagnostic
|
||||||
|
// at the offending float and aborts (exit 1). The fix is the integral-fold /
|
||||||
|
// non-integral-error rule shared with the array-dimension path.
|
||||||
|
//
|
||||||
|
// The escape hatch stays open: `y : s64 = xx 1.5` (or `cast(s64) 1.5`)
|
||||||
|
// truncates with no error — exercised on the POSITIVE side (example 0168).
|
||||||
|
//
|
||||||
|
// Regression (issue 0095): `y : s64 = 1.5` silently truncated to 1.
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
Bad :: struct {
|
||||||
|
f : s64 = 3.5; // non-integral field default → error
|
||||||
|
}
|
||||||
|
|
||||||
|
badDefault :: (x : s64 = 2.5) -> s64 { return x; } // non-integral param default → error
|
||||||
|
|
||||||
|
main :: () {
|
||||||
|
y : s64 = 1.5; // non-integral local initializer → error
|
||||||
|
b := Bad.{};
|
||||||
|
print("{}\n", b.f);
|
||||||
|
print("{}\n", badDefault());
|
||||||
|
print("{}\n", y);
|
||||||
|
}
|
||||||
@@ -4,3 +4,4 @@ S=hi
|
|||||||
P_is_null=true
|
P_is_null=true
|
||||||
KE=4 len=4 WE=4.000000
|
KE=4 len=4 WE=4.000000
|
||||||
MF=2.500000 MFR=2.500000
|
MF=2.500000 MFR=2.500000
|
||||||
|
KF=4 len=4 KFE=4
|
||||||
|
|||||||
1
examples/expected/0168-types-integral-float-to-int.exit
Normal file
1
examples/expected/0168-types-integral-float-to-int.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
local=4
|
||||||
|
neg=-2
|
||||||
|
field=4
|
||||||
|
param=6
|
||||||
|
const=8 len=8
|
||||||
|
xx=4 cast=1
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
error: cannot implicitly narrow non-integral float '1.5' to 's64'; use an explicit cast (`xx`/`cast`)
|
||||||
|
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:21:15
|
||||||
|
|
|
||||||
|
21 | y : s64 = 1.5; // non-integral local initializer → error
|
||||||
|
| ^^^
|
||||||
|
|
||||||
|
error: cannot implicitly narrow non-integral float '3.5' to 's64'; use an explicit cast (`xx`/`cast`)
|
||||||
|
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:15:15
|
||||||
|
|
|
||||||
|
15 | f : s64 = 3.5; // non-integral field default → error
|
||||||
|
| ^^^
|
||||||
|
|
||||||
|
error: cannot implicitly narrow non-integral float '2.5' to 's64'; use an explicit cast (`xx`/`cast`)
|
||||||
|
--> examples/1146-diagnostics-nonintegral-float-to-int.sx:18:26
|
||||||
|
|
|
||||||
|
18 | badDefault :: (x : s64 = 2.5) -> s64 { return x; } // non-integral param default → error
|
||||||
|
| ^^^
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
67
issues/0095-typed-local-float-int-narrowing.md
Normal file
67
issues/0095-typed-local-float-int-narrowing.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# 0095 — typed local/decl silently truncates a float initializer to an integer annotation
|
||||||
|
|
||||||
|
> **RESOLVED (F0.11).** Agra ruled the UNIFIED rule (Option B): an implicit
|
||||||
|
> float→int in a typed binding behaves exactly like the array-dimension rule —
|
||||||
|
> an **integral** float FOLDS to its integer (`4.0` → 4, `-2.0` → -2), a
|
||||||
|
> **non-integral** float is a COMPILE ERROR (`1.5`, `4.5`), and an explicit
|
||||||
|
> `xx` / `cast(T)` ALWAYS truncates (the escape). Applied consistently across
|
||||||
|
> typed local / param-default / field-default, typed module CONST, and array
|
||||||
|
> dim — all reusing the single `program_index.floatToIntExact` /
|
||||||
|
> `evalConstIntExpr` facility (no second integral check).
|
||||||
|
>
|
||||||
|
> Fix (`src/ir/lower.zig`, `src/ir/module.zig`):
|
||||||
|
> - `Builder.constFloatInfo` reads a compile-time `const_float` back from its
|
||||||
|
> Ref (value + span).
|
||||||
|
> - `coerceToType` now means IMPLICIT coercion: its `.float_to_int` arm folds an
|
||||||
|
> integral const-float to `constInt`, else emits the narrowing diagnostic.
|
||||||
|
> `coerceExplicit` is the raw truncating path; `xx` (`lowerXX`) and
|
||||||
|
> `cast(T)` route through it so the escape still truncates.
|
||||||
|
> - Field-default lowering (struct-literal pad, named-field default,
|
||||||
|
> `buildDefaultValue`) now coerces the default to the field type at the IR
|
||||||
|
> level (was silently bit-coerced by `emitStructInit`).
|
||||||
|
> - Const path: `typedConstInitFits` accepts an integral float (literal or a
|
||||||
|
> `M + 2.0`-style expression that folds via `evalComptimeInt`); `emitModuleConst`
|
||||||
|
> / `constExprValue` / `globalInitValue` fold an integral float to its int and
|
||||||
|
> reject a non-integral one.
|
||||||
|
>
|
||||||
|
> Regression tests: `examples/0168-types-integral-float-to-int.sx` (positive —
|
||||||
|
> local/field/param/const fold, `xx`/`cast` truncate), `examples/1146-diagnostics-
|
||||||
|
> nonintegral-float-to-int.sx` (negative — local/param/field error), plus the
|
||||||
|
> integral-float const cases added to `examples/0162-types-typed-module-const-
|
||||||
|
> roundtrip.sx`. Non-integral const cases in `examples/1143` stay errors.
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
A typed LOCAL (and likely typed param/field) silently truncates a floating-point
|
||||||
|
initializer to an integer annotation instead of rejecting or requiring an explicit cast.
|
||||||
|
|
||||||
|
Observed:
|
||||||
|
- `y : s64 = 1.5;` → y == 1 (float literal truncated, no diagnostic)
|
||||||
|
- `y : s64 = 2 + 0.5;` → y == 2 (float-valued expr truncated, no diagnostic)
|
||||||
|
|
||||||
|
Expected: a type-mismatch / narrowing diagnostic (consistent with typed MODULE CONSTS,
|
||||||
|
which after F0.7 reject `N : s64 : 1.5` and `N : s64 : M + 0.5`). Today consts are strict
|
||||||
|
but locals are lenient — an inconsistency.
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
```sx
|
||||||
|
#import "modules/std.sx";
|
||||||
|
main :: () {
|
||||||
|
y : s64 = 1.5;
|
||||||
|
print("{}\n", y); // prints 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Investigation prompt
|
||||||
|
Decide + implement the language rule for implicit float→int narrowing in a TYPED binding
|
||||||
|
(local / param / field) initializer. Module consts already reject it (F0.7,
|
||||||
|
registerTypedModuleConst + typedConstInitFits/constExprInitFits). Make typed-local/param/field
|
||||||
|
assignment-coercion consistent: either reject a non-integral float→int initializer with a
|
||||||
|
diagnostic (matching the const path) or require an explicit `xx`/cast. Suspected area: the
|
||||||
|
assignment / typed-binding coercion path (coerceToType ladder, specs.md §"coercion") in
|
||||||
|
src/ir/lower.zig. Verify `y : s64 = 1.5` errors (or requires a cast); confirm integral-float
|
||||||
|
folding rules (specs.md: `4.0`→4 ok, `4.5` rejected) stay consistent. Then gate.
|
||||||
|
|
||||||
|
## Disposition
|
||||||
|
Discovered during F0.7 (issue 0088) attempt-2 review. Agra ruled F0.7 fixes the
|
||||||
|
inferExprType ROOT for binary-op promotion; this typed-LOCAL narrowing is a SEPARATE
|
||||||
|
assignment-coercion concern -> its own scheduled step.
|
||||||
10
readme.md
10
readme.md
@@ -123,6 +123,16 @@ int+float arithmetic promotes to the float in either operand order (`n + 0.5` an
|
|||||||
`0.5 + n` are both `f64`), so `C : s64 : M + 0.5` is rejected regardless of order
|
`0.5 + n` are both `f64`), so `C : s64 : M + 0.5` is rejected regardless of order
|
||||||
while `F : f64 : M + 0.5` folds to `2.5`.
|
while `F : f64 : M + 0.5` folds to `2.5`.
|
||||||
|
|
||||||
|
**Float → integer narrowing (unified rule).** A float flowing into an
|
||||||
|
integer-typed binding *without* a cast follows the same integral-fold rule an
|
||||||
|
array dimension uses: an **integral** float folds to its integer, a
|
||||||
|
**non-integral** float is a compile error. This is uniform across a typed local,
|
||||||
|
a parameter default, a struct field default, and a typed constant —
|
||||||
|
`y : s64 = 4.0` and `K : s64 : 4.0` both give `4` (and `K : s64 : M + 2.0` folds
|
||||||
|
to `4`), while `y : s64 = 1.5` and `N : s64 : 1.5` both error. An explicit
|
||||||
|
`xx` / `cast(s64)` is the escape hatch and always truncates (`y : s64 = xx 1.5`
|
||||||
|
→ `1`).
|
||||||
|
|
||||||
Builtin type names (`s2`, `u8`, `bool`, `string`, …) are reserved and a *bare*
|
Builtin type names (`s2`, `u8`, `bool`, `string`, …) are reserved and a *bare*
|
||||||
spelling can't be used as an identifier at a **value-binding or declaration-name**
|
spelling can't be used as an identifier at a **value-binding or declaration-name**
|
||||||
site — a value binding (`:=` / typed local / parameter), a `::` constant or
|
site — a value binding (`:=` / typed local / parameter), a `::` constant or
|
||||||
|
|||||||
22
specs.md
22
specs.md
@@ -893,6 +893,8 @@ A **count** is a compile-time integer used as an array dimension, a `Vector`
|
|||||||
lane count, or a generic value-param count. Every count must be **integral**: an
|
lane count, or a generic value-param count. Every count must be **integral**: an
|
||||||
integral float (`4.0`, or a float-typed const `N : f64 : 4.0`) folds to its
|
integral float (`4.0`, or a float-typed const `N : f64 : 4.0`) folds to its
|
||||||
integer (`[4.0]s64` ≡ `[4]s64`), while a non-integral float (`4.5`) is rejected.
|
integer (`[4.0]s64` ≡ `[4]s64`), while a non-integral float (`4.5`) is rejected.
|
||||||
|
This is the same integral-float rule a typed binding's float→integer initializer
|
||||||
|
follows (see "Implicit float → integer", §2 Type Conversions).
|
||||||
The accepted *range* of a count is **context-dependent** — zero is legal for
|
The accepted *range* of a count is **context-dependent** — zero is legal for
|
||||||
some counts and not others:
|
some counts and not others:
|
||||||
|
|
||||||
@@ -1418,13 +1420,27 @@ isReady : ValueListenable(bool) = map(
|
|||||||
- Unsigned to strictly wider signed (`u8` → `s16`)
|
- Unsigned to strictly wider signed (`u8` → `s16`)
|
||||||
- Any integer to any float (`u8` → `f32`, `s32` → `f64`)
|
- Any integer to any float (`u8` → `f32`, `s32` → `f64`)
|
||||||
- Float to wider float (`f32` → `f64`)
|
- Float to wider float (`f32` → `f64`)
|
||||||
- Integer and float literals can convert to any numeric type implicitly
|
- Integer literals can convert to any numeric type implicitly
|
||||||
|
|
||||||
**Explicit (narrowing)** — requires `xx` prefix:
|
**Implicit float → integer (the unified narrowing rule)** — a float flowing into
|
||||||
|
an integer-typed binding without `xx`/`cast` is governed by the SAME rule an
|
||||||
|
array dimension / lane count uses (see "Array dimensions are integral", §2):
|
||||||
|
|
||||||
|
- An **integral** compile-time float **folds** to its integer:
|
||||||
|
`y : s64 = 4.0` ≡ `y : s64 = 4`, `n : s64 = -2.0` ≡ `-2`.
|
||||||
|
- A **non-integral** compile-time float is a **compile error**:
|
||||||
|
`y : s64 = 1.5` → "cannot implicitly narrow non-integral float '1.5' to 's64'".
|
||||||
|
- This applies uniformly to a typed **local**, a function **param default**, a
|
||||||
|
struct **field default**, and a typed module **constant**
|
||||||
|
(`K : s64 : 4.0` → 4; `N : s64 : 1.5` → error; `K : s64 : M + 2.0` → 4 when
|
||||||
|
the expression folds to an integer).
|
||||||
|
|
||||||
|
**Explicit (narrowing)** — requires `xx` prefix (or `cast(T)`):
|
||||||
- Integer to narrower integer (`s32` → `u8`)
|
- Integer to narrower integer (`s32` → `u8`)
|
||||||
- Signed to unsigned (`s32` → `u32`)
|
- Signed to unsigned (`s32` → `u32`)
|
||||||
- Float to narrower float (`f64` → `f32`)
|
- Float to narrower float (`f64` → `f32`)
|
||||||
- Float to any integer (`f64` → `u16`)
|
- Float to any integer (`f64` → `u16`) — always **truncates**, integral or not
|
||||||
|
(`y : s64 = xx 1.5` → 1); this is the escape hatch from the implicit rule above
|
||||||
- Unsigned to signed of same or narrower width (`u8` → `s8`)
|
- Unsigned to signed of same or narrower width (`u8` → `s8`)
|
||||||
|
|
||||||
The `xx` prefix operator marks an expression for auto-conversion to the expected type from context (assignment, declaration, argument, return):
|
The `xx` prefix operator marks an expression for auto-conversion to the expected type from context (assignment, declaration, argument, return):
|
||||||
|
|||||||
113
src/ir/lower.zig
113
src/ir/lower.zig
@@ -975,6 +975,20 @@ pub const Lowering = struct {
|
|||||||
/// `classify(.bool, s64)` yields `.widen` and would accept the bogus
|
/// `classify(.bool, s64)` yields `.widen` and would accept the bogus
|
||||||
/// `B : s64 : true`.
|
/// `B : s64 : true`.
|
||||||
fn typedConstInitFits(self: *Lowering, value: *const Node, dst_ty: TypeId) bool {
|
fn typedConstInitFits(self: *Lowering, value: *const Node, dst_ty: TypeId) bool {
|
||||||
|
// An INTEGER-annotated constant accepts a compile-time INTEGRAL float —
|
||||||
|
// a literal (`K : s64 : 4.0`) or an expression that folds to an integer
|
||||||
|
// (`K : s64 : M + 2.0` → 4) — via the SAME `evalConstIntExpr` /
|
||||||
|
// `floatToIntExact` the array-dim path uses. A non-integral float
|
||||||
|
// (`1.5`, `M + 0.5`) folds to null and falls through to the rejecting
|
||||||
|
// checks below, matching the typed-local rule.
|
||||||
|
if (self.isIntEx(dst_ty)) {
|
||||||
|
switch (value.data) {
|
||||||
|
.float_literal, .binary_op, .unary_op => {
|
||||||
|
if (self.evalComptimeInt(value) != null) return true;
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
return switch (value.data) {
|
return switch (value.data) {
|
||||||
// `---` zero-inits at any type.
|
// `---` zero-inits at any type.
|
||||||
.undef_literal => true,
|
.undef_literal => true,
|
||||||
@@ -1071,7 +1085,17 @@ pub const Lowering = struct {
|
|||||||
.null_literal => .null_val,
|
.null_literal => .null_val,
|
||||||
.int_literal => |il| .{ .int = il.value },
|
.int_literal => |il| .{ .int = il.value },
|
||||||
.bool_literal => |bl| .{ .boolean = bl.value },
|
.bool_literal => |bl| .{ .boolean = bl.value },
|
||||||
.float_literal => |fl| .{ .float = fl.value },
|
// A float initializer at an integer-typed global follows the
|
||||||
|
// implicit narrowing rule (integral folds, non-integral errors).
|
||||||
|
.float_literal => |fl| blk: {
|
||||||
|
if (self.isIntEx(var_ty)) {
|
||||||
|
if (program_index_mod.floatToIntExact(fl.value)) |iv| break :blk inst_mod.ConstantValue{ .int = iv };
|
||||||
|
if (self.diagnostics) |d|
|
||||||
|
d.addFmt(.err, v.span, "cannot implicitly narrow non-integral float '{d}' to '{s}'; use an explicit cast (`xx`/`cast`)", .{ fl.value, self.formatTypeName(var_ty) });
|
||||||
|
break :blk null;
|
||||||
|
}
|
||||||
|
break :blk inst_mod.ConstantValue{ .float = fl.value };
|
||||||
|
},
|
||||||
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
|
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
|
||||||
.array_literal => |al| self.constArrayLiteral(al.elements, var_ty) orelse self.diagnoseNonConstGlobal(vd, v),
|
.array_literal => |al| self.constArrayLiteral(al.elements, var_ty) orelse self.diagnoseNonConstGlobal(vd, v),
|
||||||
.struct_literal => |sl| self.constStructLiteral(&sl, var_ty) orelse self.diagnoseNonConstGlobal(vd, v),
|
.struct_literal => |sl| self.constStructLiteral(&sl, var_ty) orelse self.diagnoseNonConstGlobal(vd, v),
|
||||||
@@ -1178,7 +1202,18 @@ pub const Lowering = struct {
|
|||||||
return switch (expr.data) {
|
return switch (expr.data) {
|
||||||
.int_literal => |il| .{ .int = il.value },
|
.int_literal => |il| .{ .int = il.value },
|
||||||
.bool_literal => |bl| .{ .boolean = bl.value },
|
.bool_literal => |bl| .{ .boolean = bl.value },
|
||||||
.float_literal => |fl| .{ .float = fl.value },
|
// A float into an INTEGER destination follows the implicit
|
||||||
|
// narrowing rule: an integral float folds to its int, a
|
||||||
|
// non-integral one is a compile error (not a silent bit-coerce).
|
||||||
|
.float_literal => |fl| blk: {
|
||||||
|
if (self.isIntEx(expected_ty)) {
|
||||||
|
if (program_index_mod.floatToIntExact(fl.value)) |iv| break :blk inst_mod.ConstantValue{ .int = iv };
|
||||||
|
if (self.diagnostics) |d|
|
||||||
|
d.addFmt(.err, expr.span, "cannot implicitly narrow non-integral float '{d}' to '{s}'; use an explicit cast (`xx`/`cast`)", .{ fl.value, self.formatTypeName(expected_ty) });
|
||||||
|
break :blk null;
|
||||||
|
}
|
||||||
|
break :blk inst_mod.ConstantValue{ .float = fl.value };
|
||||||
|
},
|
||||||
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
|
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
|
||||||
.undef_literal => .zeroinit,
|
.undef_literal => .zeroinit,
|
||||||
// A `null` in a pointer (or optional-pointer) field is a
|
// A `null` in a pointer (or optional-pointer) field is a
|
||||||
@@ -4616,8 +4651,13 @@ pub const Lowering = struct {
|
|||||||
if (field_defaults[fi]) |default_expr| {
|
if (field_defaults[fi]) |default_expr| {
|
||||||
const saved_tt = self.target_type;
|
const saved_tt = self.target_type;
|
||||||
self.target_type = sf.ty;
|
self.target_type = sf.ty;
|
||||||
const val = self.lowerExpr(default_expr);
|
const raw = self.lowerExpr(default_expr);
|
||||||
self.target_type = saved_tt;
|
self.target_type = saved_tt;
|
||||||
|
// Coerce the default to the field type at the IR
|
||||||
|
// level (the implicit narrowing rule) so a float
|
||||||
|
// default folds/errors here instead of being
|
||||||
|
// silently bit-coerced by the backend.
|
||||||
|
const val = self.coerceToType(raw, self.builder.getRefType(raw), sf.ty);
|
||||||
fields.append(self.alloc, val) catch unreachable;
|
fields.append(self.alloc, val) catch unreachable;
|
||||||
} else {
|
} else {
|
||||||
fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable;
|
fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable;
|
||||||
@@ -4656,8 +4696,9 @@ pub const Lowering = struct {
|
|||||||
if (field_defaults[fi]) |default_expr| {
|
if (field_defaults[fi]) |default_expr| {
|
||||||
const saved_tt = self.target_type;
|
const saved_tt = self.target_type;
|
||||||
self.target_type = sf.ty;
|
self.target_type = sf.ty;
|
||||||
const val = self.lowerExpr(default_expr);
|
const raw = self.lowerExpr(default_expr);
|
||||||
self.target_type = saved_tt;
|
self.target_type = saved_tt;
|
||||||
|
const val = self.coerceToType(raw, self.builder.getRefType(raw), sf.ty);
|
||||||
fields.append(self.alloc, val) catch unreachable;
|
fields.append(self.alloc, val) catch unreachable;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -7112,7 +7153,7 @@ pub const Lowering = struct {
|
|||||||
if (src_ty == .any) {
|
if (src_ty == .any) {
|
||||||
return self.builder.emit(.{ .unbox_any = .{ .operand = val } }, dst_ty);
|
return self.builder.emit(.{ .unbox_any = .{ .operand = val } }, dst_ty);
|
||||||
}
|
}
|
||||||
return self.coerceToType(val, src_ty, dst_ty);
|
return self.coerceExplicit(val, src_ty, dst_ty);
|
||||||
}
|
}
|
||||||
// Runtime cast — fall through to builtin handling
|
// Runtime cast — fall through to builtin handling
|
||||||
}
|
}
|
||||||
@@ -13934,7 +13975,7 @@ pub const Lowering = struct {
|
|||||||
.coerce => {},
|
.coerce => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = self.coerceToType(operand, src_ty, dst_ty);
|
const result = self.coerceExplicit(operand, src_ty, dst_ty);
|
||||||
|
|
||||||
// User-space fallback via `impl Into(Target) for Source`. Only fires
|
// User-space fallback via `impl Into(Target) for Source`. Only fires
|
||||||
// when the target was explicitly named (not the .s64 default), src and
|
// when the target was explicitly named (not the .s64 default), src and
|
||||||
@@ -14362,8 +14403,9 @@ pub const Lowering = struct {
|
|||||||
if (field_defaults[i]) |default_expr| {
|
if (field_defaults[i]) |default_expr| {
|
||||||
const saved_tt = self.target_type;
|
const saved_tt = self.target_type;
|
||||||
self.target_type = f.ty;
|
self.target_type = f.ty;
|
||||||
const val = self.lowerExpr(default_expr);
|
const raw = self.lowerExpr(default_expr);
|
||||||
self.target_type = saved_tt;
|
self.target_type = saved_tt;
|
||||||
|
const val = self.coerceToType(raw, self.builder.getRefType(raw), f.ty);
|
||||||
field_vals.append(self.alloc, val) catch unreachable;
|
field_vals.append(self.alloc, val) catch unreachable;
|
||||||
} else {
|
} else {
|
||||||
field_vals.append(self.alloc, self.zeroValue(f.ty)) catch unreachable;
|
field_vals.append(self.alloc, self.zeroValue(f.ty)) catch unreachable;
|
||||||
@@ -14402,6 +14444,17 @@ pub const Lowering = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn emitModuleConst(self: *Lowering, ci: ModuleConstInfo) Ref {
|
fn emitModuleConst(self: *Lowering, ci: ModuleConstInfo) Ref {
|
||||||
|
// An integer-typed const whose initializer is a compile-time integer —
|
||||||
|
// an int literal/expression, OR an INTEGRAL float that `typedConstInitFits`
|
||||||
|
// accepted under the unified narrowing rule — materializes as its folded
|
||||||
|
// int through the SAME `evalConstIntExpr` the count / array-dim path uses.
|
||||||
|
// (`K : s64 : 4.0` → 4; `K : s64 : M + 2.0` → 4.) Non-foldable shapes
|
||||||
|
// fall through to the per-kind emitters below.
|
||||||
|
if (self.isIntEx(ci.ty)) {
|
||||||
|
if (self.evalComptimeInt(ci.value)) |iv| {
|
||||||
|
return self.builder.constInt(iv, ci.ty);
|
||||||
|
}
|
||||||
|
}
|
||||||
switch (ci.value.data) {
|
switch (ci.value.data) {
|
||||||
.int_literal => |lit| {
|
.int_literal => |lit| {
|
||||||
// If declared type is float, convert integer value to float constant
|
// If declared type is float, convert integer value to float constant
|
||||||
@@ -14498,9 +14551,26 @@ pub const Lowering = struct {
|
|||||||
return self.emitPlaceholder(field);
|
return self.emitPlaceholder(field);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How a float→int conversion is treated. An IMPLICIT coercion (a typed
|
||||||
|
/// binding initializer) folds an integral compile-time float to its int and
|
||||||
|
/// REJECTS a non-integral one; an EXPLICIT `xx` / `cast` always truncates.
|
||||||
|
const CoerceMode = enum { implicit, explicit };
|
||||||
|
|
||||||
/// Insert a conversion if src_ty and dst_ty differ.
|
/// Insert a conversion if src_ty and dst_ty differ.
|
||||||
/// Handles int widening/narrowing, float widening/narrowing, and int↔float.
|
/// Handles int widening/narrowing, float widening/narrowing, and int↔float.
|
||||||
|
/// IMPLICIT coercion — the typed-binding initializer path. A compile-time
|
||||||
|
/// float narrowing to an integer folds when integral, errors when not.
|
||||||
fn coerceToType(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId) Ref {
|
fn coerceToType(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId) Ref {
|
||||||
|
return self.coerceMode(val, src_ty, dst_ty, .implicit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// EXPLICIT coercion — the `xx` / `cast(T)` escape hatch. A float→int here
|
||||||
|
/// always truncates, bypassing the integral-fold / non-integral-error rule.
|
||||||
|
fn coerceExplicit(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId) Ref {
|
||||||
|
return self.coerceMode(val, src_ty, dst_ty, .explicit);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn coerceMode(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId, mode: CoerceMode) Ref {
|
||||||
// PLANNING: classify the built-in coercion (conversions.zig).
|
// PLANNING: classify the built-in coercion (conversions.zig).
|
||||||
// EMISSION: each arm below reproduces the original lowering.
|
// EMISSION: each arm below reproduces the original lowering.
|
||||||
switch (self.coercionResolver().classify(src_ty, dst_ty)) {
|
switch (self.coercionResolver().classify(src_ty, dst_ty)) {
|
||||||
@@ -14534,7 +14604,7 @@ pub const Lowering = struct {
|
|||||||
defer elems.deinit(self.alloc);
|
defer elems.deinit(self.alloc);
|
||||||
for (si.tuple.fields, di.tuple.fields, 0..) |sf, df, i| {
|
for (si.tuple.fields, di.tuple.fields, 0..) |sf, df, i| {
|
||||||
const fv = self.builder.emit(.{ .tuple_get = .{ .base = val, .field_index = @intCast(i), .base_type = src_ty } }, sf);
|
const fv = self.builder.emit(.{ .tuple_get = .{ .base = val, .field_index = @intCast(i), .base_type = src_ty } }, sf);
|
||||||
elems.append(self.alloc, self.coerceToType(fv, sf, df)) catch unreachable;
|
elems.append(self.alloc, self.coerceMode(fv, sf, df, mode)) catch unreachable;
|
||||||
}
|
}
|
||||||
return self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, elems.items) catch unreachable } }, dst_ty);
|
return self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, elems.items) catch unreachable } }, dst_ty);
|
||||||
},
|
},
|
||||||
@@ -14542,14 +14612,14 @@ pub const Lowering = struct {
|
|||||||
.optional_unwrap => {
|
.optional_unwrap => {
|
||||||
const child_ty = self.module.types.get(src_ty).optional.child;
|
const child_ty = self.module.types.get(src_ty).optional.child;
|
||||||
const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty);
|
const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty);
|
||||||
return self.coerceToType(unwrapped, child_ty, dst_ty);
|
return self.coerceMode(unwrapped, child_ty, dst_ty, mode);
|
||||||
},
|
},
|
||||||
// void → Optional: produce null (void is the type of null_literal)
|
// void → Optional: produce null (void is the type of null_literal)
|
||||||
.void_to_optional => return self.builder.constNull(dst_ty),
|
.void_to_optional => return self.builder.constNull(dst_ty),
|
||||||
// Concrete → Optional wrapping (coerce to the inner type first)
|
// Concrete → Optional wrapping (coerce to the inner type first)
|
||||||
.optional_wrap => {
|
.optional_wrap => {
|
||||||
const child_ty = self.module.types.get(dst_ty).optional.child;
|
const child_ty = self.module.types.get(dst_ty).optional.child;
|
||||||
const coerced = self.coerceToType(val, src_ty, child_ty);
|
const coerced = self.coerceMode(val, src_ty, child_ty, mode);
|
||||||
return self.builder.emit(.{ .optional_wrap = .{ .operand = coerced } }, dst_ty);
|
return self.builder.emit(.{ .optional_wrap = .{ .operand = coerced } }, dst_ty);
|
||||||
},
|
},
|
||||||
// Concrete → Protocol (auto type erasure)
|
// Concrete → Protocol (auto type erasure)
|
||||||
@@ -14575,7 +14645,28 @@ pub const Lowering = struct {
|
|||||||
return self.buildProtocolValue(concrete_ptr, proto_name, ctn, dst_ty, concrete_ty, heap_copy);
|
return self.buildProtocolValue(concrete_ptr, proto_name, ctn, dst_ty, concrete_ty, heap_copy);
|
||||||
},
|
},
|
||||||
.int_to_float => return self.builder.emit(.{ .int_to_float = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty),
|
.int_to_float => return self.builder.emit(.{ .int_to_float = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty),
|
||||||
.float_to_int => return self.builder.emit(.{ .float_to_int = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty),
|
.float_to_int => {
|
||||||
|
// Implicit float→int narrowing follows the unified rule (the
|
||||||
|
// same `floatToIntExact` the array-dim / `$K: Count` paths use):
|
||||||
|
// a compile-time INTEGRAL float folds to its int, a NON-integral
|
||||||
|
// one is a compile error. Explicit `xx` / `cast` (mode
|
||||||
|
// `.explicit`) skips this and truncates. A runtime float has no
|
||||||
|
// compile-time value to fold — it truncates as before.
|
||||||
|
if (mode == .implicit) {
|
||||||
|
if (self.builder.constFloatInfo(val)) |info| {
|
||||||
|
if (program_index_mod.floatToIntExact(info.value)) |iv| {
|
||||||
|
return self.builder.constInt(iv, dst_ty);
|
||||||
|
}
|
||||||
|
if (self.diagnostics) |d| {
|
||||||
|
const sp = ast.Span{ .start = info.span.start, .end = info.span.end };
|
||||||
|
d.addFmt(.err, sp, "cannot implicitly narrow non-integral float '{d}' to '{s}'; use an explicit cast (`xx`/`cast`)", .{ info.value, self.formatTypeName(dst_ty) });
|
||||||
|
}
|
||||||
|
// Error already emitted; emit the truncating op so
|
||||||
|
// lowering finishes and `hasErrors()` aborts the build.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self.builder.emit(.{ .float_to_int = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty);
|
||||||
|
},
|
||||||
// Ptr ↔ Int — explicit `xx ptr` to/from an integer-typed slot.
|
// Ptr ↔ Int — explicit `xx ptr` to/from an integer-typed slot.
|
||||||
// Emits a `bitcast` IR op; emit_llvm.zig's bitcast arm dispatches
|
// Emits a `bitcast` IR op; emit_llvm.zig's bitcast arm dispatches
|
||||||
// to LLVMBuildPtrToInt / LLVMBuildIntToPtr at the LLVM level
|
// to LLVMBuildPtrToInt / LLVMBuildIntToPtr at the LLVM level
|
||||||
|
|||||||
@@ -114,3 +114,28 @@ test "Module: globals" {
|
|||||||
try std.testing.expectEqual(GlobalId.fromIndex(0), id);
|
try std.testing.expectEqual(GlobalId.fromIndex(0), id);
|
||||||
try std.testing.expectEqual(TypeId.s32, mod.globals.items[0].ty);
|
try std.testing.expectEqual(TypeId.s32, mod.globals.items[0].ty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "Builder.constFloatInfo reads a const_float back, null for non-floats" {
|
||||||
|
const alloc = std.testing.allocator;
|
||||||
|
var mod = Module.init(alloc);
|
||||||
|
defer mod.deinit();
|
||||||
|
|
||||||
|
var b = Builder.init(&mod);
|
||||||
|
const name = mod.types.internString("f");
|
||||||
|
const entry_name = mod.types.internString("entry");
|
||||||
|
_ = b.beginFunction(name, &.{}, .void);
|
||||||
|
const entry = b.appendBlock(entry_name, &.{});
|
||||||
|
b.switchToBlock(entry);
|
||||||
|
|
||||||
|
// A const_float reads back its value (the implicit float→int rule consults
|
||||||
|
// this to fold an integral literal / locate a non-integral one).
|
||||||
|
const fref = b.constFloat(4.0, .f64);
|
||||||
|
const info = b.constFloatInfo(fref) orelse return error.TestUnexpectedResult;
|
||||||
|
try std.testing.expectEqual(@as(f64, 4.0), info.value);
|
||||||
|
|
||||||
|
// A non-float instruction is not a const_float — null.
|
||||||
|
const iref = b.constInt(7, .s64);
|
||||||
|
try std.testing.expect(b.constFloatInfo(iref) == null);
|
||||||
|
|
||||||
|
b.finalize();
|
||||||
|
}
|
||||||
|
|||||||
@@ -247,6 +247,13 @@ pub const ImplTable = struct {
|
|||||||
// ── Builder ─────────────────────────────────────────────────────────────
|
// ── Builder ─────────────────────────────────────────────────────────────
|
||||||
// Fluent API for constructing one function at a time.
|
// Fluent API for constructing one function at a time.
|
||||||
|
|
||||||
|
/// A `const_float` instruction read back from its Ref: the compile-time value
|
||||||
|
/// and the span it was emitted with.
|
||||||
|
pub const ConstFloatInfo = struct {
|
||||||
|
value: f64,
|
||||||
|
span: Span,
|
||||||
|
};
|
||||||
|
|
||||||
pub const Builder = struct {
|
pub const Builder = struct {
|
||||||
module: *Module,
|
module: *Module,
|
||||||
func: ?FuncId = null,
|
func: ?FuncId = null,
|
||||||
@@ -347,6 +354,28 @@ pub const Builder = struct {
|
|||||||
return .unresolved;
|
return .unresolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If `ref` points at a compile-time `const_float` instruction, return its
|
||||||
|
/// value and the span it was emitted with; else null. The implicit
|
||||||
|
/// float→int coercion rule reads this to fold an integral literal to its
|
||||||
|
/// int (and to locate a non-integral one for its diagnostic).
|
||||||
|
pub fn constFloatInfo(self: *Builder, ref: Ref) ?ConstFloatInfo {
|
||||||
|
if (self.func == null) return null;
|
||||||
|
const func = self.currentFunc();
|
||||||
|
const ref_idx = @intFromEnum(ref);
|
||||||
|
if (ref_idx < func.params.len) return null;
|
||||||
|
for (func.blocks.items) |*block| {
|
||||||
|
const first = block.first_ref;
|
||||||
|
if (ref_idx >= first and ref_idx < first + @as(u32, @intCast(block.insts.items.len))) {
|
||||||
|
const i = block.insts.items[ref_idx - first];
|
||||||
|
return switch (i.op) {
|
||||||
|
.const_float => |v| .{ .value = v, .span = i.span },
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Emit helpers ────────────────────────────────────────────────
|
// ── Emit helpers ────────────────────────────────────────────────
|
||||||
|
|
||||||
pub fn emit(self: *Builder, op: Op, ty: TypeId) Ref {
|
pub fn emit(self: *Builder, op: Op, ty: TypeId) Ref {
|
||||||
|
|||||||
Reference in New Issue
Block a user