The collector is not dropped by the linker, and a literal is where dyn is sharper
Two claims corrected against the thing they claimed about. A named object is linked whole -- symbol-driven selection is an archive rule, and dropping unreached code inside an included object needs -ffunction-sections and --gc-sections, which the link line does not pass. nm on any corpus program finds flan_dyn_add and flan_gc_collect in it. So the file said something the build does not do. What makes --no-gc possible is the other half of the same argument and was already written beside it: nothing refers to flan_dyn.c, so not compiling it is a change at three sites and nowhere else. And the boundary. "Typed Flan has no implicit widening" is true of a value and not of a literal: (g 1) against (defn g [x f64] ...) compiles, because the checker gives the literal the type the parameter asks for, while (defn h [y i64] f64 (g y)) is refused. A dyn value written as 1 has been through flan_dyn_from_i64 and cannot remember, so the same source read as dyn traps where read as typed it does not. Stated where the compiler lane will find it, with the two ways to close it, both of them the compiler's.
This commit is contained in:
parent
7d4bec521e
commit
090054bee1
@ -353,13 +353,39 @@ That is the asymmetry worth defending, because the operators *do* promote (§5).
|
||||
questions. An operator has no stated expectation to violate — `(+ 1 2.5)` was written by somebody who
|
||||
wanted a number and there is one obvious number. An annotation *is* a stated expectation, written down by
|
||||
a person, and quietly turning their `i64` into an `f64` would make the boundary the one place in the
|
||||
language where a type changed without anybody writing it. Typed Flan has no implicit widening; the
|
||||
boundary into typed Flan should not invent some.
|
||||
language where a *value* changed type without anybody writing it. Typed Flan does not widen a value:
|
||||
|
||||
The consequence for the compiler lane: a call from dyn code into `(defn f [x f64] ...)` with an integer
|
||||
argument traps at run time rather than converting. If that turns out to be too sharp in practice, the
|
||||
place to soften it is the *compiler*, by emitting a conversion where the checker can see both sides — not
|
||||
here, where the only thing visible is a tag.
|
||||
```lisp
|
||||
(defn g [x f64] f64 x)
|
||||
(defn h [y i64] f64 (g y)) ; scratch.flan:2:24: expected f64, found i64
|
||||
```
|
||||
|
||||
### The one divergence the compiler lane has to know about
|
||||
|
||||
Typed Flan does not widen a *value*, but a **literal adopts its expected type**. `(g 1)` compiles and
|
||||
prints `1`, because the checker gives the literal `1` the `f64` the parameter asks for; `(vec-new i64)`
|
||||
followed by `(push v 1)` works the same way.
|
||||
|
||||
A dyn value has no such history. Once `1` has been through `flan_dyn_from_i64` it is tagged `int`, and
|
||||
nothing downstream can recover that it was written as a literal in a position that wanted a float. So:
|
||||
|
||||
```lisp
|
||||
(g 1) ; typed: compiles, prints 1
|
||||
(g some-dyn) ; dyn, where some-dyn came from the literal 1: traps, "a float was wanted"
|
||||
```
|
||||
|
||||
That is a real divergence between the same source read as typed and read as dyn, and it is stated here
|
||||
rather than discovered later. Two ways out, both the compiler's and neither this file's:
|
||||
|
||||
1. **Tag the literal at the constructor.** Where the checker can see that an unannotated literal flows to
|
||||
a float context, emit `flan_dyn_from_f64` instead of `flan_dyn_from_i64`. This is the same inference
|
||||
the checker already performs for typed literals and it keeps the boundary sharp.
|
||||
2. **Emit a conversion at the call**, where both sides are visible, rather than softening
|
||||
`flan_dyn_need_f64` — which sees only a tag and could not tell a literal-derived int from a computed
|
||||
one.
|
||||
|
||||
Softening the boundary itself is the option not to take: it would accept `(g (len xs))` as readily as
|
||||
`(g 1)`, and those are not the same mistake.
|
||||
|
||||
---
|
||||
|
||||
@ -406,16 +432,21 @@ is a property of the build and not a wish:
|
||||
back-reference — `flan_rt_init` calling `flan_gc_init`, say, which was the obvious thing to write —
|
||||
would make the collector unconditional and the refusal a lie, so the heap initialises itself lazily on
|
||||
first allocation instead.
|
||||
- Consequently a program that calls no dyn operation pulls nothing out of that object, and the linker
|
||||
leaves it behind.
|
||||
**The linker does not drop it today, and this document will not claim it does.** `flan_dyn.o` is a named
|
||||
object on the link line, not an archive member, and `ld` includes a named object in full — symbol-driven
|
||||
selection is a `.a` rule. Dead-code elimination *inside* an included object needs
|
||||
`-ffunction-sections -fdata-sections -Wl,--gc-sections`, which the link line does not pass. Measured
|
||||
rather than assumed: build any corpus program and `nm` it, and `flan_dyn_add` and `flan_gc_collect` are
|
||||
both there as defined symbols.
|
||||
|
||||
The link line does **not** currently pass `-ffunction-sections`/`-Wl,--gc-sections`, and this document
|
||||
will not claim a drop the build cannot perform. What the above buys is the cheaper mechanism: the object
|
||||
is selected at the *file* level, so `--no-gc` is a one-line change at each of the three sites — the same
|
||||
per-target selection `select_csrcs` already performs for a package's C — rather than an argument with the
|
||||
linker about which sections are live. `flan_dev.c` is compiled into every build today for a reason its
|
||||
comment gives at length (a package's C refers to it and package sources are collected whatever `main`
|
||||
does); `flan_dyn.c` has no such entanglement and can genuinely be left out.
|
||||
What the one-way dependency buys is the better mechanism anyway. The object is selected at the **file**
|
||||
level: `--no-gc` is a one-line change at each of the three sites that name `Runtime_src.dyn_source` — the
|
||||
same per-target selection `select_csrcs` already performs for a package's C — and once it is not
|
||||
compiled, nothing has to be dropped. That only works because nothing else in the runtime refers to it;
|
||||
`flan_dev.c` is compiled into every build today precisely because it *is* referred to (a package's C
|
||||
names it, and package sources are collected whatever `main` does), and its own comment says so at length.
|
||||
`flan_dyn.c` has no such entanglement, and keeping it that way is the constraint this section exists to
|
||||
record.
|
||||
|
||||
The residual-dynamism refusal itself — deciding that a program is *not* fully annotated and saying which
|
||||
form is not — is the compiler's, and is not attempted here.
|
||||
|
||||
21
lib/build.ml
21
lib/build.ml
@ -864,14 +864,19 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = [])
|
||||
let objs =
|
||||
cc ~warn:runtime_warnings Runtime_src.source "flan_rt.c"
|
||||
:: [ cc ~warn:runtime_warnings Runtime_src.dev_source "flan_dev.c";
|
||||
(* The dynamic-value runtime. Compiled into every build for the
|
||||
reason flan_dev.c is, and dropped by the same means: it is its own
|
||||
translation unit and nothing in the other two names a symbol in
|
||||
it, so a program with no dyn operation in it pulls nothing out of
|
||||
this object and the linker leaves it behind. Selecting it away at
|
||||
the file level — which is what `--no-gc` will do once the compiler
|
||||
lane can prove a program is fully annotated — is then a one-line
|
||||
change here rather than an argument with the linker. *)
|
||||
(* The dynamic-value runtime. Compiled into every build today, as
|
||||
flan_dev.c is, but for a weaker reason: nothing refers to it. It is
|
||||
its own translation unit and no line of flan_rt.c or flan_dev.c
|
||||
names a symbol in it, so leaving it out is a change *here* and
|
||||
nowhere else — which is what `--no-gc` will be, once the compiler
|
||||
lane can prove a program is fully annotated.
|
||||
|
||||
The linker is not the mechanism and would not be: a named object is
|
||||
linked whole, and dropping unreached code inside one needs
|
||||
-ffunction-sections and --gc-sections, which this line does not
|
||||
pass. So the selection is at the file level, the same shape
|
||||
[select_csrcs] applies to a package's C. See docs/SPIKE-DYNAMIC.md,
|
||||
"Dropping the collector". *)
|
||||
cc ~warn:runtime_warnings Runtime_src.dyn_source "flan_dyn.c" ]
|
||||
(* 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,
|
||||
|
||||
@ -15,12 +15,13 @@
|
||||
*
|
||||
* flan_rt.c, and nothing else in the tree. The dependency does not run the
|
||||
* other way: no line of flan_rt.c or flan_dev.c names anything defined here.
|
||||
* That is the whole of what makes a `--no-gc` build possible — a program that
|
||||
* calls no dyn operation references no symbol in this translation unit, so the
|
||||
* object contributes nothing but its own size, and the compiler lane is free
|
||||
* to refuse to link it at all. A single back-reference from the release
|
||||
* runtime would make the collector unconditional and the refusal a lie. See
|
||||
* the doc's "Dropping the collector".
|
||||
* That is the whole of what makes a `--no-gc` build possible — not because the
|
||||
* linker drops this object (it does not; a named object is linked whole, and
|
||||
* `nm` on any corpus program finds flan_dyn_add in it), but because nothing
|
||||
* else needs it, so *not compiling it* is a change at the three sites that
|
||||
* name it and nowhere else. A single back-reference from the release runtime
|
||||
* would make the collector unconditional and the refusal a lie. See the doc's
|
||||
* "Dropping the collector".
|
||||
*
|
||||
* ── Threads ───────────────────────────────────────────────────────────
|
||||
*
|
||||
@ -780,12 +781,23 @@ int64_t flan_dyn_need_i64(flan_dyn v) {
|
||||
* an omission: typed Flan has no implicit widening anywhere — [(print-i64 x)]
|
||||
* used to force an explicit [(i64 x)] at every site — and a boundary that
|
||||
* quietly turned an int into a float would be the one place in the language
|
||||
* where a type changed without anybody writing it down. The dyn *operators*
|
||||
* promote, because arithmetic between a 2 and a 2.5 has an obvious answer and
|
||||
* refusing it makes dynamic code worse; the boundary into a typed f64
|
||||
* parameter does not, because there the annotation is somebody's stated
|
||||
* expectation and a mismatch is worth hearing about. That asymmetry is
|
||||
* deliberate and is argued at length in the doc. */
|
||||
* where a *value* changed type without anybody writing it down. The dyn
|
||||
* *operators* promote, because arithmetic between a 2 and a 2.5 has an obvious
|
||||
* answer and refusing it makes dynamic code worse; the boundary into a typed
|
||||
* f64 parameter does not, because there the annotation is somebody's stated
|
||||
* expectation and a mismatch is worth hearing about.
|
||||
*
|
||||
* Where this is sharper than the typed language is a *literal*. `(g 1)`
|
||||
* against `(defn g [x f64] ...)` compiles, because the checker gives the
|
||||
* literal the type the parameter asks for; a dyn value written as `1` has
|
||||
* already been through flan_dyn_from_i64 and cannot remember that it was a
|
||||
* literal. So the same source read as dyn traps where read as typed it does
|
||||
* not. That is a real divergence, it is the compiler's to close — by tagging
|
||||
* such a literal as a float where it can see the context — and it is written
|
||||
* down in the doc's boundary section so that closing it is a decision somebody
|
||||
* makes rather than a surprise somebody meets. Softening the check here is the
|
||||
* option not to take: this function sees a tag and nothing else, so it could
|
||||
* not tell (g 1) from (g (len xs)). */
|
||||
double flan_dyn_need_f64(flan_dyn v) {
|
||||
if (flan_dyn_tag(v) != FLAN_DYN_TAG_FLOAT)
|
||||
trap1(TYPE_TRAP, "f64", "a float was wanted", v);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user