One unannotated add, answering 5 to the integers and 3.75 to the floats
The boundary and the operators, which are the two halves of dyn being a type rather than a word the checker tolerates. Typed to dyn is implicit and dyn to typed is not, and the asymmetry is the design: boxing loses nothing and can happen wherever a dyn is wanted, while unboxing can fail at run time on a value the compiler cannot inspect, so it happens only where somebody wrote a type. Both go through expect, because expect is already the one place a wanted type meets a produced one, and every annotating site already calls it. Literals take their width from the dyn, not from the default. (defvar x dyn 5) holds an i64 five: the ABI carries one integer width, so the defaulting question never arises, and the literal is built at i64 rather than boxed after defaulting to i32 -- which also means 3000000000 is a dyn integer. An operator with one dyn operand is the runtime's. binary has already checked the second operand against the first, so a mixed pair arrives with the typed side boxed and the fold only has to call flan_dyn_add instead of adding. The comparisons answer bool and not a dyn holding one, because a comparison is almost always the test of an if; a program that wants it as a value boxes it again for free at that boundary. = and != never trap -- two values of unrelated types are unequal, not an error -- and the orderings do. Types.equal had no Dyn case, so dyn was equal to nothing including itself. print hands the whole value to the runtime rather than walking it: every other arm of the structural printer exists because a Flan value carries no header and only the compiler knows what it is, and a dyn is the exact reverse. The compiler carries the dyn runtime the way it already carries flan_rt.c, with the header pasted in front of the stub so there is one self-contained translation unit and one contract.
This commit is contained in:
parent
3e68089cde
commit
c8091bdbf9
@ -848,7 +848,8 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = [])
|
|||||||
cache key via [compile_c]'s [opt]/[tflags] digest — see [cflags]. *)
|
cache key via [compile_c]'s [opt]/[tflags] digest — see [cflags]. *)
|
||||||
let objs =
|
let objs =
|
||||||
cc ~warn:runtime_warnings Runtime_src.source "flan_rt.c"
|
cc ~warn:runtime_warnings Runtime_src.source "flan_rt.c"
|
||||||
:: [ cc ~warn:runtime_warnings Runtime_src.dev_source "flan_dev.c" ]
|
:: cc ~warn:runtime_warnings Runtime_src.dev_source "flan_dev.c"
|
||||||
|
:: [ cc ~warn:runtime_warnings Runtime_src.dyn_source "flan_dyn.c" ]
|
||||||
(* wasi-libc's entry point, which is not [main]. See [wasm_main_source].
|
(* wasi-libc's entry point, which is not [main]. See [wasm_main_source].
|
||||||
Not the browser's: emscripten's start code calls [main] under that name,
|
Not the browser's: emscripten's start code calls [main] under that name,
|
||||||
so the .ll's @main is already the entry point and the shim would be a
|
so the .ll's @main is already the entry point and the shim would be a
|
||||||
@ -1102,7 +1103,8 @@ let macro_module ?(opts = default) ?(csrcs = []) ?(lflags = []) ~macros
|
|||||||
let cc ?(warn = []) src name = compile_c ~opts ~tflags ~warn ~src ~name () in
|
let cc ?(warn = []) src name = compile_c ~opts ~tflags ~warn ~src ~name () in
|
||||||
let objs =
|
let objs =
|
||||||
cc ~warn:runtime_warnings Runtime_src.source "flan_rt.c"
|
cc ~warn:runtime_warnings Runtime_src.source "flan_rt.c"
|
||||||
:: [ cc ~warn:runtime_warnings Runtime_src.dev_source "flan_dev.c" ]
|
:: cc ~warn:runtime_warnings Runtime_src.dev_source "flan_dev.c"
|
||||||
|
:: [ cc ~warn:runtime_warnings Runtime_src.dyn_source "flan_dyn.c" ]
|
||||||
@ (match p.Tast.cshim with
|
@ (match p.Tast.cshim with
|
||||||
| [] -> []
|
| [] -> []
|
||||||
| parts ->
|
| parts ->
|
||||||
|
|||||||
210
lib/check.ml
210
lib/check.ml
@ -1364,10 +1364,131 @@ let type_id name =
|
|||||||
let restart_sig tys =
|
let restart_sig tys =
|
||||||
"(" ^ String.concat " " (List.map Types.to_string tys) ^ ")"
|
"(" ^ String.concat " " (List.map Types.to_string tys) ^ ")"
|
||||||
|
|
||||||
|
(* ── The dyn boundary ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Typed to dyn is implicit and dyn to typed is not. That asymmetry is the
|
||||||
|
whole of the design and it is worth saying why it is not arbitrary.
|
||||||
|
|
||||||
|
Boxing loses nothing: the value goes in and the runtime records what it was.
|
||||||
|
It can happen anywhere a dyn is wanted without a reader being surprised,
|
||||||
|
because nothing about the program's meaning turns on it. Unboxing can fail,
|
||||||
|
at run time, on a value the compiler cannot inspect -- so it happens only
|
||||||
|
where somebody *wrote a type*: a typed parameter, a typed binding, a typed
|
||||||
|
field. Those are the places a reader already understands as a claim about
|
||||||
|
what a value is, and a claim that can be wrong is exactly what a trap is
|
||||||
|
for. Nowhere else does the compiler decide a dyn is an i64 on its own.
|
||||||
|
|
||||||
|
Both directions go through [expect], because [expect] is already the one
|
||||||
|
place a wanted type meets a produced one. Every site that annotates -- and
|
||||||
|
only those sites -- calls it with [~want].
|
||||||
|
|
||||||
|
Milestone 1 boxes the scalars and refuses everything else by name. A typed
|
||||||
|
container crossing into dyn is the interesting refusal: [(Vec i64)] has a
|
||||||
|
representation the dyn runtime does not know how to walk, and heterogeneity
|
||||||
|
at milestone 1 is served by the runtime's own vector behind
|
||||||
|
[flan_dyn_vec_new] instead. That is a "not yet" and says so. *)
|
||||||
|
|
||||||
|
let dyn_i64 = Types.Int Types.I64
|
||||||
|
let dyn_f64 = Types.Float Types.F64
|
||||||
|
|
||||||
|
(* Widening to the one width the ABI carries. runtime/flan_dyn.h boxes integers
|
||||||
|
as [i64] and floats as [f64] and offers no other width, which is the
|
||||||
|
language's "dyn integers are i64" written where it is enforced. The cast is
|
||||||
|
explicit in the tree rather than left to the backend: a [Cast] is what the
|
||||||
|
language's own conversions emit, and a widening one loses nothing. *)
|
||||||
|
let widen loc (want : Types.t) (e : Tast.expr) =
|
||||||
|
if Types.equal want e.Tast.ty then e
|
||||||
|
else mk loc want (Tast.Prim (Tast.Cast want, [ e ]))
|
||||||
|
|
||||||
|
let unboxable t =
|
||||||
|
match t with
|
||||||
|
| Types.Int Types.I64 | Types.Float Types.F64 | Types.Bool -> true
|
||||||
|
| _ -> false
|
||||||
|
|
||||||
|
(* The sentence a refusal at this boundary gives. It names the type and says
|
||||||
|
which direction failed, because "expected dyn, found (Vec i64)" would read
|
||||||
|
as a type error the programmer could fix by writing something else, and
|
||||||
|
there is nothing else to write -- the feature is not there yet. *)
|
||||||
|
let no_dyn_yet loc ~into t extra =
|
||||||
|
Loc.failk "check/dyn-not-yet" loc
|
||||||
|
"%s does not cross into %s yet%s"
|
||||||
|
(Types.to_string t) (if into then "dyn" else "a written type") extra
|
||||||
|
|
||||||
|
let box loc (e : Tast.expr) : Tast.expr =
|
||||||
|
let dyn sym args = rt loc Types.Dyn sym args in
|
||||||
|
match e.Tast.ty with
|
||||||
|
| Types.Dyn -> e
|
||||||
|
| Types.Int _ -> dyn "flan_dyn_from_i64" [ widen loc dyn_i64 e ]
|
||||||
|
| Types.Float _ -> dyn "flan_dyn_from_f64" [ widen loc dyn_f64 e ]
|
||||||
|
(* The ABI takes an [int32_t], because a C signature that says [_Bool] is a
|
||||||
|
width argument nobody wants to have. *)
|
||||||
|
| Types.Bool -> dyn "flan_dyn_from_bool" [ widen loc (Types.Int Types.I32) e ]
|
||||||
|
(* A string is ptr+len and arrives as two arguments, the way every other
|
||||||
|
(ptr, len) entry point in the runtime takes one. The runtime copies: the
|
||||||
|
bytes may be a literal or a slice of a buffer the program goes on to
|
||||||
|
write. *)
|
||||||
|
| Types.String -> dyn "flan_dyn_from_bytes" [ e ]
|
||||||
|
(* Unit does not box. A value of the zero-sized type carries nothing for a
|
||||||
|
dyn word to hold, and [nil] -- which is what an [if] with no else answers
|
||||||
|
in dyn context -- is a different thing with a different constructor. The
|
||||||
|
two get confused if unit is allowed to become one. *)
|
||||||
|
| Types.Unit ->
|
||||||
|
Loc.failk "check/dyn-unit" loc
|
||||||
|
"() does not box into dyn — a value of the zero-sized type carries \
|
||||||
|
nothing a dyn could hold. The absent dyn value is nil, which is what an \
|
||||||
|
if with no else branch answers here"
|
||||||
|
| Types.Never -> e
|
||||||
|
| Types.Vec _ | Types.Map _ | Types.Slice _ | Types.Array _ ->
|
||||||
|
no_dyn_yet loc ~into:true e.Tast.ty
|
||||||
|
". The dyn container at this milestone is the runtime's own, from \
|
||||||
|
(vec-new dyn); a typed container has a representation the dyn runtime \
|
||||||
|
cannot walk"
|
||||||
|
| Types.Named _ | Types.Enum _ | Types.Option _ | Types.Ptr _
|
||||||
|
| Types.Alloc | Types.Fn _ | Types.Var _ ->
|
||||||
|
no_dyn_yet loc ~into:true e.Tast.ty ""
|
||||||
|
|
||||||
|
let unbox loc (want : Types.t) (e : Tast.expr) : Tast.expr =
|
||||||
|
let need sym ty = rt loc ty sym [ e ] in
|
||||||
|
match want with
|
||||||
|
| Types.Int Types.I64 -> need "flan_dyn_need_i64" dyn_i64
|
||||||
|
| Types.Float Types.F64 -> need "flan_dyn_need_f64" dyn_f64
|
||||||
|
| Types.Bool ->
|
||||||
|
(* The ABI answers an [int32_t]; [bool] is an [i1]. The narrowing is the
|
||||||
|
language's own cast and cannot fail — the runtime already decided the
|
||||||
|
value was a bool, so what comes back is 0 or 1. *)
|
||||||
|
widen loc Types.Bool (need "flan_dyn_need_bool" (Types.Int Types.I32))
|
||||||
|
(* Every other width is refused rather than served by a need_i64 and a
|
||||||
|
truncation. This language has no implicit narrowing anywhere, and putting
|
||||||
|
one at the boundary where a value's type was *already* uncertain is the
|
||||||
|
worst place in the program to start: the annotation would read as a check
|
||||||
|
and would be a silent discard of the high bits. The ABI grows a per-width
|
||||||
|
entry point when there is a reason to; until then the spelling that works
|
||||||
|
is an i64 and an explicit conversion after it. *)
|
||||||
|
| Types.Int _ | Types.Float _ ->
|
||||||
|
no_dyn_yet loc ~into:false want
|
||||||
|
(Printf.sprintf
|
||||||
|
" — the dyn runtime carries integers as i64 and floats as f64, so \
|
||||||
|
take it as %s and convert"
|
||||||
|
(if Types.is_numeric want && (match want with Types.Float _ -> true | _ -> false)
|
||||||
|
then "f64" else "i64"))
|
||||||
|
| _ -> no_dyn_yet loc ~into:false want ""
|
||||||
|
|
||||||
let expect loc ~want (got : Tast.expr) =
|
let expect loc ~want (got : Tast.expr) =
|
||||||
match want with
|
match want with
|
||||||
| None -> got
|
| None -> got
|
||||||
| Some w ->
|
| Some w ->
|
||||||
|
(* The boundary, and the only implicit conversion in the language. It runs
|
||||||
|
before [fits] rather than instead of it: what comes back is an ordinary
|
||||||
|
expression of the wanted type, and if the coercion did not produce one
|
||||||
|
the usual message is still the one that reports it. *)
|
||||||
|
let got =
|
||||||
|
match w, got.Tast.ty with
|
||||||
|
| Types.Dyn, Types.Dyn -> got
|
||||||
|
| Types.Dyn, _ -> box loc got
|
||||||
|
| _, Types.Dyn when Types.fits ~expected:w ~actual:Types.Dyn -> got
|
||||||
|
| _, Types.Dyn -> unbox loc w got
|
||||||
|
| _ -> got
|
||||||
|
in
|
||||||
if Types.fits ~expected:w ~actual:got.Tast.ty then got
|
if Types.fits ~expected:w ~actual:got.Tast.ty then got
|
||||||
else
|
else
|
||||||
fail loc "expected %s, found %s" (Types.to_string w)
|
fail loc "expected %s, found %s" (Types.to_string w)
|
||||||
@ -1698,6 +1819,12 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr =
|
|||||||
match e.Ast.e with
|
match e.Ast.e with
|
||||||
| Ast.Int n -> int_literal loc ~want n
|
| Ast.Int n -> int_literal loc ~want n
|
||||||
| Ast.Byte b -> int_literal loc ~want ~default:Types.U8 (Int64.of_int b)
|
| Ast.Byte b -> int_literal loc ~want ~default:Types.U8 (Int64.of_int b)
|
||||||
|
(* The float literal's own dyn case, for the reason the integer's has one:
|
||||||
|
the ABI carries one width and the literal is built at it. f64 is already
|
||||||
|
what an unconstrained float literal defaults to, so this only has to stop
|
||||||
|
the "expected dyn, found the float literal" arm below from firing. *)
|
||||||
|
| Ast.Float x when want = Some Types.Dyn ->
|
||||||
|
box loc (mk loc dyn_f64 (Tast.Float (x, Types.F64)))
|
||||||
| Ast.Float x ->
|
| Ast.Float x ->
|
||||||
let k =
|
let k =
|
||||||
match want with
|
match want with
|
||||||
@ -1932,6 +2059,16 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr =
|
|||||||
and int_literal loc ~want ?(default = Types.I32) n =
|
and int_literal loc ~want ?(default = Types.I32) n =
|
||||||
match want with
|
match want with
|
||||||
| Some (Types.Int k) -> mk loc (Types.Int k) (Tast.Int (in_range loc k n, k))
|
| Some (Types.Int k) -> mk loc (Types.Int k) (Tast.Int (in_range loc k n, k))
|
||||||
|
(* A literal in dyn position takes i64 and not the i32 an unconstrained one
|
||||||
|
defaults to. This is where "dyn integers are i64" stops being a statement
|
||||||
|
about the ABI and becomes one about the language: [(defvar x dyn 5)] holds
|
||||||
|
an i64 five, and the defaulting question a wider set of boxes would raise
|
||||||
|
never arises because there is only the one box. Handled here rather than
|
||||||
|
left to [expect] so the literal is *built* at the right width — the range
|
||||||
|
check below is the one that matters, and 3000000000 is a dyn integer even
|
||||||
|
though it is not an i32. *)
|
||||||
|
| Some Types.Dyn ->
|
||||||
|
box loc (mk loc dyn_i64 (Tast.Int (in_range loc Types.I64 n, Types.I64)))
|
||||||
(* An untyped integer constant is usable where a float is wanted, as in
|
(* An untyped integer constant is usable where a float is wanted, as in
|
||||||
Odin. A float literal is never usable where an integer is wanted. *)
|
Odin. A float literal is never usable where an integer is wanted. *)
|
||||||
| Some (Types.Float k) ->
|
| Some (Types.Float k) ->
|
||||||
@ -3221,6 +3358,14 @@ and fold_left_prim ctx ~want loc name p ok what args =
|
|||||||
match args with x :: y :: rest -> x, y, rest | _ -> assert false
|
match args with x :: y :: rest -> x, y, rest | _ -> assert false
|
||||||
in
|
in
|
||||||
let a, b = binary ctx name loc ~want:(numeric_want want) [ x; y ] in
|
let a, b = binary ctx name loc ~want:(numeric_want want) [ x; y ] in
|
||||||
|
(* One dyn operand makes the whole fold dyn. [binary] has already checked the
|
||||||
|
second against the first, so a mixed pair arrives with the typed side
|
||||||
|
boxed — [(+ x 1)] over a dyn [x] checked the literal at dyn and got an i64
|
||||||
|
five in a box. What is left is to fold with the runtime's operator instead
|
||||||
|
of the machine's. *)
|
||||||
|
if a.Tast.ty = Types.Dyn || b.Tast.ty = Types.Dyn then
|
||||||
|
dyn_fold ctx ~want loc name [ a; b ] rest
|
||||||
|
else begin
|
||||||
unconstrained ctx.env loc name ~needs:"numeric?" a.Tast.ty;
|
unconstrained ctx.env loc name ~needs:"numeric?" a.Tast.ty;
|
||||||
(* Past [unconstrained] a variable here is one the [where] clause admitted,
|
(* Past [unconstrained] a variable here is one the [where] clause admitted,
|
||||||
so the concrete predicate below has nothing to say about it — it is
|
so the concrete predicate below has nothing to say about it — it is
|
||||||
@ -3236,6 +3381,37 @@ and fold_left_prim ctx ~want loc name p ok what args =
|
|||||||
rest
|
rest
|
||||||
in
|
in
|
||||||
expect loc ~want acc
|
expect loc ~want acc
|
||||||
|
end
|
||||||
|
|
||||||
|
(* The dyn lowering of a fold: one call per operator application, left to
|
||||||
|
right, each taking and answering a dyn word. The typed side of a mixed pair
|
||||||
|
is boxed on the way in — [box] is the identity on something already dyn, so
|
||||||
|
this needs no case analysis of its own. *)
|
||||||
|
and dyn_fold ctx ~want loc name first rest =
|
||||||
|
let sym =
|
||||||
|
match name with
|
||||||
|
| "+" -> "flan_dyn_add" | "-" -> "flan_dyn_sub"
|
||||||
|
| "*" -> "flan_dyn_mul" | "/" -> "flan_dyn_div"
|
||||||
|
| "%" -> "flan_dyn_rem"
|
||||||
|
| _ ->
|
||||||
|
(* Bitwise and shift operators land here if they ever admit a dyn
|
||||||
|
operand. They do not: the runtime carries no bitwise entry points,
|
||||||
|
and an integer operation on a value that might be a float is not
|
||||||
|
something to guess at. *)
|
||||||
|
no_dyn_yet loc ~into:false Types.Dyn
|
||||||
|
(Printf.sprintf " — %s has no dyn form" name)
|
||||||
|
in
|
||||||
|
let apply acc b = rt loc Types.Dyn sym [ acc; box loc b ] in
|
||||||
|
let acc =
|
||||||
|
match first with
|
||||||
|
| [ a; b ] -> apply (box loc a) b
|
||||||
|
| _ -> assert false
|
||||||
|
in
|
||||||
|
let acc =
|
||||||
|
List.fold_left (fun acc arg -> apply acc (check ctx ~want:Types.Dyn arg))
|
||||||
|
acc rest
|
||||||
|
in
|
||||||
|
expect loc ~want acc
|
||||||
|
|
||||||
(* ── Allocation failure, spec-memory.md ────────────────────────────────
|
(* ── Allocation failure, spec-memory.md ────────────────────────────────
|
||||||
No allocating operation returns an error and none can fail silently. When
|
No allocating operation returns an error and none can fail silently. When
|
||||||
@ -3520,10 +3696,14 @@ and named_call ctx ~want loc name args =
|
|||||||
| "%" ->
|
| "%" ->
|
||||||
arity loc name 2 args;
|
arity loc name 2 args;
|
||||||
let a, b = binary ctx name loc ~want:(numeric_want want) args in
|
let a, b = binary ctx name loc ~want:(numeric_want want) args in
|
||||||
|
if a.Tast.ty = Types.Dyn || b.Tast.ty = Types.Dyn then
|
||||||
|
dyn_fold ctx ~want loc name [ a; b ] []
|
||||||
|
else begin
|
||||||
unconstrained ctx.env loc name ~needs:"numeric?" a.Tast.ty;
|
unconstrained ctx.env loc name ~needs:"numeric?" a.Tast.ty;
|
||||||
if not (Types.is_numeric a.Tast.ty || generic_ty a.Tast.ty) then
|
if not (Types.is_numeric a.Tast.ty || generic_ty a.Tast.ty) then
|
||||||
fail loc "%s takes numbers, found %s" name (Types.to_string a.Tast.ty);
|
fail loc "%s takes numbers, found %s" name (Types.to_string a.Tast.ty);
|
||||||
prim Tast.Rem a.Tast.ty [ a; b ]
|
prim Tast.Rem a.Tast.ty [ a; b ]
|
||||||
|
end
|
||||||
| "=" | "!=" | "<" | "<=" | ">" | ">=" ->
|
| "=" | "!=" | "<" | "<=" | ">" | ">=" ->
|
||||||
let p = match name with
|
let p = match name with
|
||||||
| "=" -> Tast.Eq | "!=" -> Tast.Ne | "<" -> Tast.Lt
|
| "=" -> Tast.Eq | "!=" -> Tast.Ne | "<" -> Tast.Lt
|
||||||
@ -3531,6 +3711,35 @@ and named_call ctx ~want loc name args =
|
|||||||
in
|
in
|
||||||
arity loc name 2 args;
|
arity loc name 2 args;
|
||||||
let a, b = binary ctx name loc ~want:None args in
|
let a, b = binary ctx name loc ~want:None args in
|
||||||
|
(* A comparison with a dyn operand answers a *bool*, not a dyn, even though
|
||||||
|
the runtime's own entry point answers a dyn holding one. The reason is
|
||||||
|
where the result goes: a comparison is overwhelmingly the test of an
|
||||||
|
[if] or a [while], and those want an i1. So the need_bool is applied
|
||||||
|
here, once, and a program that really wants the comparison as a dyn
|
||||||
|
value boxes it again on the way into wherever it is going — which [box]
|
||||||
|
does for free at that boundary.
|
||||||
|
|
||||||
|
[=] and [!=] are the pair that never traps: the runtime compares
|
||||||
|
structurally and answers false for values of unrelated types, because
|
||||||
|
two things being unalike is the answer to "are these equal", not an
|
||||||
|
error. The orderings do trap, and rightly — there is no true answer to
|
||||||
|
whether a string is less than a vector. *)
|
||||||
|
if a.Tast.ty = Types.Dyn || b.Tast.ty = Types.Dyn then begin
|
||||||
|
let sym =
|
||||||
|
match name with
|
||||||
|
| "=" | "!=" -> "flan_dyn_eq"
|
||||||
|
| "<" -> "flan_dyn_lt" | "<=" -> "flan_dyn_le"
|
||||||
|
| ">" -> "flan_dyn_gt" | _ -> "flan_dyn_ge"
|
||||||
|
in
|
||||||
|
let cmp = unbox loc Types.Bool (rt loc Types.Dyn sym [ box loc a; box loc b ]) in
|
||||||
|
(* [!=] has no entry point of its own: there is one structural equality
|
||||||
|
and the negation is an [i1] flip the backend folds away. *)
|
||||||
|
let r =
|
||||||
|
if String.equal name "!=" then mk loc Types.Bool (Tast.Prim (Tast.Not, [ cmp ]))
|
||||||
|
else cmp
|
||||||
|
in
|
||||||
|
expect loc ~want r
|
||||||
|
end else begin
|
||||||
(* [=] and [!=] admit one type [<] does not: a handle, which is a pair of
|
(* [=] and [!=] admit one type [<] does not: a handle, which is a pair of
|
||||||
numbers in one word and where "the same entity" is the question the
|
numbers in one word and where "the same entity" is the question the
|
||||||
type exists to answer. Ordering handles would order a slot index, which
|
type exists to answer. Ordering handles would order a slot index, which
|
||||||
@ -3548,6 +3757,7 @@ and named_call ctx ~want loc name args =
|
|||||||
"%s compares machine numbers; %s has no built-in comparison \
|
"%s compares machine numbers; %s has no built-in comparison \
|
||||||
(plan.org, Types)" name (Types.to_string a.Tast.ty);
|
(plan.org, Types)" name (Types.to_string a.Tast.ty);
|
||||||
prim p Types.Bool [ a; b ]
|
prim p Types.Bool [ a; b ]
|
||||||
|
end
|
||||||
| "not" ->
|
| "not" ->
|
||||||
arity loc name 1 args;
|
arity loc name 1 args;
|
||||||
prim Tast.Not Types.Bool [ check ctx ~want:Types.Bool (List.hd args) ]
|
prim Tast.Not Types.Bool [ check ctx ~want:Types.Bool (List.hd args) ]
|
||||||
|
|||||||
@ -4001,7 +4001,8 @@ let merged_executable ~opts ~csrcs ~lflags ~pnames (p : Tast.program) ~out ~ll =
|
|||||||
let cc src name = compile_c ~opts ~tflags ~src ~name () in
|
let cc src name = compile_c ~opts ~tflags ~src ~name () in
|
||||||
let objs =
|
let objs =
|
||||||
(cc Runtime_src.source "flan_rt.c"
|
(cc Runtime_src.source "flan_rt.c"
|
||||||
:: [ cc Runtime_src.dev_source "flan_dev.c" ])
|
:: cc Runtime_src.dev_source "flan_dev.c"
|
||||||
|
:: [ cc Runtime_src.dyn_source "flan_dyn.c" ])
|
||||||
@ (match p.Tast.cshim with
|
@ (match p.Tast.cshim with
|
||||||
| [] -> []
|
| [] -> []
|
||||||
| parts ->
|
| parts ->
|
||||||
|
|||||||
13
lib/dune
13
lib/dune
@ -26,11 +26,19 @@
|
|||||||
; so there is only ever one copy to edit. flan_dev.c goes into a dev build
|
; so there is only ever one copy to edit. flan_dev.c goes into a dev build
|
||||||
; only — it is the run-time name lookup a REPL needs and a release build has
|
; only — it is the run-time name lookup a REPL needs and a release build has
|
||||||
; no use for.
|
; no use for.
|
||||||
|
; [dyn_source] is runtime/flan_dyn_stub.c with runtime/flan_dyn.h pasted in
|
||||||
|
; front of it, because the generated module is one string and the stub includes
|
||||||
|
; the header by name. THE MERGE REPLACES THE STUB WITH runtime/flan_dyn.c and
|
||||||
|
; this rule keeps its shape — the header stays the contract both sides are
|
||||||
|
; diffed against, and concatenating it here is what makes the compiler carry a
|
||||||
|
; self-contained translation unit the way it already carries flan_rt.c.
|
||||||
(rule
|
(rule
|
||||||
(target runtime_src.ml)
|
(target runtime_src.ml)
|
||||||
(deps
|
(deps
|
||||||
%{workspace_root}/runtime/flan_rt.c
|
%{workspace_root}/runtime/flan_rt.c
|
||||||
%{workspace_root}/runtime/flan_dev.c)
|
%{workspace_root}/runtime/flan_dev.c
|
||||||
|
%{workspace_root}/runtime/flan_dyn.h
|
||||||
|
%{workspace_root}/runtime/flan_dyn_stub.c)
|
||||||
(action
|
(action
|
||||||
(with-stdout-to
|
(with-stdout-to
|
||||||
runtime_src.ml
|
runtime_src.ml
|
||||||
@ -39,4 +47,7 @@
|
|||||||
(cat %{workspace_root}/runtime/flan_rt.c)
|
(cat %{workspace_root}/runtime/flan_rt.c)
|
||||||
(echo "|c}\n\nlet dev_source = {c|\n")
|
(echo "|c}\n\nlet dev_source = {c|\n")
|
||||||
(cat %{workspace_root}/runtime/flan_dev.c)
|
(cat %{workspace_root}/runtime/flan_dev.c)
|
||||||
|
(echo "|c}\n\nlet dyn_source = {c|\n")
|
||||||
|
(cat %{workspace_root}/runtime/flan_dyn.h)
|
||||||
|
(cat %{workspace_root}/runtime/flan_dyn_stub.c)
|
||||||
(echo "|c}\n")))))
|
(echo "|c}\n")))))
|
||||||
|
|||||||
30
lib/emit.ml
30
lib/emit.ml
@ -2833,6 +2833,36 @@ declare i64 @flan_alloc_fail_align()
|
|||||||
declare i64 @flan_alloc_fail_id()
|
declare i64 @flan_alloc_fail_id()
|
||||||
declare i64 @flan_alloc_budget(ptr)
|
declare i64 @flan_alloc_budget(ptr)
|
||||||
declare void @flan_alloc_set_budget(ptr, i64)
|
declare void @flan_alloc_set_budget(ptr, i64)
|
||||||
|
; The dynamic runtime, runtime/flan_dyn.h. A flan_dyn is one machine word and
|
||||||
|
; is spelled i64 here because the typedef says uint64_t; nothing in this file
|
||||||
|
; ever looks inside one, so every operation on a dyn value is one of these.
|
||||||
|
declare i64 @flan_dyn_nil()
|
||||||
|
declare i64 @flan_dyn_from_i64(i64)
|
||||||
|
declare i64 @flan_dyn_from_f64(double)
|
||||||
|
declare i64 @flan_dyn_from_bool(i32)
|
||||||
|
declare i64 @flan_dyn_from_bytes(ptr, i64)
|
||||||
|
declare i64 @flan_dyn_vec_new()
|
||||||
|
declare i64 @flan_dyn_add(i64, i64)
|
||||||
|
declare i64 @flan_dyn_sub(i64, i64)
|
||||||
|
declare i64 @flan_dyn_mul(i64, i64)
|
||||||
|
declare i64 @flan_dyn_div(i64, i64)
|
||||||
|
declare i64 @flan_dyn_rem(i64, i64)
|
||||||
|
declare i64 @flan_dyn_lt(i64, i64)
|
||||||
|
declare i64 @flan_dyn_le(i64, i64)
|
||||||
|
declare i64 @flan_dyn_gt(i64, i64)
|
||||||
|
declare i64 @flan_dyn_ge(i64, i64)
|
||||||
|
declare i64 @flan_dyn_eq(i64, i64)
|
||||||
|
declare i64 @flan_dyn_len(i64)
|
||||||
|
declare i64 @flan_dyn_at(i64, i64)
|
||||||
|
declare void @flan_dyn_set_at(i64, i64, i64)
|
||||||
|
declare void @flan_dyn_push(i64, i64)
|
||||||
|
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)
|
||||||
|
declare void @flan_dyn_root_push(ptr)
|
||||||
|
declare void @flan_dyn_root_pop(i64)
|
||||||
|
declare void @flan_gc_init()
|
||||||
declare void @flan_dev_reg_enable()
|
declare void @flan_dev_reg_enable()
|
||||||
declare void @flan_dev_reg_note_vec(ptr, i64, ptr, i64)
|
declare void @flan_dev_reg_note_vec(ptr, i64, ptr, i64)
|
||||||
declare void @flan_dev_reg_note_map(ptr, i64, i64, ptr, i64)
|
declare void @flan_dev_reg_note_map(ptr, i64, i64, ptr, i64)
|
||||||
|
|||||||
@ -352,5 +352,18 @@ let rec render c depth (e : Tast.expr) : Tast.expr list =
|
|||||||
(Tast.While
|
(Tast.While
|
||||||
(cond, lit " " :: render c (depth + 1) elem, [ step ]));
|
(cond, lit " " :: render c (depth + 1) elem, [ step ]));
|
||||||
lit "]" ])) ]
|
lit "]" ])) ]
|
||||||
|
(* The one type this walk does not walk. Every other arm is here because a
|
||||||
|
Flan value carries no header and only the compiler knows what it is; a
|
||||||
|
dyn value is the exact opposite — the runtime knows and the compiler
|
||||||
|
does not — so the printing belongs on the side that can see the tag, and
|
||||||
|
the walk hands the whole value over.
|
||||||
|
|
||||||
|
The cost is that it writes to stdout itself rather than through
|
||||||
|
[c.emit], so a dyn printed at the REPL arrives on the program's output
|
||||||
|
and not in the REPL's buffer. Fixing that means an emit-shaped dyn
|
||||||
|
printer in the runtime — a second entry point taking the sink — and it
|
||||||
|
is not milestone 1's. *)
|
||||||
|
| Types.Dyn ->
|
||||||
|
[ unit_ (Tast.Prim (Tast.Rt "flan_dyn_print", [ e ])) ]
|
||||||
| t ->
|
| t ->
|
||||||
fail loc "no printer for %s" (Types.to_string t)
|
fail loc "no printer for %s" (Types.to_string t)
|
||||||
|
|||||||
@ -97,7 +97,11 @@ let rec equal a b =
|
|||||||
match a, b with
|
match a, b with
|
||||||
| Int x, Int y -> x = y
|
| Int x, Int y -> x = y
|
||||||
| Float x, Float y -> x = y
|
| Float x, Float y -> x = y
|
||||||
| Bool, Bool | String, String | Unit, Unit | Never, Never -> true
|
(* [Dyn] is equal to itself and to nothing else. Two dyn values may hold
|
||||||
|
different things at run time, which is the point of the type and is not
|
||||||
|
this function's question: this is identity of *static* types, and there is
|
||||||
|
one dyn type the way there is one string type. *)
|
||||||
|
| Bool, Bool | String, String | Unit, Unit | Never, Never | Dyn, Dyn -> true
|
||||||
| Named x, Named y | Enum x, Enum y -> String.equal x y
|
| Named x, Named y | Enum x, Enum y -> String.equal x y
|
||||||
| Slice x, Slice y -> equal x y
|
| Slice x, Slice y -> equal x y
|
||||||
| Array (n, x), Array (m, y) -> Int64.equal n m && equal x y
|
| Array (n, x), Array (m, y) -> Int64.equal n m && equal x y
|
||||||
|
|||||||
@ -25,7 +25,16 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include "flan_dyn.h"
|
/* The compiler carries this file as one string with flan_dyn.h pasted in front
|
||||||
|
* of it (lib/dune), and in that form there is no header on disk to find. The
|
||||||
|
* probe keeps the file compilable both ways: standalone against the real
|
||||||
|
* header, and concatenated, where the declarations are already above. The
|
||||||
|
* header's own include guard makes the two agree. */
|
||||||
|
#if defined(__has_include)
|
||||||
|
# if __has_include("flan_dyn.h")
|
||||||
|
# include "flan_dyn.h"
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
|
||||||
/* flan_rt.c's own [rt_trap] is static, so this mirrors it rather than calling
|
/* flan_rt.c's own [rt_trap] is static, so this mirrors it rather than calling
|
||||||
* it: print the sentence, offer the name to the dev daemon's hook, and leave
|
* it: print the sentence, offer the name to the dev daemon's hook, and leave
|
||||||
|
|||||||
15
test/programs/dyn-basic.flan
Normal file
15
test/programs/dyn-basic.flan
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
;;;; An unannotated defn, called at two different types.
|
||||||
|
;;;;
|
||||||
|
;;;; [(defn add [x y] dyn (+ x y))] states no type for either parameter, so both
|
||||||
|
;;;; are dyn, and the + in the body is the dyn one: a call into the runtime that
|
||||||
|
;;;; decides on what the two words actually hold. The same function serves the
|
||||||
|
;;;; integer call and the float call, which is the whole of what the feature
|
||||||
|
;;;; buys and is not something the typed language could express at all.
|
||||||
|
|
||||||
|
(defn add [x y] dyn (+ x y))
|
||||||
|
|
||||||
|
(defn main [] ()
|
||||||
|
(print (add 2 3))
|
||||||
|
(print "\n")
|
||||||
|
(print (add 1.5 2.25))
|
||||||
|
(print "\n"))
|
||||||
Loading…
x
Reference in New Issue
Block a user