The generated bindings are committed, and the hand-written three stay excluded from them

This commit is contained in:
Joseph Ferano 2026-09-13 08:27:17 +07:00
commit 324d1c6c60
12 changed files with 1036 additions and 67 deletions

120
BUILT.md
View File

@ -164,6 +164,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
@ -357,6 +359,124 @@ 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 <symbol
or pattern>` and `name <symbol> <flan-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.
#### 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 <file>` 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 6090ms 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
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.

View File

@ -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.

37
NEXT.md
View File

@ -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
@ -50,6 +32,15 @@ surface and must not be load-bearing, because the default build has no `FLAN_RAY
**2. `Key` has no `left-shift` — DONE.** `left-shift 340`, and nothing else: `PORTING.md` §5 checked every other enum
value the game touches and they were all already right.
**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, 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)` — DONE.** A failed bounds check signals
`BoundsError` with `error`; `runtime/flan_rt.c`'s `flan_bounds_error`/`flan_slice_error` walk the handlers, then offer

View File

@ -221,6 +221,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))
@ -267,6 +271,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
@ -411,6 +486,7 @@ let () =
prerr_endline
"usage: flan (read|parse|check|emit|shim) <file.flan>...\n\
\ flan import-c <header.h> [package.flan...] [clang flags...]\n\
\ flan generate-c <package-dir>\n\
\ flan build <file.flan> [-o out] [--no-bounds-checks] [--dev] \
[--debug] [--sanitize] [--target=wasm32-wasi|web]\n\
\ flan run <file.flan> [args...]\n\

View File

@ -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 <C symbol or pattern>` or `name <C \
symbol> <flan-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,33 @@ 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 +660,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 +925,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 +937,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 +1075,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 }

View File

@ -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.

View File

@ -838,23 +838,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

View File

@ -1377,7 +1377,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
@ -1437,6 +1437,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 { Loc.dmsg = 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

78
vendor/raylib/bindings vendored Normal file
View File

@ -0,0 +1,78 @@
# 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 <C symbol or pattern> do not generate a binding for this
# name <C symbol> <flan-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?
# Hand-written in raylib.flan, so excluded here: a second declare-c for one C
# symbol is refused for the whole program. These three are on a game's
# per-frame path (PORTING.md), and a hand-written line is what the signature
# check has to compare against -- generated output agrees with the header by
# construction, so it can only check the hand-written half.
exclude DrawTexturePro
exclude ImageFromImage
exclude IsWindowReady

264
vendor/raylib/generated.flan vendored Normal file
View File

@ -0,0 +1,264 @@
;;;; 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-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-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 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")

53
vendor/raylib/headers vendored
View File

@ -4,29 +4,52 @@
# 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. 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
# 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}

View File

@ -1140,15 +1140,77 @@ void flan_shim_get_mouse_position_5ad0e205(flan_ty_Vector2_1bebc5ae *out) {
*out = GetMousePosition();
}</code></pre>
<p>No library header is read, deliberately, so a build needs the shared library to be
linkable and not the <code>-devel</code> package to be installed.
<p>No library header is read during a build, deliberately, so a build needs the shared
library to be linkable and not the <code>-devel</code> package to be installed.
<strong>Guaranteed:</strong> the C typedef
and the Flan struct come from the same <code>defstruct</code>, so they cannot disagree,
and clang type-checks the wrapper against the generated prototype.
<strong>Trusted:</strong> that the <code>defstruct</code> matches the library's real
struct, and that the <code>declare-c</code> signature is the function's real signature.
A scalar's width now carries ABI weight — <code>f64</code> where the library says
<code>float</code> emits <code>double</code>, and the library reads garbage.</p>
<code>float</code> emits <code>double</code>, and the library reads garbage. That
trusted half is what the generator below checks.</p>
<h3>Generated bindings, committed</h3>
<p>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 <code>headers</code> file naming the library's own C header;
<code>flan generate-c &lt;package-dir&gt;</code> reads it with
<code>clang -Xclang -ast-dump=json</code>, turns every function it can represent into
the same <code>declare-c</code> line a person would have written, and writes them to
<code>generated.flan</code> in the package — which is <em>committed</em>.</p>
<pre><code class="sh">$ 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.</code></pre>
<p>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.</p>
<p><strong>Regeneration is the check.</strong> 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 <em>refuses to write</em> when the
package and the header disagree: every <code>defstruct</code> against the header's
record, and every hand-written <code>declare-c</code> 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.</p>
<p>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.</p>
<p>A committed generated file cannot be hand-corrected — the next regeneration destroys
the edit without telling anybody — so the corrections live in a <code>bindings</code>
file beside <code>headers</code>, which is read <em>while</em> the declarations are made.
Two directives:</p>
<pre><code class="sh"># 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?</code></pre>
<p>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: <code>Spin2D</code> and <code>spin2d</code> both kebab to
<code>spin-2d</code>, so neither takes the name, because which one won would otherwise
depend on the order the header happens to declare them in.</p>
<p>Anything neither directive can express is a hand-written <code>declare-c</code> 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
<code>Option</code>.</p>
<p>Everything the boundary cannot represent is refused by name with the reason, rather
than half-supported: an <code>Option</code>, a union, a fixed array, a map, a returned