From 85ef56f657ae510391bee99901af56363d368627 Mon Sep 17 00:00:00 2001
From: Joseph Ferano
Date: Sun, 13 Sep 2026 08:07:40 +0700
Subject: [PATCH 1/5] The bindings are committed, and regeneration is what
checks them
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
generated.flan carries the 253 declarations the importer reads out of raylib's
header, so a build needs libraylib linkable and no header at all. The opt-in
no longer decides how many bindings a package has — every build now gets all
425, they are greppable, and they diff when raylib moves.
What that gives up is the build-time check, so `flan generate-c` is the only
thing that writes the file and it compares first: every defstruct against the
header's record, every hand-written declare-c against the header's signature,
and it writes nothing when they disagree. Against the 5.1-dev header on this
machine that is ten real differences and no write.
The 172 hand-written lines stay, and not out of caution. Everything the
generator emits agrees with the header by construction, so diffing generated
output against its own source is a tautology; the hand-written lines were
transcribed by a person, so they are the only thing here a header can
contradict. All ten of those differences came from them.
`bindings` beside `headers` is what survives regeneration, because a hand-edit
to a committed generated file does not. Two directives: `exclude` drops
raylib's three allocator entry points, and `name` gives the 19 generated
predicates the `?` spelling the hand-written ones already use.
---
bin/main.ml | 76 ++++++++++
lib/cimport.ml | 246 +++++++++++++++++++++++++++++++-
lib/load.ml | 12 +-
test/test_flan.ml | 2 +-
vendor/raylib/bindings | 69 +++++++++
vendor/raylib/generated.flan | 267 +++++++++++++++++++++++++++++++++++
vendor/raylib/headers | 50 +++++--
7 files changed, 698 insertions(+), 24 deletions(-)
create mode 100644 vendor/raylib/bindings
create mode 100644 vendor/raylib/generated.flan
diff --git a/bin/main.ml b/bin/main.ml
index 25b9a04..57d0dd1 100644
--- a/bin/main.ml
+++ b/bin/main.ml
@@ -208,6 +208,10 @@ let () =
let imported, dump, env =
Flan.Cimport.header ~loc:(Flan.Loc.make header 0 0) ~header ~flags
~known_structs:(List.map fst structs) ~known_enums ~taken ~bound_syms
+ ~config:
+ (match pkg with
+ | f :: _ -> Flan.Load.binding_config (Filename.dirname f)
+ | [] -> Flan.Cimport.no_config)
in
List.iter
(fun d -> print_endline (Flan.Cimport.decl_source d))
@@ -254,6 +258,77 @@ let () =
x.Flan.Cimport.dflan x.Flan.Cimport.dsym x.Flan.Cimport.dwhy)
ds)
+ (* Regeneration. [import-c] prints what it would produce; this writes it, and
+ the difference between the two is that this one cannot skip the check.
+
+ It reads the header out of the package's own `headers`, not off the
+ command line, because the version that may be read is a property of the
+ package — `headers` is where it says which one, and `link` is where it
+ says which library that has to match. And it reads every .flan in the
+ directory *except* the file it writes, so the hand-written declarations
+ still win and the generated ones are not mistaken for them on the next
+ run.
+
+ Non-zero and nothing written when the package and the header disagree.
+ That is the whole point: the committed file is the one thing here with no
+ second opinion, so the moment of writing it is the only moment left at
+ which the library can contradict it. *)
+ | _ :: "generate-c" :: dir :: _ ->
+ with_errors dir (fun () ->
+ let loc = Flan.Loc.make dir 0 0 in
+ let out = Filename.concat dir "generated.flan" in
+ let ds =
+ List.concat_map
+ (fun f -> Flan.Parse.program (Flan.Reader.read_file f))
+ (List.filter
+ (fun f -> not (String.equal f out))
+ (Flan.Load.entries dir ".flan"))
+ in
+ let config = Flan.Load.binding_config dir in
+ match Flan.Load.header_specs ~loc dir with
+ | [] ->
+ Printf.eprintf
+ "flan generate-c: %s/headers names no header that is there. \
+ Regeneration reads the library's own header, at the version \
+ %s/link names — export it and run this again.\n" dir dir;
+ exit 2
+ | _ :: _ :: _ ->
+ Printf.eprintf
+ "flan generate-c: %s/headers names more than one header, and one \
+ generated file cannot come from several — the declarations would \
+ depend on which was read last.\n" dir;
+ exit 2
+ | [ (h, flags) ] ->
+ let r = Flan.Cimport.regenerate ~loc ~header:h ~flags ~ds ~config ~out in
+ List.iter
+ (fun (n, why) -> Printf.printf ";; refused %s: %s\n" n why)
+ r.Flan.Cimport.ghidden;
+ List.iter
+ (fun (n, why) -> Printf.eprintf "DISAGREES %s: %s\n" n why)
+ r.Flan.Cimport.gstructs;
+ List.iter
+ (fun (x : Flan.Cimport.sig_diff) ->
+ Printf.eprintf "DIFFERS %s (%s): %s\n"
+ x.Flan.Cimport.dflan x.Flan.Cimport.dsym x.Flan.Cimport.dwhy)
+ r.Flan.Cimport.gsigs;
+ if r.Flan.Cimport.gwrote then
+ Printf.printf
+ "wrote %s: %d declarations, %d refused, of %d functions in %s.\n\
+ Every defstruct and every hand-written declare-c agrees with it.\n"
+ out r.Flan.Cimport.gdecls
+ (List.length r.Flan.Cimport.ghidden) r.Flan.Cimport.gfns h
+ else begin
+ Printf.eprintf
+ "flan generate-c: %s disagrees with %s — %d struct layouts and %d \
+ hand-written signatures. Nothing was written: a generated file \
+ made against a header the library does not match is the silent \
+ failure this check exists to prevent.\n"
+ dir h
+ (List.length r.Flan.Cimport.gstructs)
+ (List.length r.Flan.Cimport.gsigs);
+ exit 1
+ end)
+
(* The IR is target-independent — [Emit] writes no triple and no datalayout,
which is what lets one .ll serve both targets — so there is nothing for a
target to change here. Refused rather than accepted and ignored: silently
@@ -398,6 +473,7 @@ let () =
prerr_endline
"usage: flan (read|parse|check|emit|shim) ...\n\
\ flan import-c [package.flan...] [clang flags...]\n\
+ \ flan generate-c \n\
\ flan build [-o out] [--no-bounds-checks] [--dev] \
[--debug] [--sanitize] [--target=wasm32-wasi|web]\n\
\ flan run [args...]\n\
diff --git a/lib/cimport.ml b/lib/cimport.ml
index a251adb..387f5d4 100644
--- a/lib/cimport.ml
+++ b/lib/cimport.ml
@@ -502,6 +502,102 @@ type imported = {
hidden : (string * string) list;
}
+(* ── The config beside the header ──────────────────────────────────── *)
+
+(* Why a config file exists at all, when the importer decides everything else
+ from the header.
+
+ Because the generated declarations are *committed*. A generated file that is
+ read at build time can be hand-corrected and the correction survives, since
+ nothing rewrites it. A generated file that is checked in is rewritten by the
+ next regeneration, so a hand-edit to it is destroyed without anybody being
+ told — which is the worst shape an edit can have. The edits therefore have
+ to live somewhere regeneration *reads* rather than somewhere it writes, and
+ this is that place.
+
+ Two directives, which are the two things the header cannot decide:
+
+ - [exclude], a C symbol or a pattern of them, for what should not be
+ generated at all.
+ - [name], a C symbol and the Flan name it is to take, for where the kebab
+ rule gives something ugly.
+
+ A postprocessing transform pass over the generated file was the alternative
+ and was rejected: a second program to understand, run over text the
+ generator had already committed to. Both directives here are applied
+ *while* the declarations are made, so the file on disk is already what the
+ config says and nothing reads it twice. *)
+
+type config = {
+ excludes : string list;
+ (* C symbol to Flan name. Keyed on the symbol and not on the kebab result,
+ because the symbol is the only spelling that is stable — the whole point
+ of an override is that the kebab result is not what is wanted. *)
+ renames : (string * string) list;
+}
+
+let no_config = { excludes = []; renames = [] }
+
+(* [*] stands for any run of characters and nothing else does anything. Enough
+ for [rl*] or [*Callback], and small enough to read at a glance; a package
+ that needs more than this wants a hand-written declare-c, which it has. *)
+let matches (pat : string) (s : string) =
+ let np = String.length pat and ns = String.length s in
+ let rec go i j =
+ if i = np then j = ns
+ else if pat.[i] = '*' then
+ let rec from k = (k <= ns && go (i + 1) k) || (k < ns && from (k + 1)) in
+ from j
+ else j < ns && Char.equal pat.[i] s.[j] && go (i + 1) (j + 1)
+ in
+ go 0 0
+
+let excluded cfg sym = List.exists (fun p -> matches p sym) cfg.excludes
+
+(* The one place the kebab rule is consulted, so an override is not a special
+ case anywhere below: collisions are computed on the name a function will
+ actually take, which means renaming one of two colliding symbols resolves
+ the collision rather than leaving both refused. *)
+let flan_name cfg sym =
+ match List.assoc_opt sym cfg.renames with Some n -> n | None -> kebab sym
+
+(* The file, in the shape of [headers] and [link] beside it: one directive per
+ line, [#] comments, blank lines ignored. A line that is neither directive is
+ an error rather than a line quietly skipped — the house rule against
+ swallowing things applies to a config as much as to a flag, and a typo in a
+ name override would otherwise show up as a binding under the wrong name. *)
+let read_config path : config =
+ if not (Sys.file_exists path) then no_config
+ else begin
+ let ch = open_in path in
+ let excludes = ref [] and renames = ref [] in
+ let rec go n =
+ match input_line ch with
+ | line ->
+ let t = String.trim line in
+ if t <> "" && t.[0] <> '#' then begin
+ let ws =
+ String.split_on_char ' ' t
+ |> List.concat_map (String.split_on_char '\t')
+ |> List.filter (fun w -> w <> "")
+ in
+ match ws with
+ | [ "exclude"; p ] -> excludes := p :: !excludes
+ | [ "name"; sym; flan ] -> renames := (sym, flan) :: !renames
+ | _ ->
+ close_in ch;
+ fail (Loc.make path n 0)
+ "a line here is `exclude ` or `name `, and this is neither: %s" t
+ end;
+ go (n + 1)
+ | exception End_of_file -> ()
+ in
+ go 1;
+ close_in ch;
+ { excludes = List.rev !excludes; renames = List.rev !renames }
+ end
+
(* [taken] is every name the package already declares, which is what makes the
sidecar additive: a hand-written [(declare-c get-gamepad-name ...)] wins
over the header, and a C symbol already bound by hand is not bound twice —
@@ -510,7 +606,7 @@ type imported = {
[-c] as well as the name itself, because [Shim] generates [foo-c] beside a
[foo] whose signature has a struct in it, and a collision there is refused
for the whole program rather than for the one binding. *)
-let of_dump ~env ~taken ~bound_syms (d : dump) : imported =
+let of_dump ~env ~taken ~bound_syms ~config (d : dump) : imported =
let decls = ref [] and hidden = ref [] in
(* Collisions are found before anything is emitted, and they take *every*
name in the colliding group down with them.
@@ -530,16 +626,32 @@ let of_dump ~env ~taken ~bound_syms (d : dump) : imported =
let candidates =
List.filter (fun f -> not (List.mem f.csym bound_syms)) d.fns
in
+ (* Excluded before anything else looks at them, so an excluded symbol is not
+ in a collision group either — which is one of the things exclusion is for.
+ It still says why, under the name it would have taken: "there is no such
+ binding" and "the package decided against this binding" are different
+ answers and a reader deserves the second one. *)
+ let dropped, candidates =
+ List.partition (fun f -> excluded config f.csym) candidates
+ in
+ List.iter
+ (fun f ->
+ hidden :=
+ (flan_name config f.csym,
+ Printf.sprintf
+ "%s is excluded by the package's binding config, so no declaration is generated for it" f.csym)
+ :: !hidden)
+ dropped;
let groups = Hashtbl.create 512 in
List.iter
(fun f ->
- let k = kebab f.csym in
+ let k = flan_name config f.csym in
Hashtbl.replace groups k (f.csym :: Option.value ~default:[]
(Hashtbl.find_opt groups k)))
candidates;
List.iter
(fun f ->
- let flan = kebab f.csym in
+ let flan = flan_name config f.csym in
let skip why = hidden := (flan, why) :: !hidden in
match List.rev (Hashtbl.find groups flan) with
| _ :: _ :: _ as all ->
@@ -547,7 +659,8 @@ let of_dump ~env ~taken ~bound_syms (d : dump) : imported =
(Printf.sprintf
"%s all kebab to %s, and which one got the name would depend on \
the order the header declares them in — so none of them takes \
- it. Bind the one you want with a hand-written declare-c"
+ it. Give the one you want a `name` in the binding config, or \
+ bind it with a hand-written declare-c"
(String.concat ", " all) flan)
| _ ->
if Hashtbl.mem taken flan || Hashtbl.mem taken (flan ^ "-c") then
@@ -811,7 +924,8 @@ let dump_of ~loc ~header ~flags =
let env_of ~known_structs ~known_enums d = { known_structs; known_enums; d }
-let header ~loc ~header:h ~flags ~known_structs ~known_enums ~taken ~bound_syms =
+let header ~loc ~header:h ~flags ~known_structs ~known_enums ~taken ~bound_syms
+ ~config =
let k =
(* Sorted, because neither the taken table nor the declaration order is a
fact about the package — two loads of the same file that enumerate them
@@ -822,14 +936,19 @@ let header ~loc ~header:h ~flags ~known_structs ~known_enums ~taken ~bound_syms
:: "\001" :: sorted known_structs
@ ("\001" :: sorted known_enums)
@ ("\001" :: sorted (Hashtbl.fold (fun n () acc -> n :: acc) taken []))
- @ ("\001" :: sorted bound_syms))
+ @ ("\001" :: sorted bound_syms)
+ (* The config is part of the question: two loads that disagree about
+ what is excluded or renamed are different questions, and serving one
+ the other's answer is the bug this key exists to prevent. *)
+ @ ("\001" :: sorted config.excludes)
+ @ ("\001" :: sorted (List.map (fun (a, b) -> a ^ "=" ^ b) config.renames)))
in
match Hashtbl.find_opt imports k with
| Some r -> r
| None ->
let d = dump_of ~loc ~header:h ~flags in
let env = env_of ~known_structs ~known_enums d in
- let r = (of_dump ~env ~taken ~bound_syms d, d, env) in
+ let r = (of_dump ~env ~taken ~bound_syms ~config d, d, env) in
Hashtbl.replace imports k r;
r
@@ -955,3 +1074,116 @@ let diff_bound ~env ~(bound : (Ast.fn * string) list) (d : dump) =
(match want with None -> "nothing" | Some t -> ty_source t)
c.cret)))
bound
+
+
+(* ── Regenerating the committed declarations ───────────────────────── *)
+
+(* Generate once, commit the result, regenerate when the library moves.
+
+ What that buys is in DISCUSS.md item 6 and it is not caching — the dump is
+ already cached on disk and in memory, so a build that reads the header pays
+ for it once either way. It is that no header is needed by *anybody*: the
+ declarations are in the repository, so they are greppable, they diff when
+ raylib moves, and a build needs libraylib linkable and nothing else. The
+ opt-in that used to decide whether a package had 172 bindings or 428 stops
+ deciding anything.
+
+ What it costs is the check. A header read at build time compared every
+ hand-written declaration against the library on every build; a committed
+ file compares nothing, because a file on disk has no second opinion. That
+ check is not decoration — it verified all 172 hand-written declarations and
+ all 16 struct layouts against raylib 5.5 and found them exactly right, and
+ against a 5.1-dev header on the same machine it found ten real differences.
+
+ So regeneration runs it, and the check *gates the write*. There is no way to
+ ask for new declarations without comparing the package against the header
+ they come from, because the one function that writes the file is this one
+ and it refuses when the two disagree. A regeneration that quietly rewrote
+ the bindings against a header the library does not match would produce
+ exactly the failure BUILT.md warns about — a permuted struct read as five
+ plausible numbers rather than as a link error — and it would produce it in a
+ committed file that looks reviewed.
+
+ The hand-written declarations are what make the signature half of that check
+ mean anything, which is why they stay. Everything the generator emits agrees
+ with the header by construction, so diffing generated output against the
+ header it came from is a tautology; the hand-written lines were transcribed
+ from raylib's documentation by a person, so they are an independent second
+ opinion and the only thing here that the header can actually contradict. *)
+
+type regen = {
+ gwrote : bool;
+ gdecls : int;
+ gfns : int;
+ ghidden : (string * string) list;
+ gstructs : (string * string) list;
+ gsigs : sig_diff list;
+}
+
+let banner h =
+ Printf.sprintf
+ ";;;; Generated from %s by `flan generate-c`. Do not edit this file.\n\
+ ;;;;\n\
+ ;;;; Every line here was read out of the C header named by `headers`, and\n\
+ ;;;; the next regeneration overwrites the file — so a correction made here\n\
+ ;;;; is destroyed without anybody being told. Corrections go in `bindings`\n\
+ ;;;; beside it, which is read *while* these lines are made: `exclude` drops\n\
+ ;;;; a function, `name` gives one a Flan name the kebab rule would not.\n\
+ ;;;; Anything neither directive can express is a hand-written declare-c in\n\
+ ;;;; the package's own .flan, which wins over this file and is left alone.\n\
+ ;;;;\n\
+ ;;;; Regenerating compares the package against the header first and\n\
+ ;;;; refuses to write when they disagree, so this file and the\n\
+ ;;;; hand-written declarations beside it agreed with %s when it was made.\n\n"
+ (Filename.basename h) (Filename.basename h)
+
+(* [ds] is the package's *hand-written* declarations: every .flan in the
+ directory except the one being written. Reading the output back in would
+ make regeneration idempotent in the worst way — every symbol would already
+ be bound, so the second run would generate nothing and cheerfully write an
+ empty file. *)
+let regenerate ~loc ~header:h ~flags ~(ds : Ast.decl list) ~config ~out =
+ let taken = Hashtbl.create 64 in
+ List.iter
+ (fun d ->
+ match Ast.declared_name d with
+ | Some n -> Hashtbl.replace taken n ()
+ | None -> ())
+ ds;
+ let pick f = List.filter_map f ds in
+ let structs =
+ pick (fun (d : Ast.decl) ->
+ match d.Ast.d with Ast.Defstruct (n, fs) -> Some (n, fs) | _ -> None)
+ and known_enums =
+ pick (fun (d : Ast.decl) ->
+ match d.Ast.d with Ast.Defenum (n, _) -> Some n | _ -> None)
+ and bound_syms =
+ pick (fun (d : Ast.decl) ->
+ match d.Ast.d with
+ | Ast.Declare (_, s) | Ast.DeclareC (_, s) -> Some s
+ | _ -> None)
+ and bound =
+ pick (fun (d : Ast.decl) ->
+ match d.Ast.d with Ast.DeclareC (fn, s) -> Some (fn, s) | _ -> None)
+ in
+ let imported, dump, env =
+ header ~loc ~header:h ~flags ~known_structs:(List.map fst structs)
+ ~known_enums ~taken ~bound_syms ~config
+ in
+ let gstructs = check_structs ~env ~structs dump in
+ let gsigs = diff_bound ~env ~bound dump in
+ let gwrote = gstructs = [] && gsigs = [] in
+ if gwrote then begin
+ let b = Buffer.create 65536 in
+ Buffer.add_string b (banner h);
+ List.iter
+ (fun d ->
+ Buffer.add_string b (decl_source d);
+ Buffer.add_char b '\n')
+ imported.decls;
+ let ch = open_out out in
+ output_string ch (Buffer.contents b);
+ close_out ch
+ end;
+ { gwrote; gdecls = List.length imported.decls;
+ gfns = List.length dump.fns; ghidden = imported.hidden; gstructs; gsigs }
diff --git a/lib/load.ml b/lib/load.ml
index 4ed981e..8fd7ebe 100644
--- a/lib/load.ml
+++ b/lib/load.ml
@@ -587,6 +587,16 @@ let header_specs ~loc dir =
else Some (h, flags)))
(read_lines path)
+(* The binding config, beside [headers] and read for the same package: which
+ functions not to generate, and what to call the ones whose kebab name is
+ not wanted. Absent is the ordinary case and means neither.
+
+ It is read here rather than by the importer because it is a property of the
+ *package*, like [headers] and [link] — the importer is given a header and a
+ config and has no directory to look in. See [Cimport.read_config] for why a
+ config exists at all once the generated declarations are committed. *)
+let binding_config dir = Cimport.read_config (Filename.concat dir "bindings")
+
let real dir = try Unix.realpath dir with Unix.Unix_error _ -> dir
(* One package, and whatever it imports.
@@ -758,7 +768,7 @@ let rec import ~seen ~open_ ~loc alias dir =
in
let r, dump, env =
Cimport.header ~loc ~header:h ~flags ~known_structs ~known_enums
- ~taken ~bound_syms
+ ~taken ~bound_syms ~config:(binding_config dir)
in
(* The point of reading the header, and the reason it is not
enough to generate declarations out of it.
diff --git a/test/test_flan.ml b/test/test_flan.ml
index bca099d..a6c7b8d 100644
--- a/test/test_flan.ml
+++ b/test/test_flan.ml
@@ -1318,7 +1318,7 @@ let () =
in
let i, d, e =
Cimport.header ~loc:Loc.unknown ~header:"headers/sample.h" ~flags:[]
- ~known_structs ~known_enums ~taken ~bound_syms:[]
+ ~known_structs ~known_enums ~taken ~bound_syms:[] ~config:Cimport.no_config
in
(i, d, e, ds)
in
diff --git a/vendor/raylib/bindings b/vendor/raylib/bindings
new file mode 100644
index 0000000..5dd7498
--- /dev/null
+++ b/vendor/raylib/bindings
@@ -0,0 +1,69 @@
+# How the generated bindings in generated.flan are shaped. One directive per
+# line, `#` comments, blank lines ignored. A line that is neither directive is
+# an error rather than a line quietly skipped.
+#
+# exclude do not generate a binding for this
+# name call it this instead of the kebab rule's
+#
+# `*` in an exclude pattern stands for any run of characters, and nothing else
+# does anything.
+#
+# Why this file exists. generated.flan is committed, so it is not a build
+# artefact anybody can hand-correct: the next `flan generate-c vendor/raylib`
+# overwrites it, and an edit made there is destroyed without anybody being
+# told. This file is read *while* those lines are made, so it is the edit that
+# survives regeneration. A postprocessing pass over the generated text was the
+# alternative and was rejected — a second program to understand, applied to
+# output the generator had already committed to.
+#
+# What does NOT belong here. Anything neither directive can express is a
+# hand-written `declare-c` in raylib.flan, which wins over the generated file
+# and is left alone by the importer. That is the escape hatch for a signature
+# the importer gets wrong and for a Flan face the header cannot describe —
+# load-font-ex-raw and load-image-from-memory-raw are both that, each wrapped
+# by a Flan defn of the same name without the suffix.
+
+# ── raylib's allocator ──────────────────────────────────────────────
+#
+# raylib exports malloc, realloc and free under its own names. Flan's memory
+# model is explicit — plan.org's rule is that an operation never falls back to
+# a hidden allocator — so binding these would put a second, untracked heap
+# behind three innocuous-looking Flan names, and a pointer from one freed by
+# the other is the kind of bug that does not point at itself. Nothing in the
+# package needs them: raylib's own Load*/Unload* pairs own everything raylib
+# allocates.
+exclude MemAlloc
+exclude MemRealloc
+exclude MemFree
+
+# ── Predicates read as questions ────────────────────────────────────
+#
+# The kebab rule gives `is-window-ready`, which is C's phrasing wearing Flan's
+# punctuation. The hand-written bindings in raylib.flan settled the convention
+# years of Lisp settled first — a predicate ends in `?` and does not begin with
+# `is` — and these are the generated half of the same set, so they follow it.
+# key-pressed? and window-ready? being spelled differently would be the split
+# this whole change exists to remove.
+#
+# The C symbol is kept verbatim in the declaration either way, so nothing here
+# is lost: the name is the Flan face and `IsWindowReady` is still what is
+# called and still what the check compares against.
+name IsWindowReady window-ready?
+name IsWindowFullscreen window-fullscreen?
+name IsWindowHidden window-hidden?
+name IsWindowMinimized window-minimized?
+name IsWindowMaximized window-maximized?
+name IsWindowFocused window-focused?
+name IsWindowResized window-resized?
+name IsWindowState window-state?
+name IsCursorOnScreen cursor-on-screen?
+name IsKeyUp key-up?
+name IsKeyPressedRepeat key-pressed-repeat?
+name IsMouseButtonUp mouse-button-up?
+name IsFileDropped file-dropped?
+name IsFileExtension file-extension?
+name IsFileNameValid file-name-valid?
+name IsPathFile path-file?
+name IsAudioStreamValid audio-stream-valid?
+name IsAudioStreamPlaying audio-stream-playing?
+name IsAudioStreamProcessed audio-stream-processed?
diff --git a/vendor/raylib/generated.flan b/vendor/raylib/generated.flan
new file mode 100644
index 0000000..981c334
--- /dev/null
+++ b/vendor/raylib/generated.flan
@@ -0,0 +1,267 @@
+;;;; Generated from raylib.h by `flan generate-c`. Do not edit this file.
+;;;;
+;;;; Every line here was read out of the C header named by `headers`, and
+;;;; the next regeneration overwrites the file — so a correction made here
+;;;; is destroyed without anybody being told. Corrections go in `bindings`
+;;;; beside it, which is read *while* these lines are made: `exclude` drops
+;;;; a function, `name` gives one a Flan name the kebab rule would not.
+;;;; Anything neither directive can express is a hand-written declare-c in
+;;;; the package's own .flan, which wins over this file and is left alone.
+;;;;
+;;;; Regenerating compares the package against the header first and
+;;;; refuses to write when they disagree, so this file and the
+;;;; hand-written declarations beside it agreed with raylib.h when it was made.
+
+(declare-c window-ready? [] bool "IsWindowReady")
+(declare-c window-fullscreen? [] bool "IsWindowFullscreen")
+(declare-c window-hidden? [] bool "IsWindowHidden")
+(declare-c window-minimized? [] bool "IsWindowMinimized")
+(declare-c window-maximized? [] bool "IsWindowMaximized")
+(declare-c window-focused? [] bool "IsWindowFocused")
+(declare-c window-resized? [] bool "IsWindowResized")
+(declare-c window-state? [flag u32] bool "IsWindowState")
+(declare-c set-window-state [flags u32] "SetWindowState")
+(declare-c clear-window-state [flags u32] "ClearWindowState")
+(declare-c toggle-fullscreen [] "ToggleFullscreen")
+(declare-c toggle-borderless-windowed [] "ToggleBorderlessWindowed")
+(declare-c maximize-window [] "MaximizeWindow")
+(declare-c minimize-window [] "MinimizeWindow")
+(declare-c restore-window [] "RestoreWindow")
+(declare-c set-window-icon [image Image] "SetWindowIcon")
+(declare-c set-window-icons [images (Ptr Image) count i32] "SetWindowIcons")
+(declare-c set-window-title [title string] "SetWindowTitle")
+(declare-c set-window-position [x i32 y i32] "SetWindowPosition")
+(declare-c set-window-monitor [monitor i32] "SetWindowMonitor")
+(declare-c set-window-min-size [width i32 height i32] "SetWindowMinSize")
+(declare-c set-window-max-size [width i32 height i32] "SetWindowMaxSize")
+(declare-c set-window-size [width i32 height i32] "SetWindowSize")
+(declare-c set-window-opacity [opacity f32] "SetWindowOpacity")
+(declare-c set-window-focused [] "SetWindowFocused")
+(declare-c get-window-handle [] (Ptr u8) "GetWindowHandle")
+(declare-c get-render-width [] i32 "GetRenderWidth")
+(declare-c get-render-height [] i32 "GetRenderHeight")
+(declare-c get-monitor-count [] i32 "GetMonitorCount")
+(declare-c get-current-monitor [] i32 "GetCurrentMonitor")
+(declare-c get-monitor-position [monitor i32] Vector2 "GetMonitorPosition")
+(declare-c get-monitor-width [monitor i32] i32 "GetMonitorWidth")
+(declare-c get-monitor-height [monitor i32] i32 "GetMonitorHeight")
+(declare-c get-monitor-physical-width [monitor i32] i32 "GetMonitorPhysicalWidth")
+(declare-c get-monitor-physical-height [monitor i32] i32 "GetMonitorPhysicalHeight")
+(declare-c get-monitor-refresh-rate [monitor i32] i32 "GetMonitorRefreshRate")
+(declare-c get-window-position [] Vector2 "GetWindowPosition")
+(declare-c get-window-scale-dpi [] Vector2 "GetWindowScaleDPI")
+(declare-c set-clipboard-text [text string] "SetClipboardText")
+(declare-c get-clipboard-image [] Image "GetClipboardImage")
+(declare-c enable-event-waiting [] "EnableEventWaiting")
+(declare-c disable-event-waiting [] "DisableEventWaiting")
+(declare-c enable-cursor [] "EnableCursor")
+(declare-c disable-cursor [] "DisableCursor")
+(declare-c cursor-on-screen? [] bool "IsCursorOnScreen")
+(declare-c end-mode-3d [] "EndMode3D")
+(declare-c end-shader-mode [] "EndShaderMode")
+(declare-c begin-blend-mode [mode i32] "BeginBlendMode")
+(declare-c end-blend-mode [] "EndBlendMode")
+(declare-c begin-scissor-mode [x i32 y i32 width i32 height i32] "BeginScissorMode")
+(declare-c end-scissor-mode [] "EndScissorMode")
+(declare-c end-vr-stereo-mode [] "EndVrStereoMode")
+(declare-c swap-screen-buffer [] "SwapScreenBuffer")
+(declare-c poll-input-events [] "PollInputEvents")
+(declare-c wait-time [seconds f64] "WaitTime")
+(declare-c set-random-seed [seed u32] "SetRandomSeed")
+(declare-c get-random-value [min i32 max i32] i32 "GetRandomValue")
+(declare-c load-random-sequence [count u32 min i32 max i32] (Ptr i32) "LoadRandomSequence")
+(declare-c unload-random-sequence [sequence (Ptr i32)] "UnloadRandomSequence")
+(declare-c take-screenshot [file-name string] "TakeScreenshot")
+(declare-c open-url [url string] "OpenURL")
+(declare-c load-file-data [file-name string data-size (Ptr i32)] (Ptr u8) "LoadFileData")
+(declare-c unload-file-data [data (Ptr u8)] "UnloadFileData")
+(declare-c save-file-data [file-name string data (Ptr u8) data-size i32] bool "SaveFileData")
+(declare-c export-data-as-code [data (Ptr u8) data-size i32 file-name string] bool "ExportDataAsCode")
+(declare-c file-exists [file-name string] bool "FileExists")
+(declare-c directory-exists [dir-path string] bool "DirectoryExists")
+(declare-c file-extension? [file-name string ext string] bool "IsFileExtension")
+(declare-c get-file-length [file-name string] i32 "GetFileLength")
+(declare-c make-directory [dir-path string] i32 "MakeDirectory")
+(declare-c change-directory [dir string] bool "ChangeDirectory")
+(declare-c path-file? [path string] bool "IsPathFile")
+(declare-c file-name-valid? [file-name string] bool "IsFileNameValid")
+(declare-c file-dropped? [] bool "IsFileDropped")
+(declare-c compress-data [data (Ptr u8) data-size i32 comp-data-size (Ptr i32)] (Ptr u8) "CompressData")
+(declare-c decompress-data [comp-data (Ptr u8) comp-data-size i32 data-size (Ptr i32)] (Ptr u8) "DecompressData")
+(declare-c decode-data-base-64 [data (Ptr u8) output-size (Ptr i32)] (Ptr u8) "DecodeDataBase64")
+(declare-c compute-crc32 [data (Ptr u8) data-size i32] u32 "ComputeCRC32")
+(declare-c compute-md5 [data (Ptr u8) data-size i32] (Ptr u32) "ComputeMD5")
+(declare-c compute-sha1 [data (Ptr u8) data-size i32] (Ptr u32) "ComputeSHA1")
+(declare-c set-automation-event-base-frame [frame i32] "SetAutomationEventBaseFrame")
+(declare-c start-automation-event-recording [] "StartAutomationEventRecording")
+(declare-c stop-automation-event-recording [] "StopAutomationEventRecording")
+(declare-c key-pressed-repeat? [key i32] bool "IsKeyPressedRepeat")
+(declare-c key-up? [key i32] bool "IsKeyUp")
+(declare-c get-key-pressed [] i32 "GetKeyPressed")
+(declare-c get-char-pressed [] i32 "GetCharPressed")
+(declare-c set-exit-key [key i32] "SetExitKey")
+(declare-c set-gamepad-mappings [mappings string] i32 "SetGamepadMappings")
+(declare-c set-gamepad-vibration [gamepad i32 left-motor f32 right-motor f32 duration f32] "SetGamepadVibration")
+(declare-c mouse-button-up? [button i32] bool "IsMouseButtonUp")
+(declare-c get-mouse-x [] i32 "GetMouseX")
+(declare-c get-mouse-y [] i32 "GetMouseY")
+(declare-c get-mouse-delta [] Vector2 "GetMouseDelta")
+(declare-c set-mouse-position [x i32 y i32] "SetMousePosition")
+(declare-c set-mouse-offset [offset-x i32 offset-y i32] "SetMouseOffset")
+(declare-c set-mouse-scale [scale-x f32 scale-y f32] "SetMouseScale")
+(declare-c get-mouse-wheel-move-v [] Vector2 "GetMouseWheelMoveV")
+(declare-c set-mouse-cursor [cursor i32] "SetMouseCursor")
+(declare-c draw-line-strip [points (Ptr Vector2) point-count i32 color Color] "DrawLineStrip")
+(declare-c draw-line-bezier [start-pos Vector2 end-pos Vector2 thick f32 color Color] "DrawLineBezier")
+(declare-c draw-circle-sector [center Vector2 radius f32 start-angle f32 end-angle f32 segments i32 color Color] "DrawCircleSector")
+(declare-c draw-circle-sector-lines [center Vector2 radius f32 start-angle f32 end-angle f32 segments i32 color Color] "DrawCircleSectorLines")
+(declare-c draw-circle-gradient [center-x i32 center-y i32 radius f32 inner Color outer Color] "DrawCircleGradient")
+(declare-c draw-rectangle-pro [rec Rectangle origin Vector2 rotation f32 color Color] "DrawRectanglePro")
+(declare-c draw-rectangle-gradient-v [pos-x i32 pos-y i32 width i32 height i32 top Color bottom Color] "DrawRectangleGradientV")
+(declare-c draw-rectangle-gradient-h [pos-x i32 pos-y i32 width i32 height i32 left Color right Color] "DrawRectangleGradientH")
+(declare-c draw-rectangle-gradient-ex [rec Rectangle top-left Color bottom-left Color top-right Color bottom-right Color] "DrawRectangleGradientEx")
+(declare-c draw-triangle-fan [points (Ptr Vector2) point-count i32 color Color] "DrawTriangleFan")
+(declare-c draw-triangle-strip [points (Ptr Vector2) point-count i32 color Color] "DrawTriangleStrip")
+(declare-c draw-poly [center Vector2 sides i32 radius f32 rotation f32 color Color] "DrawPoly")
+(declare-c draw-poly-lines [center Vector2 sides i32 radius f32 rotation f32 color Color] "DrawPolyLines")
+(declare-c draw-poly-lines-ex [center Vector2 sides i32 radius f32 rotation f32 line-thick f32 color Color] "DrawPolyLinesEx")
+(declare-c draw-spline-linear [points (Ptr Vector2) point-count i32 thick f32 color Color] "DrawSplineLinear")
+(declare-c draw-spline-basis [points (Ptr Vector2) point-count i32 thick f32 color Color] "DrawSplineBasis")
+(declare-c draw-spline-catmull-rom [points (Ptr Vector2) point-count i32 thick f32 color Color] "DrawSplineCatmullRom")
+(declare-c draw-spline-bezier-quadratic [points (Ptr Vector2) point-count i32 thick f32 color Color] "DrawSplineBezierQuadratic")
+(declare-c draw-spline-bezier-cubic [points (Ptr Vector2) point-count i32 thick f32 color Color] "DrawSplineBezierCubic")
+(declare-c draw-spline-segment-linear [p-1 Vector2 p-2 Vector2 thick f32 color Color] "DrawSplineSegmentLinear")
+(declare-c draw-spline-segment-basis [p-1 Vector2 p-2 Vector2 p-3 Vector2 p-4 Vector2 thick f32 color Color] "DrawSplineSegmentBasis")
+(declare-c draw-spline-segment-catmull-rom [p-1 Vector2 p-2 Vector2 p-3 Vector2 p-4 Vector2 thick f32 color Color] "DrawSplineSegmentCatmullRom")
+(declare-c draw-spline-segment-bezier-quadratic [p-1 Vector2 c-2 Vector2 p-3 Vector2 thick f32 color Color] "DrawSplineSegmentBezierQuadratic")
+(declare-c draw-spline-segment-bezier-cubic [p-1 Vector2 c-2 Vector2 c-3 Vector2 p-4 Vector2 thick f32 color Color] "DrawSplineSegmentBezierCubic")
+(declare-c get-spline-point-linear [start-pos Vector2 end-pos Vector2 t f32] Vector2 "GetSplinePointLinear")
+(declare-c get-spline-point-basis [p-1 Vector2 p-2 Vector2 p-3 Vector2 p-4 Vector2 t f32] Vector2 "GetSplinePointBasis")
+(declare-c get-spline-point-catmull-rom [p-1 Vector2 p-2 Vector2 p-3 Vector2 p-4 Vector2 t f32] Vector2 "GetSplinePointCatmullRom")
+(declare-c get-spline-point-bezier-quad [p-1 Vector2 c-2 Vector2 p-3 Vector2 t f32] Vector2 "GetSplinePointBezierQuad")
+(declare-c get-spline-point-bezier-cubic [p-1 Vector2 c-2 Vector2 c-3 Vector2 p-4 Vector2 t f32] Vector2 "GetSplinePointBezierCubic")
+(declare-c load-image-raw [file-name string width i32 height i32 format i32 header-size i32] Image "LoadImageRaw")
+(declare-c load-image-anim [file-name string frames (Ptr i32)] Image "LoadImageAnim")
+(declare-c load-image-anim-from-memory [file-type string file-data (Ptr u8) data-size i32 frames (Ptr i32)] Image "LoadImageAnimFromMemory")
+(declare-c load-image-from-texture [texture Texture2D] Image "LoadImageFromTexture")
+(declare-c load-image-from-screen [] Image "LoadImageFromScreen")
+(declare-c export-image-to-memory [image Image file-type string file-size (Ptr i32)] (Ptr u8) "ExportImageToMemory")
+(declare-c export-image-as-code [image Image file-name string] bool "ExportImageAsCode")
+(declare-c gen-image-gradient-linear [width i32 height i32 direction i32 start Color end Color] Image "GenImageGradientLinear")
+(declare-c gen-image-gradient-radial [width i32 height i32 density f32 inner Color outer Color] Image "GenImageGradientRadial")
+(declare-c gen-image-gradient-square [width i32 height i32 density f32 inner Color outer Color] Image "GenImageGradientSquare")
+(declare-c gen-image-checked [width i32 height i32 checks-x i32 checks-y i32 col-1 Color col-2 Color] Image "GenImageChecked")
+(declare-c gen-image-white-noise [width i32 height i32 factor f32] Image "GenImageWhiteNoise")
+(declare-c gen-image-perlin-noise [width i32 height i32 offset-x i32 offset-y i32 scale f32] Image "GenImagePerlinNoise")
+(declare-c gen-image-cellular [width i32 height i32 tile-size i32] Image "GenImageCellular")
+(declare-c gen-image-text [width i32 height i32 text string] Image "GenImageText")
+(declare-c image-copy [image Image] Image "ImageCopy")
+(declare-c image-from-image [image Image rec Rectangle] Image "ImageFromImage")
+(declare-c image-from-channel [image Image selected-channel i32] Image "ImageFromChannel")
+(declare-c image-text [text string font-size i32 color Color] Image "ImageText")
+(declare-c image-text-ex [font Font text string font-size f32 spacing f32 tint Color] Image "ImageTextEx")
+(declare-c image-format [image (Ptr Image) new-format i32] "ImageFormat")
+(declare-c image-to-pot [image (Ptr Image) fill Color] "ImageToPOT")
+(declare-c image-alpha-crop [image (Ptr Image) threshold f32] "ImageAlphaCrop")
+(declare-c image-alpha-clear [image (Ptr Image) color Color threshold f32] "ImageAlphaClear")
+(declare-c image-alpha-mask [image (Ptr Image) alpha-mask Image] "ImageAlphaMask")
+(declare-c image-alpha-premultiply [image (Ptr Image)] "ImageAlphaPremultiply")
+(declare-c image-blur-gaussian [image (Ptr Image) blur-size i32] "ImageBlurGaussian")
+(declare-c image-kernel-convolution [image (Ptr Image) kernel (Ptr f32) kernel-size i32] "ImageKernelConvolution")
+(declare-c image-resize-canvas [image (Ptr Image) new-width i32 new-height i32 offset-x i32 offset-y i32 fill Color] "ImageResizeCanvas")
+(declare-c image-mipmaps [image (Ptr Image)] "ImageMipmaps")
+(declare-c image-dither [image (Ptr Image) r-bpp i32 g-bpp i32 b-bpp i32 a-bpp i32] "ImageDither")
+(declare-c image-rotate [image (Ptr Image) degrees i32] "ImageRotate")
+(declare-c image-rotate-cw [image (Ptr Image)] "ImageRotateCW")
+(declare-c image-rotate-ccw [image (Ptr Image)] "ImageRotateCCW")
+(declare-c image-color-tint [image (Ptr Image) color Color] "ImageColorTint")
+(declare-c image-color-invert [image (Ptr Image)] "ImageColorInvert")
+(declare-c image-color-grayscale [image (Ptr Image)] "ImageColorGrayscale")
+(declare-c image-color-contrast [image (Ptr Image) contrast f32] "ImageColorContrast")
+(declare-c image-color-brightness [image (Ptr Image) brightness i32] "ImageColorBrightness")
+(declare-c image-color-replace [image (Ptr Image) color Color replace Color] "ImageColorReplace")
+(declare-c load-image-colors [image Image] (Ptr Color) "LoadImageColors")
+(declare-c load-image-palette [image Image max-palette-size i32 color-count (Ptr i32)] (Ptr Color) "LoadImagePalette")
+(declare-c unload-image-colors [colors (Ptr Color)] "UnloadImageColors")
+(declare-c unload-image-palette [colors (Ptr Color)] "UnloadImagePalette")
+(declare-c get-image-alpha-border [image Image threshold f32] Rectangle "GetImageAlphaBorder")
+(declare-c image-clear-background [dst (Ptr Image) color Color] "ImageClearBackground")
+(declare-c image-draw-pixel-v [dst (Ptr Image) position Vector2 color Color] "ImageDrawPixelV")
+(declare-c image-draw-line [dst (Ptr Image) start-pos-x i32 start-pos-y i32 end-pos-x i32 end-pos-y i32 color Color] "ImageDrawLine")
+(declare-c image-draw-line-v [dst (Ptr Image) start Vector2 end Vector2 color Color] "ImageDrawLineV")
+(declare-c image-draw-line-ex [dst (Ptr Image) start Vector2 end Vector2 thick i32 color Color] "ImageDrawLineEx")
+(declare-c image-draw-circle [dst (Ptr Image) center-x i32 center-y i32 radius i32 color Color] "ImageDrawCircle")
+(declare-c image-draw-circle-v [dst (Ptr Image) center Vector2 radius i32 color Color] "ImageDrawCircleV")
+(declare-c image-draw-circle-lines [dst (Ptr Image) center-x i32 center-y i32 radius i32 color Color] "ImageDrawCircleLines")
+(declare-c image-draw-circle-lines-v [dst (Ptr Image) center Vector2 radius i32 color Color] "ImageDrawCircleLinesV")
+(declare-c image-draw-rectangle [dst (Ptr Image) pos-x i32 pos-y i32 width i32 height i32 color Color] "ImageDrawRectangle")
+(declare-c image-draw-rectangle-v [dst (Ptr Image) position Vector2 size Vector2 color Color] "ImageDrawRectangleV")
+(declare-c image-draw-rectangle-rec [dst (Ptr Image) rec Rectangle color Color] "ImageDrawRectangleRec")
+(declare-c image-draw-rectangle-lines [dst (Ptr Image) rec Rectangle thick i32 color Color] "ImageDrawRectangleLines")
+(declare-c image-draw-triangle [dst (Ptr Image) v-1 Vector2 v-2 Vector2 v-3 Vector2 color Color] "ImageDrawTriangle")
+(declare-c image-draw-triangle-ex [dst (Ptr Image) v-1 Vector2 v-2 Vector2 v-3 Vector2 c-1 Color c-2 Color c-3 Color] "ImageDrawTriangleEx")
+(declare-c image-draw-triangle-lines [dst (Ptr Image) v-1 Vector2 v-2 Vector2 v-3 Vector2 color Color] "ImageDrawTriangleLines")
+(declare-c image-draw-triangle-fan [dst (Ptr Image) points (Ptr Vector2) point-count i32 color Color] "ImageDrawTriangleFan")
+(declare-c image-draw-triangle-strip [dst (Ptr Image) points (Ptr Vector2) point-count i32 color Color] "ImageDrawTriangleStrip")
+(declare-c image-draw [dst (Ptr Image) src Image src-rec Rectangle dst-rec Rectangle tint Color] "ImageDraw")
+(declare-c image-draw-text [dst (Ptr Image) text string pos-x i32 pos-y i32 font-size i32 color Color] "ImageDrawText")
+(declare-c image-draw-text-ex [dst (Ptr Image) font Font text string position Vector2 font-size f32 spacing f32 tint Color] "ImageDrawTextEx")
+(declare-c load-texture-cubemap [image Image layout i32] Texture2D "LoadTextureCubemap")
+(declare-c update-texture [texture Texture2D pixels (Ptr u8)] "UpdateTexture")
+(declare-c update-texture-rec [texture Texture2D rec Rectangle pixels (Ptr u8)] "UpdateTextureRec")
+(declare-c gen-texture-mipmaps [texture (Ptr Texture2D)] "GenTextureMipmaps")
+(declare-c set-texture-filter [texture Texture2D filter i32] "SetTextureFilter")
+(declare-c set-texture-wrap [texture Texture2D wrap i32] "SetTextureWrap")
+(declare-c draw-texture-pro [texture Texture2D source Rectangle dest Rectangle origin Vector2 rotation f32 tint Color] "DrawTexturePro")
+(declare-c color-is-equal [col-1 Color col-2 Color] bool "ColorIsEqual")
+(declare-c color-to-int [color Color] i32 "ColorToInt")
+(declare-c color-from-hsv [hue f32 saturation f32 value f32] Color "ColorFromHSV")
+(declare-c color-tint [color Color tint Color] Color "ColorTint")
+(declare-c color-brightness [color Color factor f32] Color "ColorBrightness")
+(declare-c color-contrast [color Color contrast f32] Color "ColorContrast")
+(declare-c color-alpha [color Color alpha f32] Color "ColorAlpha")
+(declare-c color-alpha-blend [dst Color src Color tint Color] Color "ColorAlphaBlend")
+(declare-c color-lerp [color-1 Color color-2 Color factor f32] Color "ColorLerp")
+(declare-c get-pixel-color [src-ptr (Ptr u8) format i32] Color "GetPixelColor")
+(declare-c set-pixel-color [dst-ptr (Ptr u8) color Color format i32] "SetPixelColor")
+(declare-c get-pixel-data-size [width i32 height i32 format i32] i32 "GetPixelDataSize")
+(declare-c load-font-from-image [image Image key Color first-char i32] Font "LoadFontFromImage")
+(declare-c load-font-from-memory [file-type string file-data (Ptr u8) data-size i32 font-size i32 codepoints (Ptr i32) codepoint-count i32] Font "LoadFontFromMemory")
+(declare-c load-font-data [file-data (Ptr u8) data-size i32 font-size i32 codepoints (Ptr i32) codepoint-count i32 type i32] (Ptr GlyphInfo) "LoadFontData")
+(declare-c gen-image-font-atlas [glyphs (Ptr GlyphInfo) glyph-recs (Ptr (Ptr Rectangle)) glyph-count i32 font-size i32 padding i32 pack-method i32] Image "GenImageFontAtlas")
+(declare-c unload-font-data [glyphs (Ptr GlyphInfo) glyph-count i32] "UnloadFontData")
+(declare-c export-font-as-code [font Font file-name string] bool "ExportFontAsCode")
+(declare-c draw-text-pro [font Font text string position Vector2 origin Vector2 rotation f32 font-size f32 spacing f32 tint Color] "DrawTextPro")
+(declare-c draw-text-codepoints [font Font codepoints (Ptr i32) codepoint-count i32 position Vector2 font-size f32 spacing f32 tint Color] "DrawTextCodepoints")
+(declare-c set-text-line-spacing [spacing i32] "SetTextLineSpacing")
+(declare-c load-codepoints [text string count (Ptr i32)] (Ptr i32) "LoadCodepoints")
+(declare-c unload-codepoints [codepoints (Ptr i32)] "UnloadCodepoints")
+(declare-c get-codepoint-count [text string] i32 "GetCodepointCount")
+(declare-c get-codepoint [text string codepoint-size (Ptr i32)] i32 "GetCodepoint")
+(declare-c get-codepoint-next [text string codepoint-size (Ptr i32)] i32 "GetCodepointNext")
+(declare-c get-codepoint-previous [text string codepoint-size (Ptr i32)] i32 "GetCodepointPrevious")
+(declare-c text-is-equal [text-1 string text-2 string] bool "TextIsEqual")
+(declare-c text-length [text string] u32 "TextLength")
+(declare-c text-split [text string delimiter i8 count (Ptr i32)] (Ptr (Ptr i8)) "TextSplit")
+(declare-c text-find-index [text string find string] i32 "TextFindIndex")
+(declare-c text-to-integer [text string] i32 "TextToInteger")
+(declare-c text-to-float [text string] f32 "TextToFloat")
+(declare-c draw-grid [slices i32 spacing f32] "DrawGrid")
+(declare-c load-wave-from-memory [file-type string file-data (Ptr u8) data-size i32] Wave "LoadWaveFromMemory")
+(declare-c update-sound [sound Sound data (Ptr u8) sample-count i32] "UpdateSound")
+(declare-c export-wave-as-code [wave Wave file-name string] bool "ExportWaveAsCode")
+(declare-c load-music-stream-from-memory [file-type string data (Ptr u8) data-size i32] Music "LoadMusicStreamFromMemory")
+(declare-c load-audio-stream [sample-rate u32 sample-size u32 channels u32] AudioStream "LoadAudioStream")
+(declare-c audio-stream-valid? [stream AudioStream] bool "IsAudioStreamValid")
+(declare-c unload-audio-stream [stream AudioStream] "UnloadAudioStream")
+(declare-c update-audio-stream [stream AudioStream data (Ptr u8) frame-count i32] "UpdateAudioStream")
+(declare-c audio-stream-processed? [stream AudioStream] bool "IsAudioStreamProcessed")
+(declare-c play-audio-stream [stream AudioStream] "PlayAudioStream")
+(declare-c pause-audio-stream [stream AudioStream] "PauseAudioStream")
+(declare-c resume-audio-stream [stream AudioStream] "ResumeAudioStream")
+(declare-c audio-stream-playing? [stream AudioStream] bool "IsAudioStreamPlaying")
+(declare-c stop-audio-stream [stream AudioStream] "StopAudioStream")
+(declare-c set-audio-stream-volume [stream AudioStream volume f32] "SetAudioStreamVolume")
+(declare-c set-audio-stream-pitch [stream AudioStream pitch f32] "SetAudioStreamPitch")
+(declare-c set-audio-stream-pan [stream AudioStream pan f32] "SetAudioStreamPan")
+(declare-c set-audio-stream-buffer-size-default [size i32] "SetAudioStreamBufferSizeDefault")
diff --git a/vendor/raylib/headers b/vendor/raylib/headers
index 4f001e7..3cbc5c1 100644
--- a/vendor/raylib/headers
+++ b/vendor/raylib/headers
@@ -4,29 +4,49 @@
# means "if it is there" — an optional line with nothing behind it is simply
# not read.
#
-# Why this exists. The declare-c lines in raylib.flan were transcribed by
-# hand from raylib's documentation, and until now nothing could check that
-# any of them matched the real function — BUILT.md records that as trusted
-# rather than guaranteed. Point this at raylib's own header and the compiler
-# reads the signatures instead: every hand-written line is compared against
-# the library's, every defstruct against the header's record, and any raylib
-# function the package has not bound becomes available under its own name.
+# What this is for, now that the bindings are committed. generated.flan holds
+# every declaration the importer produced, in the repository, so a build needs
+# libraylib linkable and no header at all. This line is read by two things:
#
-# Why it is optional. A build needs libraylib linkable and *not* raylib-devel
-# installed, which is a property worth keeping; requiring a header would take
-# it from everyone to give the check to whoever has one. So the default build
-# is unchanged and this is opt-in, the same shape as ${FLAN_RAYLIB_WEB} in
-# `link`.
+# 1. `flan generate-c vendor/raylib`, which is the only way generated.flan
+# is written. It reads the header named here, compares the package
+# against it, and refuses to write when they disagree — so it is not
+# possible to regenerate the bindings without comparing them to the
+# library they claim to bind.
+#
+# 2. an ordinary build, when the variable happens to be set. Every C symbol
+# is bound already — by hand in raylib.flan or by generation in
+# generated.flan — so the importer generates nothing and the header read
+# is purely the check. That check is now over all 425 declarations rather
+# than the 172 hand-written ones, because the generated file is a package
+# file like any other and is checked like one.
+#
+# Why it is still optional. A build needs libraylib linkable and *not*
+# raylib-devel installed, which is a property worth keeping; requiring a header
+# would take it from everyone to give the check to whoever has one. Before the
+# bindings were committed this marker also decided how many bindings a build
+# got, which was the real cost of it being opt-in; it no longer decides that,
+# and all it now withholds is a check that regeneration has already run once.
+# Same shape as ${FLAN_RAYLIB_WEB} in `link`, and for the same reason.
#
# The version must match the shared library `link` names — 5.5, libraylib.so.550.
# Reading one version's header while linking another's library is exactly the
-# silent disagreement this exists to prevent, and `flan import-c` will say so:
-# against a 5.1-dev header it reports ten differences that are all real.
+# silent disagreement this exists to prevent, and it is caught rather than
+# described: against a 5.1-dev header, regeneration reports ten differences
+# that are all real and writes nothing.
#
# export FLAN_RAYLIB_H=/path/to/raylib-5.5/src/raylib.h
#
-# To see what it would do without building anything:
+# vendor/raylib/build-web.sh already clones that exact tag to build the browser
+# archive, so a tree that has built for web has the matching header at
+# vendor/raylib/web/raylib-5.5/src/raylib.h.
+#
+# To see what regeneration would produce without writing anything:
#
# flan import-c $FLAN_RAYLIB_H vendor/raylib/raylib.flan
#
+# What shapes the generated half — which functions are skipped, and what they
+# are called — is `bindings` beside this file. See its comments for why a
+# committed generated file needs a config at all.
+#
?${FLAN_RAYLIB_H}
From ecfe180d2e48acc2c3b5a5aa84940a4131f4b05e Mon Sep 17 00:00:00 2001
From: Joseph Ferano
Date: Sun, 13 Sep 2026 08:09:59 +0700
Subject: [PATCH 2/5] The config is tested by what it is for, not by its parser
alone
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Exclusion says why rather than going quiet, a pattern matches by prefix, an
override changes the Flan face and leaves the C symbol verbatim, and a rename
dissolves a kebab collision instead of leaving both halves refused — that last
one is why the kebab rule is consulted in exactly one place.
The file itself too: a line that is neither directive is an error, because a
typo in a name override would otherwise land a binding under the wrong name.
---
test/test_flan.ml | 99 +++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 99 insertions(+)
diff --git a/test/test_flan.ml b/test/test_flan.ml
index a6c7b8d..a147e76 100644
--- a/test/test_flan.ml
+++ b/test/test_flan.ml
@@ -1378,6 +1378,105 @@ let () =
check "nor is the other half of the collision"
(not (List.exists (fun l -> contains l "\"spin2d\"") produced));
+ (* ── The config beside the header (Cimport.read_config) ────────── *)
+
+ (* Why there is a config at all: the generated declarations are committed, so
+ a hand-edit to them is destroyed by the next regeneration and the edit has
+ to live somewhere regeneration reads instead. These are the two things it
+ can say. *)
+ let with_config config =
+ let taken = Hashtbl.create 16 in
+ List.iter
+ (fun d ->
+ match Ast.declared_name d with
+ | Some n -> Hashtbl.replace taken n ()
+ | None -> ())
+ fixture_ds;
+ let known_structs =
+ List.filter_map
+ (fun (d : Ast.decl) ->
+ match d.Ast.d with Ast.Defstruct (n, _) -> Some n | _ -> None)
+ fixture_ds
+ and known_enums =
+ List.filter_map
+ (fun (d : Ast.decl) ->
+ match d.Ast.d with Ast.Defenum (n, _) -> Some n | _ -> None)
+ fixture_ds
+ in
+ let i, _, _ =
+ Cimport.header ~loc:Loc.unknown ~header:"headers/sample.h" ~flags:[]
+ ~known_structs ~known_enums ~taken ~bound_syms:[] ~config
+ in
+ (List.map Cimport.decl_source i.Cimport.decls, i.Cimport.hidden)
+ in
+
+ let lines, hidden =
+ with_config { Cimport.excludes = [ "set_seed" ]; renames = [] }
+ in
+ check "an excluded symbol is not generated"
+ (not (List.exists (fun l -> contains l "\"set_seed\"") lines));
+ (* Not silently absent. "there is no such binding" and "the package decided
+ against this binding" are different answers and a reader gets the second,
+ which is what [hidden] is for. *)
+ check "and it says it was excluded rather than going quiet"
+ (List.exists
+ (fun (n, why) -> n = "set-seed" && contains why "excluded by the package")
+ hidden);
+
+ let lines, _ =
+ with_config { Cimport.excludes = [ "add_*" ]; renames = [] }
+ in
+ check "an exclude pattern matches by prefix"
+ (not (List.exists (fun l -> contains l "\"add_ints\"") lines));
+ check "and leaves everything it does not match"
+ (List.exists (fun l -> contains l "\"set_seed\"") lines);
+
+ let lines, _ =
+ with_config
+ { Cimport.excludes = []; renames = [ ("set_seed", "seed!") ] }
+ in
+ (* The C symbol is kept verbatim, so an override changes the Flan face and
+ nothing else — which is what makes it safe to spell a predicate the way
+ Lisp spells one. *)
+ check "a name override is the Flan name, and the C symbol is untouched"
+ (List.mem "(declare-c seed! [seed u32] \"set_seed\")" lines);
+
+ (* The collision above is refused because neither Spin2D nor spin2d may take
+ [spin-2d] by an accident of header order. Naming one of them is the way
+ out, and it is the reason the kebab rule is consulted in exactly one
+ place: the groups are computed on the name a function will really take,
+ so the rename dissolves the group rather than leaving both refused. *)
+ let lines, hidden =
+ with_config
+ { Cimport.excludes = []; renames = [ ("Spin2D", "spin-2d-upper") ] }
+ in
+ check "a rename resolves a collision for both halves"
+ (List.exists (fun l -> contains l "\"Spin2D\"") lines
+ && List.exists (fun l -> contains l "\"spin2d\"") lines);
+ check "and the collision is no longer refused"
+ (not (List.mem_assoc "spin-2d" hidden));
+
+ (* The file, since a typo in it would otherwise show up as a binding under
+ the wrong name. A line that is neither directive is an error rather than a
+ line quietly skipped. *)
+ let config_file text =
+ let f = Filename.temp_file "flan-bindings" "" in
+ let ch = open_out f in
+ output_string ch text;
+ close_out ch;
+ f
+ in
+ let c = Cimport.read_config (config_file "# a comment\n\nexclude Mem*\nname IsWindowReady window-ready?\n") in
+ check "read_config reads an exclude" (c.Cimport.excludes = [ "Mem*" ]);
+ check "read_config reads a name override"
+ (c.Cimport.renames = [ ("IsWindowReady", "window-ready?") ]);
+ check "a missing config is no exclusions and no overrides"
+ (Cimport.read_config "no-such-bindings-file" = Cimport.no_config);
+ check "a line that is neither directive is refused"
+ (match Cimport.read_config (config_file "rename Foo bar\n") with
+ | _ -> false
+ | exception Loc.Error (_, m) -> contains m "and this is neither");
+
(* A refused name is a name that exists and cannot be had — Zig's failDecl,
which Load.refuse_hidden already implements for main. Nothing may be in
both lists, or asking for a name that works would report that it does
From 65fd49f0c612c8f707c9dc55bc756958d2dc4825 Mon Sep 17 00:00:00 2001
From: Joseph Ferano
Date: Sun, 13 Sep 2026 08:13:32 +0700
Subject: [PATCH 3/5] Where the generated half is written down, in all four
places it belongs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
web/index.html had no section on the FFI's generated half at all; it has one
now, with the command, the config, and the reason committing the output is what
makes the no-header property honest rather than a caveat.
BUILT.md gets why the 172 stay, which is the part that is easy to get wrong:
136 of them are exactly what the rule produces and the rest are expressible as
overrides, so the superset argument is sound and still leads somewhere bad —
deleting them reduces the signature check to a tautology.
DISCUSS.md 6a and 6b are answered rather than left open, and 6b's own point
about enums turns out to be live in the tree: key-down? keeps its Key
parameter because it is hand-written, and the generated key-up? beside it
takes an i32.
---
BUILT.md | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++
DISCUSS.md | 15 +++++++++
NEXT.md | 42 ++++++++----------------
web/index.html | 68 ++++++++++++++++++++++++++++++++++++--
4 files changed, 182 insertions(+), 32 deletions(-)
diff --git a/BUILT.md b/BUILT.md
index b6b7d8d..2152c32 100644
--- a/BUILT.md
+++ b/BUILT.md
@@ -162,6 +162,8 @@ described is refused with that reason, so `vendor/raylib` describing thirteen
structs is what makes the import thirteen structs wide, and describing a
fourteenth widens it. Of raylib 5.5's 581 functions, 256 import, 153 are
refused, and 172 are left alone because the package already binds them by hand.
+(253 and 156 once `bindings` excludes raylib's three allocator entry points —
+see the next section.)
Not generating `defstruct`s is what makes the check possible at all. Generate
them and the header becomes the authority on layout, and comparing the
@@ -355,6 +357,93 @@ header cache and the same build is 0.92s against 0.83s. So the header read is
compiling a shim with 428 wrappers in it, which is the object cache's business
and already warm after one build.
+### The bindings are committed now — `generated.flan`, `bindings`, `flan generate-c`
+
+The section above reads the header at build time, behind an opt-in
+`?${FLAN_RAYLIB_H}` in `headers`, because a build should need libraylib
+linkable and not raylib-devel installed. That opt-in was doing two jobs and
+only one of them was defensible: it decided whether a *check* ran, which is
+fine to make optional, and it decided whether a package had 172 bindings or
+428, which is not. `DrawTexturePro` being reachable only by exporting an
+environment variable is the shape of that second job, and it blocked a real
+game — see PORTING.md.
+
+**Generate once, commit the result, regenerate when raylib moves.**
+`flan generate-c vendor/raylib` reads the header named by `headers`, writes
+`vendor/raylib/generated.flan`, and that file is checked in. No header is
+needed by anybody: every build gets all 425 declarations, they are greppable,
+and they show up in a diff when the library moves. **Caching is not the
+argument** — the dump is already cached on disk and in memory, so a build that
+reads a header pays for it once either way. The argument is the dependency and
+the diff.
+
+**What it costs is the check, so regeneration runs it and the check gates the
+write.** A file on disk has no second opinion, so nothing compares the bindings
+against the library on an ordinary build any more. The one function that writes
+`generated.flan` therefore compares first — every `defstruct` against the
+header's record, every hand-written `declare-c` against the header's signature
+— and writes nothing when they disagree. It is not possible to regenerate
+without comparing, because there is no other way to write the file. Pointed at
+the 5.1-dev header on this machine while the package is written for 5.5, it
+reports the same ten real differences the diff above found and writes nothing.
+
+**The 172 hand-written lines stay, and the reason is not caution.** It was
+tempting to delete them: 136 of the 172 are exactly what the kebab rule would
+have produced, and the other 36 could be spelled as name overrides, so the
+generated set really is a superset. The argument against is the one this
+section is about. Everything the generator emits agrees with the header *by
+construction* — the declaration and the prototype come from one dump — so
+diffing generated output against the header it came from is a tautology, and
+replacing the hand-written set would quietly reduce the signature half of the
+check to nothing. The hand-written lines were transcribed from raylib's
+documentation by a person; they are the only declarations in the package a
+header can actually contradict. All ten of the 5.1-dev differences came from
+them. They are not a parallel set to maintain — they are the second opinion,
+and the check is what maintains them.
+
+The opt-in did not go away, it stopped deciding anything important. With
+`FLAN_RAYLIB_H` set, an ordinary build still reads the header, and since every
+C symbol is now bound — by hand or by generation — the importer generates
+nothing and the read is purely the check. It is a *better* check than before:
+425 declarations rather than 172, because `generated.flan` is a package file
+like any other and is checked like one.
+
+**`bindings`, beside `headers`, is what survives regeneration.** A committed
+generated file cannot be hand-corrected — the next run overwrites it and the
+edit is destroyed without anybody being told, which is the worst shape an edit
+can have — so the corrections have to live somewhere regeneration *reads*. Two
+directives, which are the two things the header cannot decide: `exclude ` and `name `. A postprocessing transform pass
+was considered and rejected: a second program to understand, run over text the
+generator had already committed to.
+
+Both are applied *while* the declarations are made, which is not a detail. The
+kebab rule is consulted in exactly one place, so collision groups are computed
+on the name a function will really take — which means renaming one of two
+colliding symbols dissolves the collision instead of leaving both refused, and
+`Spin2D`/`spin2d` gains a way out that is not a hand-written line. An excluded
+symbol still reports that it was excluded rather than going quiet: "there is no
+such binding" and "the package decided against this binding" are different
+answers.
+
+What is actually in raylib's: `exclude Mem*`, because raylib exports
+malloc/realloc/free under its own names and binding them would put a second
+untracked heap behind three innocuous-looking Flan names, against plan.org's
+rule that an operation never falls back to a hidden allocator. And 19 `name`
+lines giving the generated predicates the `?` spelling the hand-written ones
+already use — `window-ready?` rather than `is-window-ready`, because
+`key-pressed?` and `is-window-ready` living in one package is precisely the
+split this change exists to remove. The C symbol is kept verbatim in
+`Ast.DeclareC` either way, so a rename costs nothing: it is still what is
+called and still what the check compares against.
+
+Two bindings the config cannot express stay hand-written, and they are the
+reason `declare-c` remains the escape hatch: `LoadFontEx` and
+`LoadImageFromMemory` are bound `-raw` and wrapped by a Flan function of the
+same name without the suffix, one taking a slice and one answering with an
+`Option`. The importer refuses them by name collision with those wrappers,
+which is the correct answer.
+
### What a headless FFI test can and cannot pin
Worth knowing before writing another one, because two plausible tests in a row turned out to check nothing.
diff --git a/DISCUSS.md b/DISCUSS.md
index d76a798..854460a 100644
--- a/DISCUSS.md
+++ b/DISCUSS.md
@@ -182,6 +182,10 @@ build, and a permuted `Texture2D` or an `f64` for a `float` is caught by name.
What is left is two decisions, and both are the author's.
+**6a and 6b are answered. See BUILT.md, "The bindings are committed now".** It became a code generator whose
+output is committed, and the 172 hand-written lines were *not* migrated — for a reason 6b did not reach. The two
+sections below are kept as the reasoning that got there.
+
### 6a. Does reading the header stay a build-time step, or become a code generator?
The cost, measured, with the wrappers pruned by `Reach` as they already were:
@@ -222,6 +226,17 @@ better than the rule's. `IsKeyPressed` is `key-pressed?` by hand and `is-key-pre
would become an integer at the call site. A migration is therefore not a deletion; it is a deletion plus a
kept list of the lines whose face is deliberately nicer than the header's.
+**Answered: no, and the blocker is not the one above.** Vendoring stopped being the question once the *output* was
+committed rather than the header — no header is needed at any build. The count was also smaller than feared: 136 of
+the 172 are exactly what the rule produces, and the other 36 are expressible as `name` overrides in `bindings`.
+
+What actually decides it is that migration would gut the check. Everything the generator emits agrees with the header
+by construction, so diffing generated output against its own source proves nothing; the hand-written lines have a
+different author, so they are the only declarations a header can contradict — and all ten of the 5.1-dev differences
+came from them. Delete them and the signature half of the check silently becomes a tautology. The enum point above
+survives intact as a second reason: `(rl/key-down? :space)` keeps its `Key` parameter only because that line is
+hand-written.
+
## 7. Watching variables
Raised while designing item 1, and deliberately separated from it.
diff --git a/NEXT.md b/NEXT.md
index 53ea38d..7356cd0 100644
--- a/NEXT.md
+++ b/NEXT.md
@@ -1,27 +1,9 @@
-## Queued: commit the generated bindings, with a config beside them
+## Queued: an idiomatic layer over the generated bindings
-**Decided.** `flan import-c` already exists and prints the lines; kebab-casing is already implemented and reversible
-(the C symbol is kept verbatim in `Ast.DeclareC`, so the rule never has to be undone).
-
-**Generate once, commit the result, regenerate when raylib moves.** What changes against today's opt-in header read:
-no header is needed by anyone, so the `?${FLAN_RAYLIB_H}` split disappears and every build gets all 428 bindings; the
-bindings become greppable and diffable in the repo; and the hand-written 172 stop being a separate set to maintain.
-Caching is not the argument — the dump is already cached on disk and in memory.
-
-**What is given up, and it is the real cost:** the build-time check against the real header stops being automatic and
-becomes something run at regeneration. That check earned its place — it verified all 172 hand-written declarations and
-all 16 struct layouts against raylib 5.5 and found them exactly right, and against a 5.1-dev header on the same
-machine it found ten genuine differences. Keep it as a step, and make regeneration run it.
-
-**A config file beside `headers`**, because the generated file is committed and therefore any hand-edit is destroyed by
-the next regeneration — the config is what survives. Wanted: **exclude patterns**, and **name overrides** where
-kebab-casing gives something ugly. A postprocessing transform pass was considered and rejected: it is a second program
-to understand and the config covers the real cases.
-
-**Document it on the web page.** `web/index.html` has no section on the FFI's generated half.
-
-**Later, not now: an idiomatic layer.** Thin Flan-shaped wrappers *over* the generated bindings, not instead of them —
-the generated set stays honest to C, and the layer is where a Flan-shaped API lives.
+Thin Flan-shaped wrappers **over** the generated bindings, not instead of them. The generated set stays honest to C —
+that is what makes it checkable against the header — and the layer is where a Flan-shaped API lives. Two of these
+already exist by hand in `vendor/raylib/raylib.flan` and are the shape to copy: `collision-point-poly?` takes a slice
+and `collision-lines` answers with an `Option`, each wrapping a `-raw` binding of the same name.
## Queued: a restart is not a transaction, and the docs must say so
@@ -42,13 +24,15 @@ skip a frame and carry on rather than die — exactly the case where a non-idemp
Three things, in order. The first two are one line each and unblock a real game.
-**1. `DrawTexturePro` is not bound, and it is the one true blocker for `siam-farmer`.** See `PORTING.md`, written
-against both the Clojure implementation and the WIP Common Lisp port. Every tile in both goes through it;
-`DrawTextureRec` does not scale and `DrawTextureEx` takes no source rect, so the renderer **cannot be written at all**
-in a default build. It is reachable only through the opt-in `FLAN_RAYLIB_H` import, which `vendor/raylib/headers`
-deliberately keeps optional. One `declare-c` line.
+**1. `DrawTexturePro` — done, and not by a hand-written line.** It was the one true blocker for `siam-farmer`
+(see `PORTING.md`: every tile in both implementations goes through it, and neither `DrawTextureRec` nor
+`DrawTextureEx` substitutes). It was reachable only through the opt-in `FLAN_RAYLIB_H` import. The bindings are
+committed now, so `rl/draw-texture-pro` is in `vendor/raylib/generated.flan` and a default build has it. **Nothing
+should add it by hand** — a second `declare-c` for the same C symbol is refused for the whole program.
-**2. `Key` has no `left-shift`.** Both implementations use shift+1..5 to pick the tilemap. One enum member.
+**2. `Key` has no `left-shift`.** Both implementations use shift+1..5 to pick the tilemap. One enum member, and
+still a hand edit: the importer generates functions and only functions, so no `defenum` comes out of the header.
+`vendor/raylib/raylib.flan` is where `Key` lives.
**3. An out-of-bounds index should signal a condition, not `exit(134)`.** `PORTING.md`'s own first recommendation
after the binding lines. The game indexes grids straight from mouse coordinates — `game.lisp` had to add an
diff --git a/web/index.html b/web/index.html
index 22601ef..01cc815 100644
--- a/web/index.html
+++ b/web/index.html
@@ -1140,15 +1140,77 @@ void flan_shim_get_mouse_position_5ad0e205(flan_ty_Vector2_1bebc5ae *out) {
*out = GetMousePosition();
}
-No library header is read, deliberately, so a build needs the shared library to be
-linkable and not the -devel package to be installed.
+
No library header is read during a build, deliberately, so a build needs the shared
+library to be linkable and not the -devel package to be installed.
Guaranteed: the C typedef
and the Flan struct come from the same defstruct, so they cannot disagree,
and clang type-checks the wrapper against the generated prototype.
Trusted: that the defstruct matches the library's real
struct, and that the declare-c signature is the function's real signature.
A scalar's width now carries ABI weight — f64 where the library says
-float emits double, and the library reads garbage.
+float emits double, and the library reads garbage. That
+trusted half is what the generator below checks.
+
+Generated bindings, committed
+
+Writing a binding per function by hand does not scale past the ones a program
+happens to call, so most of raylib's package is not written by hand. A package
+directory may carry a headers file naming the library's own C header;
+flan generate-c <package-dir> reads it with
+clang -Xclang -ast-dump=json, turns every function it can represent into
+the same declare-c line a person would have written, and writes them to
+generated.flan in the package — which is committed.
+
+$ export FLAN_RAYLIB_H=/path/to/raylib-5.5/src/raylib.h
+$ flan generate-c vendor/raylib
+wrote vendor/raylib/generated.flan: 253 declarations, 156 refused, of 581 functions.
+Every defstruct and every hand-written declare-c agrees with it.
+
+Committing the output rather than generating at build time is what keeps the
+no-header property honest: the declarations are in the repository, so every build gets
+all of them, they are greppable, and they show up in a diff when the library moves. The
+argument is not caching — the clang dump is already cached on disk and in memory.
+
+Regeneration is the check. The cost of committing the output is that
+nothing compares the bindings against reality on every build any more, so the one
+function that writes the file compares first and refuses to write when the
+package and the header disagree: every defstruct against the header's
+record, and every hand-written declare-c against the header's signature.
+Pointed at a raylib 5.1-dev header while the package is written for 5.5, it reports ten
+real differences and writes nothing — which is exactly the silent version skew a
+generated file would otherwise bake in and make look reviewed.
+
+This is also why the hand-written bindings are kept rather than replaced by generated
+ones. Everything the generator emits agrees with the header by construction, so diffing
+generated output against the header it came from proves nothing; the hand-written lines
+were transcribed by a person, so they are the only declarations a header can actually
+contradict. All ten of those differences came from them.
+
+A committed generated file cannot be hand-corrected — the next regeneration destroys
+the edit without telling anybody — so the corrections live in a bindings
+file beside headers, which is read while the declarations are made.
+Two directives:
+
+# raylib's own malloc/realloc/free, which would be a second untracked heap
+# behind three innocuous Flan names.
+exclude Mem*
+
+# The kebab rule gives is-window-ready. Lisp spells a predicate with a ?.
+name IsWindowReady window-ready?
+
+An excluded function still says it was excluded rather than going quiet, and a name
+override changes only the Flan face — the C symbol is kept verbatim in the declaration,
+so it is still what is called and still what the check compares. Renaming is also the
+way out of a collision: Spin2D and spin2d both kebab to
+spin-2d, so neither takes the name, because which one won would otherwise
+depend on the order the header happens to declare them in.
+
+Anything neither directive can express is a hand-written declare-c in the
+package's own source, which wins over the generated file and is left alone by the
+generator. That is the escape hatch for a signature the importer gets wrong and for a
+Flan face the header cannot describe — raylib keeps two, each a raw binding wrapped by a
+Flan function of the same name, one taking a slice and one answering with an
+Option.
Everything the boundary cannot represent is refused by name with the reason, rather
than half-supported: an Option, a union, a fixed array, a map, a returned
From 7332bed14aa97ac548a8d3e2f3e680b15ff646b4 Mon Sep 17 00:00:00 2001
From: Joseph Ferano
Date: Sun, 13 Sep 2026 08:22:10 +0700
Subject: [PATCH 4/5] The acceptance case stops asking for a header it no
longer needs
The imported-bindings program was skipped without FLAN_RAYLIB_H, which was
right when the bindings only existed if a header was read and is now the one
gate hiding the change. It runs on the same terms as every other raylib case:
libraylib linkable, no raylib-devel. A generated.flan regenerated empty or
stale is now caught on an ordinary machine rather than only on one with a
header exported.
Reach.link already answers the shim worry BUILT.md's cold-build attribution
would otherwise raise: sand.flan links 110 wrappers, not 425, because the
bindings nothing reachable calls are dropped. What is left is 65ms of frontend
on a cold build, against a header read that was 60-90ms of a fresh session and
15.5ms of every redefinition. Both numbers are in BUILT.md now.
Also a swallowed line continuation in the exclusion message.
---
BUILT.md | 31 +++++++++++++++++++++++++++++++
lib/cimport.ml | 3 ++-
test/test_acceptance.ml | 32 +++++++++++++++-----------------
3 files changed, 48 insertions(+), 18 deletions(-)
diff --git a/BUILT.md b/BUILT.md
index 2152c32..9c79c32 100644
--- a/BUILT.md
+++ b/BUILT.md
@@ -437,6 +437,37 @@ split this change exists to remove. The C symbol is kept verbatim in
`Ast.DeclareC` either way, so a rename costs nothing: it is still what is
called and still what the check compares against.
+#### What committing 253 more declarations costs, measured
+
+The section above says the cost is the check. That is the cost that mattered,
+but it is not the only one, and this file does not omit measured numbers.
+
+Every build now carries 425 `declare-c` where a default build carried 172, and
+the obvious worry is the shim: BUILT.md's own cold-build attribution above
+blames "the object cache compiling a shim with 428 wrappers in it", and that
+was the *opt-in* path. It is not what happens, because `Reach.link` drops the
+bindings nothing reachable calls and the shim comes back in parts for exactly
+that purpose. `sand.flan` links **110** wrappers, not 425 — `nm sand | grep -c
+flan_shim_`. A program that never draws still compiles no drawing wrapper.
+(`flan shim ` prints the unpruned view, so it says 427 and is not the
+number a build pays.)
+
+What is left is frontend work on 253 more declarations, and it is small:
+
+| cold build of `sand.flan`, object cache cleared | best of 3 |
+|---|---|
+| 425 declarations (committed bindings) | 2.16s |
+| 172 declarations (`generated.flan` moved aside) | 2.10s |
+
+**+65ms, about 3%, and cold only** — the object cache serves the shim after one
+build, and the redefinition path never recompiles C at all. Against it: the
+header read this removes was 60–90ms of a fresh session by the measurement
+above, and DISCUSS.md 6a measured it at 15.5ms on *every* redefinition, which is
+a 50% increase on the number the dev-loop lane exists to keep small. So for
+anyone who had the opt-in switched on this is a straight win, and for everyone
+else it is 65ms once per cold build in exchange for 253 bindings that were
+previously unreachable.
+
Two bindings the config cannot express stay hand-written, and they are the
reason `declare-c` remains the escape hatch: `LoadFontEx` and
`LoadImageFromMemory` are bound `-raw` and wrapped by a Flan function of the
diff --git a/lib/cimport.ml b/lib/cimport.ml
index 387f5d4..2f99f6e 100644
--- a/lib/cimport.ml
+++ b/lib/cimport.ml
@@ -639,7 +639,8 @@ let of_dump ~env ~taken ~bound_syms ~config (d : dump) : imported =
hidden :=
(flan_name config f.csym,
Printf.sprintf
- "%s is excluded by the package's binding config, so no declaration is generated for it" f.csym)
+ "%s is excluded by the package's binding config, so no \
+ declaration is generated for it" f.csym)
:: !hidden)
dropped;
let groups = Hashtbl.create 512 in
diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml
index 54fd10b..b565855 100644
--- a/test/test_acceptance.ml
+++ b/test/test_acceptance.ml
@@ -802,23 +802,21 @@ let () =
TextLength of "hello" is 5, which is only true if the generated wrapper
NUL-terminated the copy.
- Skipped without FLAN_RAYLIB_H, because the import is opt-in — a build
- needs libraylib linkable and not raylib-devel installed, and that is a
- property worth keeping. The importer's own table does not skip: it runs
- against test/headers/sample.h, which is committed. *)
- (match Sys.getenv_opt "FLAN_RAYLIB_H" with
- | Some h when Sys.file_exists h
- && Sys.command "ldconfig -p 2>/dev/null | grep -q libraylib" = 0 ->
- let out = "10\n5\n287454020\n17\n34\n51\n68\n" in
- outputs "raylib, bindings read from the header" "programs/raylib-imported.flan" out;
- (* At -O0 too, for the reason the rest of the table is: every struct
- here crosses as (addr v) on a local, which is the alloca mem2reg
- would launder before anyone noticed it was wrong. *)
- outputs ~opt:"-O0" "raylib, bindings read from the header, -O0"
- "programs/raylib-imported.flan" out
- | _ ->
- print_endline
- "acceptance: skipping the imported-bindings case (FLAN_RAYLIB_H unset)");
+ This used to be skipped without FLAN_RAYLIB_H, because the generated
+ bindings only existed when a header was read. They are committed now —
+ vendor/raylib/generated.flan — so it runs on the same terms as every
+ other raylib case here: libraylib linkable, and no raylib-devel. That is
+ the change stated as a test rather than as a claim. If generated.flan
+ were ever regenerated empty or stale, this is what would say so, and it
+ would say so on an ordinary machine rather than only on one with a
+ header exported. *)
+ let out = "10\n5\n287454020\n17\n34\n51\n68\n" in
+ outputs "raylib, bindings generated from the header" "programs/raylib-imported.flan" out;
+ (* At -O0 too, for the reason the rest of the table is: every struct
+ here crosses as (addr v) on a local, which is the alloca mem2reg
+ would launder before anyone noticed it was wrong. *)
+ outputs ~opt:"-O0" "raylib, bindings generated from the header, -O0"
+ "programs/raylib-imported.flan" out;
(* raylib's Image family, headless, and the strongest FFI case here: an
Image is pixels in RAM, so raylib *computes* with it rather than
From 1504258afb1eae10d3f8976fce4cd375f64720b7 Mon Sep 17 00:00:00 2001
From: Joseph Ferano
Date: Sun, 13 Sep 2026 08:23:30 +0700
Subject: [PATCH 5/5] What the header check can actually contradict, said in
headers too
BUILT.md and the web page both make the point that the generated half agrees
with the header by construction; headers said "425 declarations" and left a
reader to infer that all 425 are independently verified. It is the file
somebody reads when deciding whether to export the variable at all.
---
vendor/raylib/headers | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/vendor/raylib/headers b/vendor/raylib/headers
index 3cbc5c1..cc92c18 100644
--- a/vendor/raylib/headers
+++ b/vendor/raylib/headers
@@ -17,9 +17,12 @@
# 2. an ordinary build, when the variable happens to be set. Every C symbol
# is bound already — by hand in raylib.flan or by generation in
# generated.flan — so the importer generates nothing and the header read
-# is purely the check. That check is now over all 425 declarations rather
-# than the 172 hand-written ones, because the generated file is a package
-# file like any other and is checked like one.
+# is purely the check. It runs over all 425 declarations rather than the
+# 172 hand-written ones, because the generated file is a package file like
+# any other and is checked like one — though only the hand-written ones can
+# actually disagree, since the generated half came out of this header and
+# agrees with it by construction. That is also why the hand-written lines
+# were kept rather than replaced by generated ones.
#
# Why it is still optional. A build needs libraylib linkable and *not*
# raylib-devel installed, which is a property worth keeping; requiring a header