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