flan/test/programs/global-init.flan
Joseph Ferano 495629f5f3 A global's initialiser may be computed, and both backends run it the same way
The x86 backend ran initialisers from .init_array and the LLVM one refused
them by name, so (defvar frame Allocator (arena-new 262144)) — which the
author kept writing — was a program on one backend and an error on the other.
A rule that holds on one backend and not the other is not a rule.

The checker lifts a computed initialiser into a function of its own and the
global's initialiser becomes the call. That is what gives it a frame, which is
the bug underneath the feature: a `let` or a `match` in an initialiser indexed
a slot array of length zero and took the x86 emitter down with an uncaught
Invalid_argument.

Both backends call the lifted initialisers from main, after flan_rt_init and
before a line of the program's own code — Odin's __$startup_runtime shape, not
a constructor, so the runtime is up and the order is the compiler's to choose.
x86 keeps .init_array for one thing only, and it is named: writing the
constant image this backend has no folder for, which is standing in for the
other backend's object image rather than for a program.

The computed globals are sorted by what they read, transitively through the
functions they call, so a global written above the one it reads works and a
ring is refused with every name in it. A reload still re-runs nothing: a new
global with a computed initialiser starts as ZII on both backends.

The refusal that lived in x86.ml is now the checker's and is narrower. Nothing
can escape an initialiser — the handler and restart stacks are empty and every
frame it pushes it also pops — so what is refused is a signal or an
invoke-restart with no handler-bind or restart-case around it, which is inert
by construction. A restart-case inside one is ordinary code, which is what
makes (defvar data (Vec u8) (slurp "level.edn")) an ordinary program.

Three refusals go with the premise they rested on: a container global with a
computed initialiser, a union member in a defvar, and a data type case in one.
A defconst is untouched and keeps all three.

One change here is not about any of that. sand.flan carried an unfinished
line — (defvar game-data (embed (with-allocator frame ))), which parses as a
declaration whose type is (embed ...) — so the checker refused the file and
`dune test` was red at the tip of dev-loop before a line of this landed,
verified by stashing this work and rebuilding. It is commented out rather than
guessed at: the arena above it is the half that works, and what the global
should read is the author's to decide.
2026-09-19 04:24:52 +07:00

76 lines
2.9 KiB
Plaintext

;;;; Computed global initialisers — plan.org, Data model.
;;;;
;;;; A global whose value is not something a linker can write into the image.
;;;; The initialiser is lifted into a function of its own and called at startup,
;;;; from main, after the runtime is up and before a line of the program's own
;;;; code — Odin's __$startup_runtime shape rather than a constructor. Both
;;;; backends do it the same way, which is what the x86 survey is comparing.
;;;;
;;;; What each half of this file is asserting:
;;;;
;;;; - an arena allocated at startup and used from main, which is the form
;;;; the author kept writing: (defvar frame Allocator (arena-new N)).
;;;; - the order. `derived` is written above the global it reads, so the
;;;; declaration order is the wrong one and the sort is what makes it 30.
;;;; - a dependency that runs through a call rather than through the text of
;;;; the initialiser: `via-fn` names no global at all, and the function it
;;;; calls reads one.
;;;; - control flow in an initialiser. Every one of these needs a frame, and
;;;; before the lift there was none — a `let` in an initialiser indexed a
;;;; slot array of length zero and took the compiler down with it.
;;;; - a container loaded by its own initialiser, and a data type case
;;;; written into a global. Both were refused while there was nowhere for
;;;; an initialiser to run.
(defdata Shape [Nothing (Circle [r i32]) (Square [side i32])])
;; Written above `base`, and it reads it.
(defvar derived i64 (* base 10))
(defvar base i64 (+ 1 2))
(defn twice-base [] i64 (* base 2))
;; Names no global; the function it calls does.
(defvar via-fn i64 (+ (twice-base) 1))
;; The author's arena, and the Vec it is meant to hold.
(defvar frame Allocator (arena-new 262144))
;; Control flow, each shape in its own global.
(defn maybe [] (Option i32) (Some 3))
(defvar matched i32 (match (maybe) (Some x) x None 0))
(defvar branched i64 (if (> base 2) (let [k (+ base 1)] (* k 2)) 0))
(defvar counted i64 (let [t (i64 0)] (while (< t 4) (set t (+ t 1))) t))
;; A data type case: a store at startup, where a constant would have needed a
;; byte-level encoder for the payload blob.
(defvar shape Shape (Shape.Circle {.r 7}))
;; And a container whose real value only exists behind an allocator.
(defvar names (Vec i64) (vec-new i64))
(defn main [] i32
(println derived)
(println base)
(println via-fn)
(println matched)
(println branched)
(println counted)
(match shape
(Circle r) (println r)
(Square s) (println s)
Nothing (println -1))
;; The Vec was made with the default allocator at startup and is still the
;; program's when main runs.
(push names 11)
(push names 22)
(println (len (as-slice names)))
(println (at names 1))
(free names)
;; And the arena, used the way sand.flan means to use it.
(with-allocator frame
(let [v (vec-new i64)]
(push v 7)
(println (at v 0))))
(free-all frame)
0)