The numeric casts open dyn boxes, coercing across kinds with one warning per site
This commit is contained in:
commit
6baed9a0ba
92
FIX.org
92
FIX.org
@ -1033,3 +1033,95 @@ inverting the condition to move the last operand into the else arm costs a
|
||||
[not] per operand and worse locs than it buys. What would fix it is check_if
|
||||
preferring the arm that is not a compiler temp when it decides which one to
|
||||
blame — a change in check.ml, which this lane did not own.
|
||||
|
||||
* A numeric cast opens a dyn box, decided 2026-09-20
|
||||
Every numeric cast — =(f64 x)=, =(i64 x)=, =(u32 x)=, =(f32 x)=, all of
|
||||
them — takes a dyn operand now. Until this, the cast arm refused it with "f64
|
||||
converts a number, found dyn", and the only thing in the language that opened
|
||||
a box was a typed parameter, so a program wanting a number out of a dyn wrote
|
||||
a one-line function whose parameter slot did the unboxing and called *that*.
|
||||
A cast is the operator for "convert this to that"; it is the spelling that
|
||||
should have worked.
|
||||
|
||||
Three cases, and the middle one is the author's call.
|
||||
|
||||
1. Same kind. The box holds what the cast asks for, so the cast is the unbox
|
||||
and nothing else. A dyn box only ever holds an i64, an f64 or a bool among
|
||||
the numbers, so =(f32 d)= on a float box unboxes to f64 and narrows, and
|
||||
=(u32 d)= on an int box unboxes to i64 and narrows — each by the rule the
|
||||
same cast already follows on a typed operand.
|
||||
|
||||
2. Cross kind — COERCE, with a warning. The author's words were "just coerce
|
||||
it with a warning". =(f64 int-box)= is 7 -> 7.0 and =(i64 float-box)= is
|
||||
2.5 -> 2, the truncation toward zero =(i64 2.5)= already does, range-check
|
||||
and ArithError included. This overrides the tempting rule of matching the
|
||||
parameter boundary, which traps on a kind mismatch: 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, not because the
|
||||
conversion is in doubt.
|
||||
|
||||
3. A box holding a non-number traps: text, nil, keyword, vec, map — and
|
||||
*bool*, which is not a special case but the parameter boundary's existing
|
||||
answer mirrored. flan_dyn_need_i64 refuses a dyn holding true at a typed
|
||||
i64 parameter today, and =(i64 d)= refuses it for the same reason and in
|
||||
the same voice.
|
||||
|
||||
** The warning is once per SITE
|
||||
These casts sit in per-cell-per-frame loops — sand.flan runs at 120fps — so a
|
||||
per-occurrence line is a flood and not a diagnostic. check.ml threads the
|
||||
site's loc text into the runtime call and flan_dyn.c keeps a small table of
|
||||
sites it has already spoken about, keyed on the loc's *bytes* rather than its
|
||||
address: the two backends emit their own constants for it and neither promises
|
||||
that two mentions of one site share a pointer. Sixty-four sites, and past that
|
||||
it stops deduplicating rather than stops warning — the noisy failure, not the
|
||||
silent one. The line is:
|
||||
|
||||
flan FILE:LINE:COL: (f64 x) found a dyn holding an int, and converted it to f64 — warned once for this site
|
||||
|
||||
Both builds warn, dev and release. No precedent was found making a diagnostic
|
||||
of this kind dev-only: the allocation registry's notes are the one runtime
|
||||
family a release build drops, and those are a *feature* being disabled, not a
|
||||
warning being hushed. After the first hit this costs a tag compare and a
|
||||
linear scan of a handful of entries, which is nothing.
|
||||
|
||||
** How it lowers, and why that shape
|
||||
check.ml's [cast_dyn] builds a branch, 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))))
|
||||
|
||||
flan_dyn_cast_kind answers 1 for a float box and 0 for an int box, traps for
|
||||
everything else, and warns when the answer disagrees with the target. Each arm
|
||||
is then an ordinary [Cast] over an ordinary need — *the same node* a typed
|
||||
operand of that type would have produced.
|
||||
|
||||
The alternative was a coercing runtime entry point answering the finished
|
||||
number, and it was rejected because =(i64 2.5)= is not a bare fptosi in this
|
||||
compiler: Emit.check_cast range-checks it and signals ArithError when the
|
||||
value will not fit, and lib/x86.ml does the same. 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 — a fork of exactly the kind "Arithmetic
|
||||
semantics do not fork across the two spaces" forbids. With the branch there is
|
||||
nothing to keep in step, and "x86 tracks LLVM -O0" holds by construction:
|
||||
programs/dyn-cast.flan prints byte-identical output on both backends,
|
||||
warnings and trap included.
|
||||
|
||||
The generic cast arm — =(t x)= inside a body with ={:where (numeric? $t)}= —
|
||||
did NOT grow a dyn case and did not need one: the operand's type there is what
|
||||
the bound admits, and numeric? does not admit dyn, so a dyn cannot reach that
|
||||
arm. Pinned in test_flan.ml.
|
||||
|
||||
** What this repeals
|
||||
test_flan.ml's row "a keyword with no expectation converts as dyn" pinned
|
||||
=(i64 :space)= as a *check* error. It is a well-typed program now and a
|
||||
run-time trap instead; the row became an [accepts] saying so. That is the
|
||||
whole of the behaviour change outside the new feature.
|
||||
|
||||
** For the author: the shims in sand.flan can go
|
||||
sand.flan defines =dyn->f64= and =dyn->u32=, one-line functions whose only
|
||||
job is that their parameter slot unboxes. Every call site can now write the
|
||||
cast directly — =(f64 d)=, =(u32 d)= — and the two defns deleted. Not done
|
||||
here: sand.flan is the author's WIP and this lane did not touch it.
|
||||
|
||||
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, is_float ? "a float" : "an int",
|
||||
(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 ? "a float" : "an int",
|
||||
(int)target_len, (const char *)target);
|
||||
}
|
||||
}
|
||||
return is_float;
|
||||
}
|
||||
|
||||
/* ── Roots ─────────────────────────────────────────────────────────────
|
||||
*
|
||||
* Recorded and otherwise ignored. The shadow stack is kept, and its depth
|
||||
|
||||
72
test/programs/dyn-cast.flan
Normal file
72
test/programs/dyn-cast.flan
Normal file
@ -0,0 +1,72 @@
|
||||
;;;; A numeric cast opens a dyn box — FIX.org 2026-09-20.
|
||||
;;;;
|
||||
;;;; Before this, a typed parameter was the only thing in the language that
|
||||
;;;; could open a box, so a program wanting a number out of a dyn wrote a
|
||||
;;;; one-line function whose parameter slot did the unboxing and called that.
|
||||
;;;; A cast is the operator for "convert this to that", so it is the spelling
|
||||
;;;; that should have worked; here it does.
|
||||
;;;;
|
||||
;;;; Three behaviours, in the order main runs them:
|
||||
;;;;
|
||||
;;;; 1. Same kind. The box holds what the cast asks for, so the cast is the
|
||||
;;;; unbox and nothing else is written or warned.
|
||||
;;;; 2. Cross kind. The box holds the other number. The cast still converts —
|
||||
;;;; exactly as the same cast converts a typed operand of that type — and
|
||||
;;;; one line goes to stderr naming the site. The two cross-kind casts here
|
||||
;;;; sit inside a dotimes that runs them eight times each, and stderr gets
|
||||
;;;; two lines, because the warning is per *site*: these casts live in frame
|
||||
;;;; loops and a per-value line would be a flood rather than a diagnostic.
|
||||
;;;; 3. A box holding something that is not a number. That still traps, the way
|
||||
;;;; a typed parameter traps on it today, and the runtime owns the sentence.
|
||||
;;;; It is the last thing main does, so everything above it has printed.
|
||||
|
||||
;; The box. Unannotated parameters are dyn, so this is the boxing site and the
|
||||
;; call below is where a typed literal crosses in.
|
||||
(defn as-dyn [x] dyn x)
|
||||
|
||||
(defn main [] ()
|
||||
(let [i (as-dyn 7)
|
||||
f (as-dyn 2.5)]
|
||||
;; ── 1. Same kind: the cast is the unbox ──────────────────────────
|
||||
;; An int box to every integer width, and a float box to both floats.
|
||||
;; None of these warns: the box holds what was asked for.
|
||||
(print (i64 i))
|
||||
(print "\n")
|
||||
(print (i64 (i32 i)))
|
||||
(print "\n")
|
||||
(print (i64 (u32 i)))
|
||||
(print "\n")
|
||||
(print (i64 (u8 i)))
|
||||
(print "\n")
|
||||
(print (f64 f))
|
||||
(print "\n")
|
||||
(print (f64 (f32 f)))
|
||||
(print "\n")
|
||||
|
||||
;; ── 2. Cross kind: it converts, and warns once for the site ──────
|
||||
;; (f64 int-box) is 7 -> 7.0, the same widening (f64 7) does on a typed
|
||||
;; operand. (i64 float-box) is 2.5 -> 2, the same truncation toward zero
|
||||
;; (i64 2.5) does — range-checked by the emitter's own check_cast, because
|
||||
;; the arm really is that cast with an unbox in front of it.
|
||||
;;
|
||||
;; Eight turns of the loop, two sites, two lines on stderr.
|
||||
(dotimes [n 8]
|
||||
(print (f64 i))
|
||||
(print "\n")
|
||||
(print (i64 f))
|
||||
(print "\n"))
|
||||
|
||||
;; A third site, and the third warning. An int box straight to f32 is one
|
||||
;; sitofp from i64, which is what (f32 x) on a typed i64 is. Written down
|
||||
;; because the tempting "fix" — go through f64 and then narrow — rounds
|
||||
;; twice and is a different number for values an i64 can hold and an f64
|
||||
;; cannot round to an f32 the same way.
|
||||
(print (f64 (f32 i)))
|
||||
(print "\n"))
|
||||
|
||||
;; ── 3. And the box that holds no number ──────────────────────────────
|
||||
;; A bool, which is what the typed parameter boundary already refuses: a dyn
|
||||
;; holding true does not satisfy an i64 there and does not satisfy (i64 x)
|
||||
;; here. The runtime owns the message.
|
||||
(print (i64 (as-dyn true)))
|
||||
(print "\n"))
|
||||
@ -3730,6 +3730,83 @@ level "1"
|
||||
some_nil ~opt:"-O0" ();
|
||||
some_nil ~x86:true ();
|
||||
|
||||
(* A numeric cast opening a dyn box — FIX.org 2026-09-20.
|
||||
programs/dyn-cast.flan is one program because the three behaviours are
|
||||
one story told in order: the same-kind casts print, the cross-kind ones
|
||||
print and warn, and the non-numeric box ends the process. The trap is
|
||||
last for the reason [dyn_boundary]'s is — one program, one ending.
|
||||
|
||||
Three things are asserted, and the third is the one that needed the
|
||||
work. First, the numbers: the same-kind casts are pure unboxes, and the
|
||||
cross-kind ones convert the way the same cast converts a typed operand,
|
||||
so (f64 int-box) is 7 and (i64 float-box) is 2 — the truncation toward
|
||||
zero (i64 2.5) already does, not a rounding this feature invented.
|
||||
|
||||
Second, the trap. A bool box is refused by [flan_dyn_cast_kind], which
|
||||
is the decision to mirror the typed parameter boundary rather than to
|
||||
invent a rule: a dyn holding true does not satisfy an i64 parameter
|
||||
today either.
|
||||
|
||||
Third, ONCE PER SITE. The two cross-kind casts in the middle sit inside
|
||||
a [dotimes] that runs eight times, so a per-value warning would put
|
||||
sixteen lines on stderr; what is counted here is three — one for each
|
||||
of the program's three cross-kind cast *sites*, the third being the
|
||||
(f32 int-box) after the loop. That count is the whole reason the
|
||||
runtime carries a table of sites at all: these casts live in frame
|
||||
loops, sand.flan's at 120fps, and a flood is not a diagnostic.
|
||||
|
||||
[run] merges stderr into stdout, so the warnings and the numbers come
|
||||
back in one string and the count is a count over it. *)
|
||||
let dyn_cast_out = "7\n7\n7\n7\n2.5\n2.5\n" in
|
||||
let dyn_cast ?opt ?x86 () =
|
||||
let exe = compile ?opt ?x86 "programs/dyn-cast.flan" in
|
||||
let code, text = run exe None in
|
||||
let name =
|
||||
"dyn: a numeric cast opens the box, and warns once per site"
|
||||
^ (match opt with Some o -> ", " ^ o | None -> "")
|
||||
^ (match x86 with Some true -> ", --x86" | _ -> "")
|
||||
in
|
||||
(* Occurrences of the warning's stable half. Counted rather than
|
||||
matched, because what is being pinned is a number and not a
|
||||
sentence. *)
|
||||
let needle = "warned once for this site" in
|
||||
let warnings =
|
||||
let n = String.length needle and m = String.length text in
|
||||
let c = ref 0 in
|
||||
for i = 0 to m - n do
|
||||
if String.sub text i n = needle then incr c
|
||||
done;
|
||||
!c
|
||||
in
|
||||
if code <> 134
|
||||
|| not (contains text dyn_cast_out)
|
||||
(* The two cross-kind conversions, by their values: 7 -> 7.0 and
|
||||
2.5 -> 2, eight times each. *)
|
||||
|| not (contains text "7\n2\n7\n2\n")
|
||||
|| warnings <> 3
|
||||
|| not (contains text "found a dyn holding an int, and converted it to f64")
|
||||
|| not (contains text "found a dyn holding a float, and converted it to i64")
|
||||
|| not (contains text "found a dyn holding an int, and converted it to f32")
|
||||
(* The non-numeric box, in the runtime's own words. *)
|
||||
|| not (contains text "bool, and a number was wanted")
|
||||
then begin
|
||||
incr failures;
|
||||
Printf.printf
|
||||
"FAIL %s\n got: %S (exit %d, %d warnings)\n wanted: %S, \
|
||||
three warnings, then a trap (exit 134)\n"
|
||||
name text code warnings dyn_cast_out
|
||||
end;
|
||||
(try Sys.remove exe with Sys_error _ -> ())
|
||||
in
|
||||
dyn_cast ();
|
||||
dyn_cast ~opt:"-O0" ();
|
||||
(* The dev daemon's default backend. The parity rule (FIX.org, "x86 tracks
|
||||
LLVM -O0") is what this row is: the same numbers, the same three
|
||||
warnings and the same trap, which is only true because the conversion
|
||||
on each arm is the [Cast] node both backends already lowered rather
|
||||
than arithmetic this feature wrote twice. *)
|
||||
dyn_cast ~x86:true ();
|
||||
|
||||
(* ── Typed containers into dyn as views, M2 item 3 ────────────────
|
||||
programs/dyn-view.flan takes its mode from argv, the way bounds.flan
|
||||
does, because a survey and a trap cannot share a process: mode 0 is
|
||||
|
||||
@ -1429,6 +1429,36 @@ let () =
|
||||
(defn main [] i32\n\
|
||||
\ (match (unbox-opt (box-it 42)) (Some x) (if (= x 42) 0 1) None 1))";
|
||||
|
||||
(* A numeric cast opens a dyn box — FIX.org 2026-09-20. The checker's half
|
||||
is small: every cast name the language has admits a dyn operand now, and
|
||||
that is what these rows are. What the cast then *does* is a run-time
|
||||
question and lives in test_acceptance.ml's dyn-cast rows — the same split
|
||||
as the typed boundary, whose refusal is here and whose trap is there. *)
|
||||
accepts "every numeric cast takes a dyn"
|
||||
"(defn d [] dyn 7)\n\
|
||||
(defn main [] i32\n\
|
||||
\ (do (i64 (d)) (i32 (d)) (i16 (d)) (i8 (d))\n\
|
||||
\ (u64 (d)) (u32 (d)) (u16 (d)) (u8 (d))\n\
|
||||
\ (f64 (d)) (f32 (d))\n\
|
||||
\ 0))";
|
||||
(* And a cast the checker could already see was wrong is wrong for the same
|
||||
reason it was: dyn is admitted by name, not by the numeric test going
|
||||
soft. A string is not a number and never reaches a runtime tag. *)
|
||||
rejects_check "a cast still refuses a non-numeric typed operand"
|
||||
"(defn main [] i32 (i64 \"hi\"))"
|
||||
~needle:"converts a number";
|
||||
(* The *generic* cast — [($t x)] in a body whose signature says
|
||||
[{:where (numeric? $t)}] — is a separate arm in the checker and did not
|
||||
grow a dyn case. It did not need one: the operand's type there is what
|
||||
the bound admits, and [numeric?] does not admit dyn, so a dyn cannot
|
||||
reach that arm to begin with. The refusal is the bound's, at the call
|
||||
that would have instantiated it. *)
|
||||
rejects_check "a generic cast's operand is still what its bound admits"
|
||||
"(defn conv [x $t] $t {:where (numeric? $t)} (t x))\n\
|
||||
(defn d [] dyn 7)\n\
|
||||
(defn main [] i32 (i32 (conv (d))))"
|
||||
~needle:"numeric?";
|
||||
|
||||
(* The map operations ride the words the typed map already owns: get, put,
|
||||
len, has-key? — one question, one word, on both sides. has-key? on a
|
||||
typed map still checks against its K. *)
|
||||
@ -1893,8 +1923,16 @@ let () =
|
||||
keyword-specific one. *)
|
||||
rejects_check "a keyword needs an enum"
|
||||
"(defn g [x i32] ()) (defn f [] () (g :space))" ~needle:"is expected here";
|
||||
rejects_check "a keyword with no expectation converts as dyn"
|
||||
"(defn f [] () (print (i64 :space)))" ~needle:"found dyn";
|
||||
(* This row used to be a rejection, and the sentence it wanted was the cast
|
||||
arm's "converts a number, found dyn". FIX.org 2026-09-20 took that
|
||||
refusal away on purpose: a numeric cast opens a dyn box, so [(i64 d)]
|
||||
compiles for every dyn [d] and the question of what the box holds moved
|
||||
to run time. A keyword's box holds no number and traps there —
|
||||
[flan_dyn_cast_kind]'s sentence, the same one a bool's box gets, pinned
|
||||
in test_acceptance.ml's dyn-cast rows. What is left here is that the
|
||||
program is now well-typed, which is the change. *)
|
||||
accepts "a keyword is dyn, and a numeric cast on it is a run-time question"
|
||||
"(defn f [] () (print (i64 :space)))";
|
||||
rejects_check "a keyword that is not a member"
|
||||
"(defenum Key [space 32]) (defn g [k Key] ()) (defn f [] () (g :spcae))"
|
||||
~needle:"has no member :spcae";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user