flan/runtime/flan_dyn.h
Joseph Ferano 3e68089cde A parameter with no type is dyn, decided where the type names are all known
The type itself, the ABI its operations call into, and the one decision the
feature could not avoid: (defn f [x y]) is one parameter or two, and which one
depends on whether y names a type.

Parse does not decide it. That lookup is the one its defn comment records being
removed for being wrong twice in one day -- the set of type names is incomplete
at parse time by construction, and macros generating definitions is what
widened the failure. So the vector is carried undecided, as Ast.pitems, and
paired in Check, after every file is loaded, every macro expanded and every
header imported. The set is complete there. It is not complete across time, and
the comment says so: a defstruct written later changes a signature with no edit
to the function.

The return slot stays mandatory and dyn is written out in it. The ambiguity
there has no syntactic resolution at all -- a capitalised head in a list is both
a type application and a struct literal -- so the third state the parameters
needed does not exist for the return type, and ret = None goes on meaning Unit.

What the feature costs, and what is taken back: a slot with no type used to be a
syntax error, so a mistyped type now reads as an extra parameter with no
diagnostic. A name within one edit of a type's gets the resolver's own
did-you-mean, and an unknown capitalised name is reported as the unknown type it
is -- not one parameter in the corpus is capitalised. A lowercase name
resembling no type is the feature working, and is the residual.

The x86 backend refuses dyn by name; both callers already name --llvm, and the
daemon takes that backend by default, so this is the first thing a user of dyn
sees. The JS dialect refuses it too, for the opposite reason -- every value
there is already dynamic and what is missing is only the lowering.

runtime/flan_dyn.h is the fixed ABI. flan_dyn_stub.c stands in until the real
collector lands and says in its header that it verifies nothing about roots.
2026-09-19 05:47:49 +07:00

111 lines
5.4 KiB
C

/* flan_dyn — the dynamic-value ABI.
*
* Milestone 1 of dynamic-by-default. A [flan_dyn] is one machine word, and
* every operation the compiler cannot type statically becomes a call to one of
* the functions below. The compiler emits these declarations from Emit; this
* header is the same contract written for C, and the two are diffed rather
* than trusted to agree.
*
* The word is opaque. Nothing outside the runtime may read a tag out of it,
* because which bits carry the tag is the runtime's business and milestone 2
* moves them: the compiler only ever passes words it was given back to the
* functions here. That is what lets boxing change representation without a
* recompile of the emitter.
*
* Every function takes and returns scalars, for the reason flan_rt.c gives:
* nothing returns a struct by value, so the emitted .ll never has to agree
* with a platform's struct-return ABI.
*/
#ifndef FLAN_DYN_H
#define FLAN_DYN_H
#include <stdint.h>
typedef uint64_t flan_dyn;
/* ── Construction ──────────────────────────────────────────────────────
*
* The typed-to-dyn direction. Integer literals in dyn context box as i64:
* there is one integer width behind a dyn value, so the defaulting question
* that a wider set of boxes would raise does not arise.
*
* [flan_dyn_nil] is the absent value, and is what an [if] with no else branch
* answers in dyn context. It is not Unit — Unit does not box, because a value
* of the zero-sized type carries nothing a dyn word could hold. */
flan_dyn flan_dyn_nil(void);
flan_dyn flan_dyn_from_i64(int64_t v);
flan_dyn flan_dyn_from_f64(double v);
flan_dyn flan_dyn_from_bool(int32_t v);
/* A string, as ptr+len — the shape [T] and string already have in Emit.ll.
* The runtime copies: the bytes behind a Flan string may be a literal in
* rodata or a slice of a buffer the program goes on to write. */
flan_dyn flan_dyn_from_bytes(const uint8_t *ptr, int64_t len);
/* The heterogeneous vector. In milestone 1 this is the runtime's own object
* rather than a Flan (Vec T) that happens to hold dyn words, which is why
* push/at/len on it go through the dyn ops below: the compiler knows only
* that it holds a dyn. */
flan_dyn flan_dyn_vec_new(void);
/* ── Operations ────────────────────────────────────────────────────────
*
* Arithmetic dispatches on what the two words actually hold and traps through
* flan_trap_hook when they do not agree. The message is the runtime's: it is
* the side that knows which pair of types arrived, and a message assembled by
* the compiler could only name the static types, which are dyn and dyn. */
flan_dyn flan_dyn_add(flan_dyn a, flan_dyn b);
flan_dyn flan_dyn_sub(flan_dyn a, flan_dyn b);
flan_dyn flan_dyn_mul(flan_dyn a, flan_dyn b);
flan_dyn flan_dyn_div(flan_dyn a, flan_dyn b);
flan_dyn flan_dyn_rem(flan_dyn a, flan_dyn b);
/* The orderings answer a dyn holding a bool, not a C int: the result of an
* operation on dyn operands is a dyn, so that a comparison can be pushed into
* a heterogeneous vector like anything else. Where the compiler needs an i1 to
* branch on it follows with flan_dyn_need_bool. */
flan_dyn flan_dyn_lt(flan_dyn a, flan_dyn b);
flan_dyn flan_dyn_le(flan_dyn a, flan_dyn b);
flan_dyn flan_dyn_gt(flan_dyn a, flan_dyn b);
flan_dyn flan_dyn_ge(flan_dyn a, flan_dyn b);
/* Structural, and never traps. Two values of unrelated types are not an error
* to compare — they are unequal. This is the one op where a type mismatch has
* an answer instead of a trap, and = is the operator most likely to meet a
* heterogeneous container. */
flan_dyn flan_dyn_eq(flan_dyn a, flan_dyn b);
flan_dyn flan_dyn_len(flan_dyn v);
flan_dyn flan_dyn_at(flan_dyn v, flan_dyn i);
void flan_dyn_set_at(flan_dyn v, flan_dyn i, flan_dyn x);
void flan_dyn_push(flan_dyn v, flan_dyn x);
void flan_dyn_print(flan_dyn v);
/* ── Extraction ────────────────────────────────────────────────────────
*
* The dyn-to-typed direction, and the only one: a dyn reaches a typed slot
* through an annotation the programmer wrote — a typed parameter, a typed
* binding — and never by inference. A mismatch traps; the runtime owns the
* message for the reason given above. */
int64_t flan_dyn_need_i64(flan_dyn v);
double flan_dyn_need_f64(flan_dyn v);
int32_t flan_dyn_need_bool(flan_dyn v);
/* ── Roots ─────────────────────────────────────────────────────────────
*
* The collector is precise, so it has to be told where the live dyn words on
* the machine stack are. A dyn local or temporary that lives across a call or
* an allocation is pushed as a root at its binding and popped at scope exit,
* one [flan_dyn_root_pop] per scope carrying the count the scope pushed.
*
* The address is registered, not the word: the slot is written again while it
* is rooted, and a collection in between has to see the current value. */
void flan_dyn_root_push(flan_dyn *slot);
void flan_dyn_root_pop(int64_t n);
/* Called once from main before any other function here. */
void flan_gc_init(void);
#endif /* FLAN_DYN_H */