A numeric cast opens a dyn box itself
This commit is contained in:
parent
2127d4d070
commit
b6ea14f930
72
lib/check.ml
72
lib/check.ml
@ -1674,6 +1674,69 @@ let unbox loc (want : Types.t) (e : Tast.expr) : Tast.expr =
|
||||
then "f64" else "i64"))
|
||||
| _ -> no_dyn_yet loc ~into:false want ""
|
||||
|
||||
(* ── A numeric cast written on a dyn, FIX.org 2026-09-20 ───────────────
|
||||
*
|
||||
[(f64 d)] where [d] is dyn. Until this, the only place in the language
|
||||
that opened a box was a typed parameter, which is why a program that
|
||||
wanted a number out of a dyn had to define a one-line function whose
|
||||
parameter slot did the unboxing and call *that*. A cast is already the
|
||||
operator for "convert this to that", so it is the spelling that should
|
||||
have worked, and now does.
|
||||
|
||||
What is built is a branch on the box's tag, not a call that converts:
|
||||
|
||||
(let ([s d])
|
||||
(if (= (flan_dyn_cast_kind s "file:1:2" "u32" 0) 1)
|
||||
(u32 (flan_dyn_need_f64 s))
|
||||
(u32 (flan_dyn_need_i64 s))))
|
||||
|
||||
Each arm is an ordinary [Cast] over an ordinary [need], so the conversion
|
||||
is *the same node* a typed operand of that type would have produced.
|
||||
That is the point of this shape rather than a coercing runtime entry point
|
||||
that answers the finished number: [(i64 2.5)] is not a bare [fptosi] in
|
||||
this compiler — [Emit.check_cast] range-checks it first and signals
|
||||
ArithError when the value will not fit, and the x86 backend does the same
|
||||
— so a C function returning an [int64_t] would have had to grow its own
|
||||
second opinion about range and NaN, in a second place, for two backends.
|
||||
Here there is nothing to keep in step: [(i64 float-box)] *is* [(i64 x)]
|
||||
with an unbox in front of it, which is exactly what the semantics say.
|
||||
|
||||
[need_f64] and [need_i64] are the trapping entry points, and neither can
|
||||
trap here: each is reached only on the arm where the tag has already been
|
||||
read as its own. The trap that can happen is the runtime's, for a box
|
||||
holding a non-number, and [flan_dyn_cast_kind] owns that sentence — bool
|
||||
included, which is what [flan_dyn_need_i64] already does with a bool at a
|
||||
typed parameter. The cross-kind case does not trap: it converts and warns
|
||||
once for the site, the author's call, recorded in FIX.org.
|
||||
|
||||
The slot exists because the value is read three times — once for the tag,
|
||||
once on whichever arm runs — and the argument may be an arbitrary
|
||||
expression. [unbox_option] above builds the same shape for the same
|
||||
reason. *)
|
||||
let cast_dyn ctx loc (target : Types.t) (got : Tast.expr) : Tast.expr =
|
||||
let s = fresh_slot ctx Types.Dyn in
|
||||
let sv = mk loc Types.Dyn (Tast.Local s) in
|
||||
let want_float = match target with Types.Float _ -> 1 | _ -> 0 in
|
||||
let kind =
|
||||
rt loc (Types.Int Types.I32) "flan_dyn_cast_kind"
|
||||
[ sv; here loc;
|
||||
mk loc Types.String (Tast.Str (Types.to_string target));
|
||||
mk loc (Types.Int Types.I32) (Tast.Int (Int64.of_int want_float, Types.I32)) ]
|
||||
in
|
||||
let is_float =
|
||||
mk loc Types.Bool
|
||||
(Tast.Prim (Tast.Eq,
|
||||
[ kind;
|
||||
mk loc (Types.Int Types.I32) (Tast.Int (1L, Types.I32)) ]))
|
||||
in
|
||||
let arm sym ty = widen loc target (rt loc ty sym [ sv ]) in
|
||||
mk loc target
|
||||
(Tast.Let ([ (s, got) ],
|
||||
[ mk loc target
|
||||
(Tast.If (is_float,
|
||||
arm "flan_dyn_need_f64" dyn_f64,
|
||||
arm "flan_dyn_need_i64" dyn_i64)) ]))
|
||||
|
||||
(* nil is written [nil] and nothing else produces it, so this is the whole of
|
||||
"the checker can see a nil reaching here" — a name, not a dataflow fact.
|
||||
There is no propagation through a [let] or a call in this checker (see
|
||||
@ -6123,9 +6186,16 @@ and named_call ctx ~want loc name args =
|
||||
let a = check ctx (List.hd args) in
|
||||
(match a.Tast.ty with
|
||||
| Types.Enum _ -> ()
|
||||
(* A dyn opens here — [cast_dyn], FIX.org 2026-09-20. Only on this arm:
|
||||
the generic one above casts to a type *variable*, whose [where] clause
|
||||
says the operand is numeric, and a dyn is not what a [numeric?] bound
|
||||
admits. *)
|
||||
| Types.Dyn -> ()
|
||||
| t when Types.is_numeric t -> ()
|
||||
| t -> fail loc "%s converts a number, found %s" name (Types.to_string t));
|
||||
prim (Tast.Cast target) target [ a ]
|
||||
(match a.Tast.ty with
|
||||
| Types.Dyn -> cast_dyn ctx loc target a
|
||||
| _ -> prim (Tast.Cast target) target [ a ])
|
||||
|
||||
(* ── ordinary calls ────────────────────────────────────────────── *)
|
||||
(* A local or a parameter holding a function value, called by the name it is
|
||||
|
||||
@ -3653,6 +3653,11 @@ declare void @flan_dyn_print(i64)
|
||||
declare i64 @flan_dyn_need_i64(i64)
|
||||
declare double @flan_dyn_need_f64(i64)
|
||||
declare i32 @flan_dyn_need_bool(i64)
|
||||
; A numeric cast written on a dyn answers which numeric tag the box holds;
|
||||
; check.ml's [cast_dyn] branches on it and each arm is an ordinary need plus
|
||||
; the ordinary cast. The two slices are the site's location and the target's
|
||||
; name, each crossing as ptr+len.
|
||||
declare i32 @flan_dyn_cast_kind(i64, ptr, i64, ptr, i64, i32)
|
||||
; nil <-> None at an (Option T) boundary, and (Some nil)'s run-time half —
|
||||
; M2 queue item 4, check.ml's [box_option]/[unbox_option] and the [Some]
|
||||
; builtin.
|
||||
|
||||
@ -1139,6 +1139,92 @@ uint8_t flan_dyn_need_bool(flan_dyn v) {
|
||||
return (uint8_t)(dyn_payload(v) ? 1 : 0);
|
||||
}
|
||||
|
||||
/* ── A numeric cast opening a box, FIX.org 2026-09-20 ───────────────────
|
||||
*
|
||||
* [(f64 d)] on a dyn. The *conversion* is not done here: [check.ml] lowers
|
||||
* such a cast to a branch on this function's answer, and each arm is the
|
||||
* ordinary [flan_dyn_need_i64]/[flan_dyn_need_f64] followed by the cast the
|
||||
* emitter already emits for a typed argument of that type. So this function
|
||||
* decides one thing — which of the two numeric tags the box holds — and
|
||||
* everything about the arithmetic (the fptosi range check, NaN, the
|
||||
* narrowing rule) stays where it already was, identical in both backends and
|
||||
* identical to the typed spelling of the same cast.
|
||||
*
|
||||
* Answers 1 for a float box, 0 for an int box. Every other tag traps, with
|
||||
* the same [trap1] sentence the typed boundary's own refusals use — bool
|
||||
* included, which mirrors [flan_dyn_need_i64] refusing a bool today rather
|
||||
* than inventing a new rule for casts.
|
||||
*
|
||||
* [want_float] is what the *target* type is: 1 for f32/f64, 0 for the
|
||||
* integer widths. When it disagrees with the box, the cast still happens —
|
||||
* the author's call, "just coerce it with a warning" — and the warning below
|
||||
* is the whole of what the disagreement costs. A cast is already a
|
||||
* conversion operator, [(f64 5)] converts a typed integer, so converting
|
||||
* across the box is the cast doing its job; the warning exists because the
|
||||
* box's kind was not what the program apparently expected.
|
||||
*
|
||||
* Once per *site*, not per value. These casts sit in per-cell-per-frame
|
||||
* loops — sand.flan runs at 120fps — and a per-occurrence line would be a
|
||||
* flood rather than a diagnostic. The site is the [loc] text [check.ml]
|
||||
* passes in, and the table below is keyed on its *bytes* rather than its
|
||||
* address: the two backends emit their own constants for it and neither
|
||||
* promises that two mentions of one site share one pointer.
|
||||
*
|
||||
* The table is fixed and small because it is only ever as large as the
|
||||
* number of cross-kind cast sites a program has, which is a handful in the
|
||||
* programs this was written for. A program with more than [SITE_MAX] of them
|
||||
* stops deduplicating for the overflow — it still warns, every time, which
|
||||
* is the noisy failure rather than the silent one. Not thread-safe, and
|
||||
* deliberately: a duplicated or dropped line under a race is a diagnostic
|
||||
* that came out twice, and the alternative is a lock on a path that runs per
|
||||
* cast in a frame loop. */
|
||||
|
||||
#define SITE_MAX 64
|
||||
|
||||
static struct { const uint8_t *ptr; int64_t len; } warned_sites[SITE_MAX];
|
||||
static int warned_count;
|
||||
|
||||
static int site_first_time(const uint8_t *loc, int64_t loc_len) {
|
||||
for (int i = 0; i < warned_count; i++)
|
||||
if (warned_sites[i].len == loc_len &&
|
||||
memcmp(warned_sites[i].ptr, loc, (size_t)loc_len) == 0)
|
||||
return 0;
|
||||
if (warned_count < SITE_MAX) {
|
||||
warned_sites[warned_count].ptr = loc;
|
||||
warned_sites[warned_count].len = loc_len;
|
||||
warned_count++;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int32_t flan_dyn_cast_kind(flan_dyn v, const uint8_t *loc, int64_t loc_len,
|
||||
const uint8_t *target, int64_t target_len,
|
||||
int32_t want_float) {
|
||||
int32_t tag = flan_dyn_tag(v);
|
||||
if (tag != FLAN_DYN_TAG_INT && tag != FLAN_DYN_TAG_FLOAT) {
|
||||
/* [trap1] takes the operation as a C string and the target is a Flan
|
||||
* slice, so it is copied out. Every cast name is two or three bytes; the
|
||||
* clamp is for a caller this file cannot see. */
|
||||
char name[8];
|
||||
size_t n = (size_t)target_len < sizeof name - 1 ? (size_t)target_len
|
||||
: sizeof name - 1;
|
||||
memcpy(name, target, n);
|
||||
name[n] = '\0';
|
||||
trap1(TYPE_TRAP, name, "a number was wanted", v);
|
||||
}
|
||||
int32_t is_float = tag == FLAN_DYN_TAG_FLOAT ? 1 : 0;
|
||||
if (is_float != (want_float ? 1 : 0) && site_first_time(loc, loc_len)) {
|
||||
fflush(stdout);
|
||||
fprintf(stderr,
|
||||
"flan %.*s: (%.*s x) found a dyn holding %s, and converted it to "
|
||||
"%.*s — warned once for this site\n",
|
||||
(int)loc_len, (const char *)loc, (int)target_len,
|
||||
(const char *)target, tag_of(v), (int)target_len,
|
||||
(const char *)target);
|
||||
}
|
||||
return is_float;
|
||||
}
|
||||
|
||||
/* nil <-> None at an (Option T) boundary. Cannot trap — every dyn value
|
||||
* answers this one way or the other. */
|
||||
int32_t flan_dyn_is_nil(flan_dyn v) {
|
||||
|
||||
@ -144,6 +144,21 @@ int64_t flan_dyn_need_i64(flan_dyn v);
|
||||
double flan_dyn_need_f64(flan_dyn v);
|
||||
uint8_t flan_dyn_need_bool(flan_dyn v);
|
||||
|
||||
/* A numeric cast written on a dyn — [(f64 d)], [(u32 d)] — FIX.org
|
||||
* 2026-09-20. Unlike the parameter boundary above this one coerces: it
|
||||
* answers which numeric tag the box holds (1 float, 0 int) and the caller
|
||||
* branches, so the conversion itself is the cast the compiler already emits
|
||||
* for a typed operand of that tag. A box holding anything else traps, bool
|
||||
* included, exactly as [flan_dyn_need_i64] refuses one.
|
||||
*
|
||||
* [want_float] says what the target type is, and a disagreement writes one
|
||||
* line to stderr — once per [loc], not once per value, because these casts
|
||||
* run inside frame loops. [loc] and [target] are Flan slices: pointer and
|
||||
* length, not NUL-terminated. */
|
||||
int32_t flan_dyn_cast_kind(flan_dyn v, const uint8_t *loc, int64_t loc_len,
|
||||
const uint8_t *target, int64_t target_len,
|
||||
int32_t want_float);
|
||||
|
||||
/* nil <-> None at an (Option T) boundary, and (Some nil)'s refusal — M2 item
|
||||
* 4. [flan_dyn_is_nil] is the tag test the boundary's runtime half needs and
|
||||
* does not want to build out of [flan_dyn_tag] and a comparison at every call
|
||||
|
||||
@ -296,6 +296,51 @@ int32_t flan_dyn_need_bool(flan_dyn v) {
|
||||
return c->u.b;
|
||||
}
|
||||
|
||||
/* The cast boundary's tag question — see flan_dyn.h. The stub keeps its own
|
||||
* trap vocabulary, as every function above it does; what it must agree with
|
||||
* the real runtime about is the *answer*, 1 for a float box and 0 for an int
|
||||
* one, because that is what the compiler branches on. The once-per-site
|
||||
* table is the real runtime's word for word: a program built against the
|
||||
* stub that warns twice for one line would be a difference in the
|
||||
* diagnostic, which is the thing this pair exists to keep identical. */
|
||||
|
||||
#define STUB_SITE_MAX 64
|
||||
|
||||
static struct { const uint8_t *ptr; int64_t len; } stub_warned[STUB_SITE_MAX];
|
||||
static int stub_warned_count;
|
||||
|
||||
int32_t flan_dyn_cast_kind(flan_dyn v, const uint8_t *loc, int64_t loc_len,
|
||||
const uint8_t *target, int64_t target_len,
|
||||
int32_t want_float) {
|
||||
cell *c = as(v);
|
||||
if (c->tag != T_I64 && c->tag != T_F64)
|
||||
dyn_trap("DynExpectedNumber",
|
||||
"a numeric cast was written on this value and it is not a number");
|
||||
int32_t is_float = c->tag == T_F64 ? 1 : 0;
|
||||
if (is_float != (want_float ? 1 : 0)) {
|
||||
int first = 1;
|
||||
for (int i = 0; i < stub_warned_count; i++)
|
||||
if (stub_warned[i].len == loc_len &&
|
||||
memcmp(stub_warned[i].ptr, loc, (size_t)loc_len) == 0)
|
||||
first = 0;
|
||||
if (first) {
|
||||
if (stub_warned_count < STUB_SITE_MAX) {
|
||||
stub_warned[stub_warned_count].ptr = loc;
|
||||
stub_warned[stub_warned_count].len = loc_len;
|
||||
stub_warned_count++;
|
||||
}
|
||||
fflush(stdout);
|
||||
fprintf(stderr,
|
||||
"flan %.*s: (%.*s x) found a dyn holding %s, and converted it "
|
||||
"to %.*s — warned once for this site\n",
|
||||
(int)loc_len, (const char *)loc, (int)target_len,
|
||||
(const char *)target, is_float ? "float" : "int",
|
||||
(int)target_len, (const char *)target);
|
||||
}
|
||||
}
|
||||
return is_float;
|
||||
}
|
||||
|
||||
/* ── Roots ─────────────────────────────────────────────────────────────
|
||||
*
|
||||
* Recorded and otherwise ignored. The shadow stack is kept, and its depth
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user