An enum is four bytes, and the header check now reads the constants

Two gaps the raylib examples hit.

The layout check compared a Flan enum against the header's `int` and
called it a disagreement. It is not one: Shim.cty lowers a defenum to
int32_t in a struct field exactly as it does in a parameter, which is
what the signature check already knew and the layout check did not. One
predicate now serves both, symmetric, and tolerant of a 32-bit integer
and nothing else -- f64 against the library's float still fails, in the
very struct whose other field is an enum. Camera3D.projection is a
CameraProjection again and rl/camera-projection is gone with it, so
`.projection :perspective` resolves at the construction site.

And generate-c's claim said nothing about a defconst or a defenum
member, so a wrong flag bit was completely silent. `bindings` gained
`enum`, `const` and `constant` lines saying what a Flan constant is
called in C -- the prefix is nowhere in the Flan name, so it is declared
rather than guessed. Nothing goes quiet in either direction: a name the
rule builds and the header lacks is reported, a rule that reaches
nothing is reported, and a defenum with no line is itself a finding,
because otherwise the silence just moves up one level.

clang's dump gives anonymous EnumDecls for every raylib enum and no
value at all for an enumerator written without `= n`, so the constants
are one flat table and the values are counted the way C counts them.
cache_format bumped with the dump type.
This commit is contained in:
Joseph Ferano 2026-09-13 14:11:35 +07:00
parent 781295e862
commit 9223c9002a
10 changed files with 757 additions and 85 deletions

View File

@ -359,6 +359,76 @@ 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 check reaches the constants now — `bindings`, `check_constants`
The claim `generate-c` made was that every `defstruct` and every hand-written
`declare-c` agrees with the header, and that claim held. It said nothing about
a `defconst` or a `defenum` member — and **a wrong flag bit or a wrong enum
member is completely silent.** No link error, no type error; a window that does
not open, or a key that never fires. That is the class the header read exists
to catch and the class hardest to see by reading, and raylib's package carries
sixteen `ConfigFlags` bits and eight enums that were all transcribed by hand.
**A Flan constant has no C spelling stored anywhere, so one has to be built.**
A function never needs this: `declare-c` keeps the C symbol verbatim, so the
wrapper reads the library's spelling rather than reconstructing it. A constant
has no declaration to keep it in. So the rule is uppercase-and-underscore —
`left-shift` is `LEFT_SHIFT`, `msaa-4x-hint` is `MSAA_4X_HINT` — and the
*prefix*, which is nowhere in the Flan name, is declared in `bindings`:
```
enum Key KEY_
const flag- FLAG_
constant Gesture/double-tap GESTURE_DOUBLETAP
```
**Nothing goes quiet, in either direction, and that is most of the design.** A
name the rule builds and the header does not have is *reported*, because a
mapping that silently matched nothing would read as coverage and provide none
— worse than no check. A rule that reaches no Flan name is reported too, which
is what catches a typo in the prefix. And a `defenum` with no `enum` line is
itself a finding, because otherwise the silence simply moves up one level: the
next lane adds an enum, adds no line, and nothing notices. `enum Foo -` is how
a package says out loud that the header has nothing to check `Foo` against —
a sentence somebody wrote rather than a line nobody did.
`defconst` is deliberately not held to that. A package's constants are mostly
its own — raylib's 26 colours, an example's screen size — and demanding a line
for each would be noise with no second author behind it. `gesture-all` is the
honest case: 1023 is the OR of ten members and no enumerator has that value,
so nothing claims to check it.
**Two things about clang's dump this rests on, both found by looking rather
than assumed.** raylib's enums are *anonymous* — `typedef enum { FLAG_VSYNC_HINT
= 0x40, ... } ConfigFlags;` is an `EnumDecl` with no name and a separate
typedef beside it — so the constants are collected into one flat table, which
is the table C itself keeps at file scope anyway. And an enumerator written
without `= n` carries no value in the dump at all, so values are counted the
way C counts them; `TraceLogLevel` is eight members with one initialiser
between them, and reading only the explicit ones would have checked one of
eight and passed the rest.
### An enum-typed `defstruct` field is a layout, not a disagreement
`Shim.cty` lowers a Flan `defenum` to `int32_t` in a struct field exactly as it
does in a parameter, and a C enum is an `int`, so the two are the same four
bytes. The signature check knew that; the layout check did not, and reported
`field projection is CameraProjection in the defstruct and i32 (int)` — which
cost `Camera3D` an enum-typed field and bought a conversion function beside it.
One predicate now serves both, symmetric so the enum may be on either side.
The tolerance is for a 32-bit integer and **nothing else**, which is the whole
point: `f64` where the library says `float` lays out eight bytes where there
are four, every field after it moves, and it reads as plausible numbers rather
than as a link error. An enum against an `i16` or an `i64` is a real
disagreement and stays one.
What it buys is at the construction site. `.projection :perspective` resolves
against the enum's members and a typo is a compile error there — a keyword
resolves only where an enum type is expected, so an `i32` field would have
taken any number at all. Fixing the layout check is therefore the whole of that
second problem for this case: make the field legal and the keyword follows.
### The bindings are committed now — `generated.flan`, `bindings`, `flan generate-c`
The section above reads the header at build time, behind an opt-in

23
NEXT.md
View File

@ -46,20 +46,17 @@ why they are here and not in a binding list.
somewhere before a pointer can become a slice, and C does not supply one. Possibly a
`(slice-from-ptr p n)` where the caller states the length and owns being right about it.
2. **An enum-typed `defstruct` field is refused by the layout check**, even when the representation
is identical. The message is `DISAGREES Camera3D: field projection is CameraProjection in the
defstruct and i32 (int)`. The check is comparing a Flan enum against the header's `int` and
calling that a disagreement, which it is not — an enum is an `i32`. Worked around with an
`rl/camera-projection` helper so the call site still takes a checked keyword. Related and
probably the same decision: a keyword resolves only where an enum type is expected, so
`:perspective` cannot be written into an `i32` field directly.
2. ~~**An enum-typed `defstruct` field is refused by the layout check.**~~ **Closed.** The layout
check now accepts an enum where the header says `int`, symmetrically, and still refuses
anything that is not four bytes. `Camera3D.projection` is a `CameraProjection` again and the
`rl/camera-projection` helper is gone; `.projection :perspective` resolves at the construction
site, so the keyword half of the problem went away with it. See BUILT.md.
3. **The header check does not reach `defconst` or `defenum`.** `generate-c`'s claim is that every
`defstruct` and every hand-written `declare-c` agrees with the header, and that claim holds. It
says nothing about constants. **A wrong flag bit or a wrong enum member is silent** — which is
exactly the class of error the header read exists to catch, and the class hardest to see by
reading. This lane added 16 `ConfigFlags` constants and four enums by hand off the header, so the
values are right today and nothing would notice if they stopped being.
3. ~~**The header check does not reach `defconst` or `defenum`.**~~ **Closed.** It reaches both.
`bindings` gained `enum`, `const` and `constant` lines that say what a Flan constant is called in
C; every mapped name is compared by value, and a name the mapping cannot find, a rule that
reaches nothing, and a `defenum` with no line at all are each reported rather than skipped. All
eight raylib enums and all 16 `ConfigFlags` bits check out against 5.5. See BUILT.md.
4. **raymath is `static inline`, so there is no symbol to bind.** `Clamp`, `Vector2Add`, `Remap` and
the rest exist only in the header. `declare-c` has nothing to name. rlgl's matrix stack is

View File

@ -257,19 +257,50 @@ let () =
ds
in
if bound <> [] then
match Flan.Cimport.diff_bound ~env ~bound dump with
(match Flan.Cimport.diff_bound ~env ~bound dump with
| [] ->
Printf.printf
";; all %d hand-written declare-c agree with the header\n"
(List.length bound)
| ds ->
| diffs ->
Printf.printf ";; %d of %d hand-written declare-c disagree\n"
(List.length ds) (List.length bound);
(List.length diffs) (List.length bound);
List.iter
(fun (x : Flan.Cimport.sig_diff) ->
Printf.printf ";; DIFFERS %s (%s): %s\n"
x.Flan.Cimport.dflan x.Flan.Cimport.dsym x.Flan.Cimport.dwhy)
ds)
diffs);
(* And the constants, which until now nothing read at all: a wrong flag
bit or a wrong enum member is the one kind of error here that is
completely silent. *)
let enums =
List.filter_map
(fun (d : Flan.Ast.decl) ->
match d.Flan.Ast.d with
| Flan.Ast.Defenum (n, ms) -> Some (n, ms)
| _ -> None)
ds
and pconsts =
List.filter_map
(fun (d : Flan.Ast.decl) ->
match d.Flan.Ast.d with
| Flan.Ast.Defconst (n, _, e) -> Some (n, e)
| _ -> None)
ds
in
let config =
match pkg with
| f :: _ -> Flan.Load.binding_config (Filename.dirname f)
| [] -> Flan.Cimport.no_config
in
(match Flan.Cimport.check_constants ~config ~enums ~consts:pconsts dump with
| [] ->
if enums <> [] then
Printf.printf ";; every defenum member agrees with the header\n"
| bad ->
List.iter
(fun (n, why) -> Printf.printf ";; DISAGREES %s: %s\n" n why)
bad))
(* 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.
@ -324,21 +355,26 @@ let () =
Printf.eprintf "DIFFERS %s (%s): %s\n"
x.Flan.Cimport.dflan x.Flan.Cimport.dsym x.Flan.Cimport.dwhy)
r.Flan.Cimport.gsigs;
List.iter
(fun (n, why) -> Printf.eprintf "DISAGREES %s: %s\n" n why)
r.Flan.Cimport.gconsts;
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"
Every defstruct, every hand-written declare-c and every mapped\n\
constant 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"
"flan generate-c: %s disagrees with %s — %d struct layouts, %d \
hand-written signatures and %d constants. 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);
(List.length r.Flan.Cimport.gsigs)
(List.length r.Flan.Cimport.gconsts);
exit 1
end)

View File

@ -47,16 +47,16 @@
"raylib [core] example - core world screen")
(defer (rl/close-window))
;; `projection` is an i32 and not a CameraProjection, because the header
;; says the field is `int` and the layout check holds this file to that.
;; rl/camera-projection is the conversion, and it still takes a keyword, so
;; a misspelt projection is a compile error rather than a 0.
;; `projection` is a CameraProjection, so the keyword resolves against the
;; enum's members right here and a misspelt projection is a compile error
;; rather than a 0. raylib's field is `int` and the layout is unchanged —
;; an enum is four bytes either way.
(set camera
(rl/Camera3D {.position (rl/Vector3 {.x 10.0 .y 10.0 .z 10.0})
.target (rl/Vector3 {.x 0.0 .y 0.0 .z 0.0})
.up (rl/Vector3 {.x 0.0 .y 1.0 .z 0.0})
.fovy 45.0
.projection (rl/camera-projection :perspective)}))
.projection :perspective}))
(set cube (rl/Vector3 {.x 0.0 .y 0.0 .z 0.0}))

View File

@ -111,6 +111,25 @@ let kebab (s : string) : string =
s;
Buffer.contents b
(* The other direction, and it is deliberately not an inverse of [kebab].
A C *function* never needs one: the symbol is stored verbatim in the
declaration. A C *constant* does, because there is no declaration to store
it in a [defenum] member and a [defconst] are Flan names with Flan
values and nothing in the source says which enumerator in the header they
came from. So the C name has to be reconstructed, and this is the rule:
uppercase, and a hyphen becomes an underscore. [left-shift] is
[LEFT_SHIFT], [msaa-4x-hint] is [MSAA_4X_HINT].
What it cannot reconstruct is the prefix raylib's [KEY_], [FLAG_],
[GAMEPAD_BUTTON_] because the Flan name does not contain it. That is
declared in the package's [bindings] file rather than guessed, for the
reason [read_config] gives about every other guess: a rule that quietly
fails to match a name would be worse than no check at all. *)
let screaming (s : string) : string =
String.map (fun c -> if c = '-' then '_' else Char.uppercase_ascii c)
(String.uppercase_ascii s)
(* ── What clang was asked, and what it said ────────────────────────── *)
(* One C function, as the dump describes it and before anything is decided
@ -138,6 +157,15 @@ type dump = {
on every target this compiles for, which is also what [Shim] lowers a Flan
[defenum] to, so the two agree by construction. *)
enums : string list;
(* Every enumerator in the header, flat: name to value.
Flat and not grouped by enum, because raylib's enums are *anonymous*
[typedef enum { FLAG_VSYNC_HINT = 0x40, ... } ConfigFlags;] is an
[EnumDecl] with no name at all, and the typedef beside it is a separate
node. Keying on the enum's name would find nothing on the corpus this
exists for. C puts enumerators in the ordinary namespace at file scope
anyway, so the flat table is the one C itself keeps. *)
consts : (string * int64) list;
}
let clang_argv ~header ~flags =
@ -209,6 +237,7 @@ let read_dump ~header (root : Cjson.t) : dump =
let same f = try Unix.realpath f = want with Unix.Unix_error _ -> f = want in
let cur = ref "" in
let fns = ref [] and records = ref [] and typedefs = ref [] and enums = ref [] in
let consts = ref [] in
List.iter
(fun d ->
(match Cjson.mem "loc" d with
@ -274,6 +303,36 @@ let read_dump ~header (root : Cjson.t) : dump =
(Cjson.arr "inner" d)
in
if ok then records := { rname = nm; rfields } :: !records
(* An enumerator with no [= n] carries no [ConstantExpr] in the dump at
all, so the value has to be counted the way C counts it: one more
than the one before, starting at zero. That is not an edge case
raylib's TraceLogLevel writes [LOG_ALL = 0] and then seven bare
names, so reading only the explicit ones would check one member of
eight and quietly pass the rest. *)
| Some "EnumDecl", _ when mine ->
let next = ref 0L in
List.iter
(fun c ->
if Cjson.str "kind" c = Some "EnumConstantDecl" then begin
(match
List.find_map
(fun i ->
if Cjson.str "kind" i = Some "ConstantExpr" then
Cjson.str "value" i
else None)
(Cjson.arr "inner" c)
with
| Some v ->
(match Int64.of_string_opt v with
| Some n -> next := n
| None -> ())
| None -> ());
(match Cjson.str "name" c with
| Some n -> consts := (n, !next) :: !consts
| None -> ());
next := Int64.add !next 1L
end)
(Cjson.arr "inner" d)
| Some "TypedefDecl", Some nm when mine ->
(match qual d with
| Some u ->
@ -284,7 +343,8 @@ let read_dump ~header (root : Cjson.t) : dump =
| _ -> ())
(Cjson.arr "inner" root);
{ fns = List.rev !fns; records = List.rev !records;
typedefs = List.rev !typedefs; enums = List.rev !enums }
typedefs = List.rev !typedefs; enums = List.rev !enums;
consts = List.rev !consts }
(* ── C types into Flan types ───────────────────────────────────────── *)
@ -534,9 +594,32 @@ type config = {
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;
(* The three that say how a Flan constant is spelled in C, which is the one
thing neither the header nor the Flan source contains. See [screaming]
for why it has to be said and [check_constants] for what is done with it.
[enum_prefixes]: a [defenum]'s name, and the prefix its members carry in
C. [("Key", "KEY_")] checks [left-shift] against [KEY_LEFT_SHIFT]. The
prefix ["-"] means the header has nothing to check this enum against and
that is deliberate.
[const_prefixes]: a prefix of [defconst] names, and the prefix the
corresponding C enumerators carry. [("flag-", "FLAG_")] checks
[flag-vsync-hint] against [FLAG_VSYNC_HINT].
[constants]: one Flan name, spelled [Enum/member] or as a bare
[defconst] name, and the exact C name it answers to. The narrow
exception, for the one name a prefix rule gets wrong raylib's
[GESTURE_DOUBLETAP] against a member the package spells [double-tap]. It
also brings a name under the check that no prefix rule covers. *)
enum_prefixes : (string * string) list;
const_prefixes : (string * string) list;
constants : (string * string) list;
}
let no_config = { excludes = []; renames = [] }
let no_config =
{ excludes = []; renames = []; enum_prefixes = []; const_prefixes = [];
constants = [] }
(* [*] 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
@ -571,6 +654,7 @@ let read_config path : config =
else begin
let ch = open_in path in
let excludes = ref [] and renames = ref [] in
let enum_prefixes = ref [] and const_prefixes = ref [] and constants = ref [] in
let rec go n =
match input_line ch with
| line ->
@ -584,18 +668,26 @@ let read_config path : config =
match ws with
| [ "exclude"; p ] -> excludes := p :: !excludes
| [ "name"; sym; flan ] -> renames := (sym, flan) :: !renames
| [ "enum"; flan; c ] -> enum_prefixes := (flan, c) :: !enum_prefixes
| [ "const"; flan; c ] -> const_prefixes := (flan, c) :: !const_prefixes
| [ "constant"; flan; c ] -> constants := (flan, c) :: !constants
| _ ->
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
"a line here is `exclude <C symbol or pattern>`, `name <C \
symbol> <flan-name>`, `enum <FlanEnum> <C_PREFIX>`, `const \
<flan-prefix> <C_PREFIX>` or `constant <flan-name> <C_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 }
{ excludes = List.rev !excludes; renames = List.rev !renames;
enum_prefixes = List.rev !enum_prefixes;
const_prefixes = List.rev !const_prefixes;
constants = List.rev !constants }
end
(* [taken] is every name the package already declares, which is what makes the
@ -728,6 +820,41 @@ let of_dump ~env ~taken ~bound_syms ~config (d : dump) : imported =
struct it only ever holds by pointer, and a header that is a different
version of the library is a normal state of affairs to be told about rather
than stopped by. *)
(* Two rendered Flan types, compared for *representation* rather than for
spelling.
The one difference that is not a difference is an enum against a 32-bit
integer. A Flan [defenum] is an [i32] that is what [Shim.cty] lowers one
to, in a struct field and in a parameter alike and a C enum is an [int],
so the two lay out the same four bytes and differ only in the face they
present. Signedness goes with it: raylib spells [IsGestureDetected]'s
parameter [unsigned int] and the package calls it [Gesture], and both are
four bytes in a register.
Symmetric on purpose. The enum may be on either side: the header says
[CameraProjection] and the [defstruct] says [i32], or the header says [int]
and the [defstruct] says [CameraProjection]. Both are the same statement
about four bytes, and reporting either was the bug this closes.
What it does *not* accept is anything else. [f64] against the library's
[float] is eight bytes where there are four, every field after it moves,
and it reads as plausible numbers rather than as a link error which is
the whole reason this check exists. An enum against an [i16], an [i64] or
an [f32] is a real disagreement and stays one. *)
let enum_like env (t : Ast.texpr) =
match t.Ast.t with
| Ast.Tname n -> List.mem n env.known_enums
| _ -> false
let int32_like s = String.equal s "i32" || String.equal s "u32"
let agrees env (a : Ast.texpr) (b : Ast.texpr) =
let na = ty_source a and nb = ty_source b in
String.equal na nb
|| (enum_like env a && int32_like nb)
|| (enum_like env b && int32_like na)
let check_structs ~env ~(structs : (string * Ast.field list) list) (d : dump) =
let record n =
match List.find_opt (fun r -> r.rname = n) d.records with
@ -767,7 +894,7 @@ let check_structs ~env ~(structs : (string * Ast.field list) list) (d : dump) =
| None -> None (* a field type this cannot render says nothing *)
| Some want ->
let a = ty_source want and b = ty_source f.Ast.fty in
if String.equal a b then None
if agrees env want f.Ast.fty then None
else
Some
(Printf.sprintf "field %s is %s in the defstruct and %s (%s) in %s"
@ -781,6 +908,164 @@ let check_structs ~env ~(structs : (string * Ast.field list) list) (d : dump) =
| Some r -> Option.map (fun m -> (n, m)) (field_mismatch fs r))
structs
(* ── Checking the package's constants against the header's ─────────── *)
(* The half of the claim that was missing, and the one with the worst failure
mode.
[generate-c] used to say that every [defstruct] and every hand-written
[declare-c] agreed with the header, and that claim held. It said nothing
about [defconst] or [defenum] so a wrong flag bit or a wrong enum member
was *silent*, which is exactly the class of error the header read exists to
catch and the one hardest to see by reading. [FLAG_WINDOW_MINIMIZED] is
0x200; typing 0x100 gives a program that opens a window and hides it, with
no diagnostic anywhere and nothing in the source that looks wrong.
{2 How a Flan name is turned into a C one}
Not by a rule alone. [screaming] reconstructs [LEFT_SHIFT] from
[left-shift], but the prefix the C name carries [KEY_], [FLAG_],
[GAMEPAD_BUTTON_] is nowhere in the Flan source, so it is *declared*, in
the package's [bindings] file:
{v
enum Key KEY_ # (defenum Key [left-shift 340]) vs KEY_LEFT_SHIFT
const flag- FLAG_ # (defconst flag-vsync-hint ...) vs FLAG_VSYNC_HINT
constant Gesture/double-tap GESTURE_DOUBLETAP # the one the rule misses
v}
{2 Nothing goes quiet, in either direction}
A name the rule builds and the header does not have is *reported* and not
skipped. A mapping that silently matched nothing would be worse than no
check at all: it would read as coverage and provide none.
For the same reason a [defenum] with no [enum] line is itself a finding.
Otherwise the silence simply moves up one level the next lane adds an
enum, adds no line, and nothing notices. [enum Foo -] is how a package says
out loud that the header has nothing to check [Foo] against; it is a
sentence somebody wrote rather than a line nobody did. An [enum] or [const]
rule that matches no Flan name at all is a finding too, which is what
catches a typo in the rule.
[defconst] is *not* held to that. A package's constants are mostly its own
raylib's 26 colours, a screen size and demanding a line for each would
be noise with no second author behind it. A [defconst] is checked when a
rule or a [constant] line brings it under the check, and raylib's
[gesture-all] is the honest example of one that is not: 1023 is the OR of
ten members and no C enumerator has that value to compare against. *)
let check_constants ~config
~(enums : (string * (string * int64) list) list)
~(consts : (string * Ast.expr) list) (d : dump) : (string * string) list =
let found = Hashtbl.create 512 in
List.iter (fun (n, v) -> Hashtbl.replace found n v) d.consts;
let out = ref [] in
let say name fmt = Printf.ksprintf (fun m -> out := (name, m) :: !out) fmt in
(* One Flan name, its value, and the C name it claims to be. *)
let compare_one flan v cname =
match Hashtbl.find_opt found cname with
| None ->
say flan
"the header has no constant named %s, so nothing here checks %s — \
fix the mapping in `bindings` or the name in the source"
cname flan
| Some cv ->
if not (Int64.equal cv v) then
say flan "%s is %Ld here and %s is %Ld in the header" flan v cname cv
in
let explicit = config.constants in
let used = Hashtbl.create 16 in
(* Enum members. *)
List.iter
(fun (ename, members) ->
match List.assoc_opt ename config.enum_prefixes with
| None ->
say ename
"the defenum %s has no `enum` line in the package's `bindings`, so \
nothing checks its members against the header add `enum %s \
<C_PREFIX>`, or `enum %s -` to say the header has nothing to \
check it against"
ename ename ename
| Some "-" -> Hashtbl.replace used ("enum:" ^ ename) ()
| Some prefix ->
Hashtbl.replace used ("enum:" ^ ename) ();
List.iter
(fun (m, v) ->
let flan = ename ^ "/" ^ m in
let cname =
match List.assoc_opt flan explicit with
| Some c -> c
| None -> prefix ^ screaming m
in
compare_one flan v cname)
members)
enums;
(* Plain constants, and only the ones a rule or a [constant] line reaches. *)
let int_of (e : Ast.expr) =
match e.Ast.e with Ast.Int v -> Some v | _ -> None
in
List.iter
(fun (n, e) ->
let cname =
match List.assoc_opt n explicit with
| Some c -> Some c
| None ->
List.find_map
(fun (fp, cp) ->
match strip_prefix fp n with
| Some rest ->
Hashtbl.replace used ("const:" ^ fp) ();
Some (cp ^ screaming rest)
| None -> None)
config.const_prefixes
in
match cname with
| None -> ()
| Some cname ->
(match int_of e with
| Some v -> compare_one n v cname
| None ->
say n
"%s is mapped to %s but its value is not a plain integer, so \
there is nothing to compare"
n cname))
consts;
(* A rule that reaches nothing. A typo in a prefix would otherwise read as
coverage and provide none, which is the failure this whole section is
about. *)
List.iter
(fun (ename, _) ->
if not (Hashtbl.mem used ("enum:" ^ ename)) then
say ename
"`enum %s` in the package's `bindings` names no defenum the \
package declares" ename)
config.enum_prefixes;
List.iter
(fun (fp, _) ->
if not (Hashtbl.mem used ("const:" ^ fp)) then
say fp
"`const %s` in the package's `bindings` matches no defconst the \
package declares" fp)
config.const_prefixes;
(* And a [constant] line naming nothing. *)
List.iter
(fun (flan, _) ->
let known =
List.mem_assoc flan consts
|| List.exists
(fun (ename, members) ->
List.exists (fun (m, _) -> String.equal (ename ^ "/" ^ m) flan)
members)
enums
in
if not known then
say flan
"`constant %s` in the package's `bindings` names no defconst and no \
enum member the package declares" flan)
explicit;
List.rev !out
(* ── The entry point ───────────────────────────────────────────────── *)
let dump_of_clang ~loc ~header ~flags =
@ -818,7 +1103,7 @@ let dump_of_clang ~loc ~header ~flags =
stable across builds, and safe to delete. One directory to clear rather than
two. *)
let cache_format = 1
let cache_format = 2
(* The same directory [Build.cachedir] makes, spelled here rather than called:
[Load] is upstream of this file and downstream of [Reach], so reaching
@ -942,7 +1227,10 @@ let header ~loc ~header:h ~flags ~known_structs ~known_enums ~taken ~bound_syms
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)))
@ ("\001" :: sorted (List.map (fun (a, b) -> a ^ "=" ^ b) config.renames))
@ ("\001" :: sorted (List.map (fun (a, b) -> a ^ "=" ^ b) config.enum_prefixes))
@ ("\001" :: sorted (List.map (fun (a, b) -> a ^ "=" ^ b) config.const_prefixes))
@ ("\001" :: sorted (List.map (fun (a, b) -> a ^ "=" ^ b) config.constants)))
in
match Hashtbl.find_opt imports k with
| Some r -> r
@ -1014,27 +1302,11 @@ let diff_bound ~env ~(bound : (Ast.fn * string) list) (d : dump) =
dwhy = "the header does not declare this function at all" }
| Some c ->
let say why = Some { dsym = csym; dflan = fn.Ast.name; dwhy = why } in
(* An enum on the Flan side against a plain int from the header is
the expected difference and not a finding that is what a Flan
[defenum] *is* at the boundary, and giving it a name is the whole
point of declaring one. Signedness goes with it: raylib spells
[IsGestureDetected]'s parameter [unsigned int] and the package
calls it [Gesture], and since both are four bytes in a register
there is no ABI difference to report. What is still reported is an
enum against something that is *not* a 32-bit integer, which would
be a real one. *)
let enum_like (t : Ast.texpr) =
match t.Ast.t with
| Ast.Tname n -> List.mem n env.known_enums
| _ -> false
in
let int32_like s = String.equal s "i32" || String.equal s "u32" in
(* [agrees] above is the whole of it: an enum against a 32-bit
integer is the expected difference and not a finding, and an enum
against anything else still is one. *)
let norm (t : Ast.texpr) = ty_source t in
let same a b =
String.equal (norm a) (norm b)
|| (enum_like a && int32_like (norm b))
|| (enum_like b && int32_like (norm a))
in
let same a b = agrees env a b in
if c.cvariadic then None
else if List.length fn.Ast.params <> List.length c.cparams then
say
@ -1119,6 +1391,7 @@ type regen = {
ghidden : (string * string) list;
gstructs : (string * string) list;
gsigs : sig_diff list;
gconsts : (string * string) list;
}
let banner h =
@ -1155,9 +1428,12 @@ let regenerate ~loc ~header:h ~flags ~(ds : Ast.decl list) ~config ~out =
let structs =
pick (fun (d : Ast.decl) ->
match d.Ast.d with Ast.Defstruct (n, fs) -> Some (n, fs) | _ -> None)
and known_enums =
and enums =
pick (fun (d : Ast.decl) ->
match d.Ast.d with Ast.Defenum (n, _) -> Some n | _ -> None)
match d.Ast.d with Ast.Defenum (n, ms) -> Some (n, ms) | _ -> None)
and pconsts =
pick (fun (d : Ast.decl) ->
match d.Ast.d with Ast.Defconst (n, _, e) -> Some (n, e) | _ -> None)
and bound_syms =
pick (fun (d : Ast.decl) ->
match d.Ast.d with
@ -1169,11 +1445,12 @@ let regenerate ~loc ~header:h ~flags ~(ds : Ast.decl list) ~config ~out =
in
let imported, dump, env =
header ~loc ~header:h ~flags ~known_structs:(List.map fst structs)
~known_enums ~taken ~bound_syms ~config
~known_enums:(List.map fst 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
let gconsts = check_constants ~config ~enums ~consts:pconsts dump in
let gwrote = gstructs = [] && gsigs = [] && gconsts = [] in
if gwrote then begin
let b = Buffer.create 65536 in
Buffer.add_string b (banner h);
@ -1187,4 +1464,5 @@ let regenerate ~loc ~header:h ~flags ~(ds : Ast.decl list) ~config ~out =
close_out ch
end;
{ gwrote; gdecls = List.length imported.decls;
gfns = List.length dump.fns; ghidden = imported.hidden; gstructs; gsigs }
gfns = List.length dump.fns; ghidden = imported.hidden; gstructs; gsigs;
gconsts }

View File

@ -752,11 +752,18 @@ let rec import ~seen ~open_ ~loc alias dir =
| Ast.Defstruct (n, _) -> Some n
| _ -> None)
ds
and known_enums =
and enums =
List.filter_map
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.Defenum (n, _) -> Some n
| Ast.Defenum (n, ms) -> Some (n, ms)
| _ -> None)
ds
and pconsts =
List.filter_map
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.Defconst (n, _, e) -> Some (n, e)
| _ -> None)
ds
(* A C symbol the package already binds by hand is left alone:
@ -773,9 +780,10 @@ let rec import ~seen ~open_ ~loc alias dir =
| _ -> None)
ds
in
let config = binding_config dir in
let r, dump, env =
Cimport.header ~loc ~header:h ~flags ~known_structs ~known_enums
~taken ~bound_syms ~config:(binding_config dir)
Cimport.header ~loc ~header:h ~flags ~known_structs
~known_enums:(List.map fst enums) ~taken ~bound_syms ~config
in
(* The point of reading the header, and the reason it is not
enough to generate declarations out of it.
@ -845,6 +853,33 @@ let rec import ~seen ~open_ ~loc alias dir =
"the declare-c of %s disagrees with %s: %s"
x.Cimport.dflan h x.Cimport.dwhy)
(Cimport.diff_bound ~env ~bound dump);
(* And the constants, which nothing read until now. A wrong flag
bit and a wrong enum member are the two errors in this file
that are completely silent no link error, no type error,
just a window that does not open or a key that never fires
and they are the class the header read exists to catch. The
mapping from a Flan name to a C one is declared in `bindings`
rather than guessed; see [Cimport.check_constants]. *)
let cloc n =
(* A member reads [Key/left-shift]; the declaration that can be
pointed at is the defenum, so the name is cut at the slash. *)
let n =
match String.index_opt n '/' with
| Some i -> String.sub n 0 i
| None -> n
in
List.find_map
(fun (d : Ast.decl) ->
match Ast.declared_name d with
| Some m when String.equal m n -> Some d.Ast.dloc
| _ -> None)
ds
in
List.iter
(fun (n, why) ->
fail (Option.value ~default:loc (cloc n))
"the package disagrees with %s: %s" h why)
(Cimport.check_constants ~config ~enums ~consts:pconsts dump);
r)
(header_specs ~loc dir)
in

View File

@ -20,6 +20,31 @@ typedef struct Pair Point;
typedef enum Mood { MOOD_CALM = 0, MOOD_CROSS = 1 } Mood;
/* Constants, for the defconst and defenum check.
*
* Anonymous on purpose: this is the shape raylib uses everywhere the
* EnumDecl carries no name at all and the typedef beside it is a separate
* node so enumerators have to be collected flat rather than keyed on the
* enum they came from. SHADE_DARK has no initialiser, so its value is counted
* from the one before rather than read; raylib's TraceLogLevel is written
* exactly that way and reading only the explicit ones would check one member
* of eight. SHADE_HALFDARK is the one a prefix rule cannot reach, the way
* raylib writes GESTURE_DOUBLETAP where every sibling is underscored. */
typedef enum { SHADE_LIGHT = 4, SHADE_MID = 5, SHADE_DARK, SHADE_HALFDARK = 9 } Shading;
/* A bitfield, which a Flan package holds as separate defconsts rather than as
* one enum because the call takes the OR of several raylib's ConfigFlags. */
typedef enum { OPT_LOUD = 1, OPT_FAST = 2, OPT_FANCY_MODE = 4 } Options;
/* An int field a package may reasonably describe with an enum: the two are
* the same four bytes and the enum is the better face. `scale` beside it is
* the width the check must go on refusing. */
typedef struct Mode { int kind; float scale; } Mode;
/* And the same thing the other way round, so the tolerance is symmetric: the
* header names the enum and the package may say i32. */
typedef struct Feel { Mood mood; int n; } Feel;
typedef void (*Notify)(void *user, unsigned int n);
/* --- accepted --- */

View File

@ -1527,7 +1527,7 @@ let () =
in
let lines, hidden =
with_config { Cimport.excludes = [ "set_seed" ]; renames = [] }
with_config { Cimport.no_config with Cimport.excludes = [ "set_seed" ] }
in
check "an excluded symbol is not generated"
(not (List.exists (fun l -> contains l "\"set_seed\"") lines));
@ -1540,7 +1540,7 @@ let () =
hidden);
let lines, _ =
with_config { Cimport.excludes = [ "add_*" ]; renames = [] }
with_config { Cimport.no_config with Cimport.excludes = [ "add_*" ] }
in
check "an exclude pattern matches by prefix"
(not (List.exists (fun l -> contains l "\"add_ints\"") lines));
@ -1549,7 +1549,7 @@ let () =
let lines, _ =
with_config
{ Cimport.excludes = []; renames = [ ("set_seed", "seed!") ] }
{ Cimport.no_config with Cimport.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
@ -1564,7 +1564,7 @@ let () =
so the rename dissolves the group rather than leaving both refused. *)
let lines, hidden =
with_config
{ Cimport.excludes = []; renames = [ ("Spin2D", "spin-2d-upper") ] }
{ Cimport.no_config with Cimport.renames = [ ("Spin2D", "spin-2d-upper") ] }
in
check "a rename resolves a collision for both halves"
(List.exists (fun l -> contains l "\"Spin2D\"") lines
@ -1645,6 +1645,193 @@ let () =
~structs:(structs_of (program "(defstruct Nowhere [q i32])\n")) dump
= []);
(* An enum field against the header's [int]. A Flan defenum lowers to
int32_t in a struct field exactly as it does in a parameter, so this is
the same four bytes with a better face on it and not a disagreement
which is what let raylib's Camera3D.projection stop being an i32 with a
conversion function beside it. *)
check "an enum-typed field against the header's int is not reported"
(Cimport.check_structs ~env
~structs:(structs_of (program "(defstruct Mode [kind Mood scale f32])\n"))
dump
= []);
(* Symmetric: the header may be the side that names the enum. *)
check "an i32 field against the header's enum is not reported"
(Cimport.check_structs ~env
~structs:(structs_of (program "(defstruct Feel [mood i32 n i32])\n")) dump
= []);
(* And the whole point of the check survives it. The tolerance is for a
32-bit integer and nothing else, so the width hazard BUILT.md names f64
where the library says float still fails, in the very struct whose
other field is an enum. *)
check "a widened field beside an enum field is still reported"
(match
Cimport.check_structs ~env
~structs:(structs_of (program "(defstruct Mode [kind Mood scale f64])\n"))
dump
with
| [ ("Mode", why) ] -> contains why "f64" && contains why "f32"
| _ -> false);
(* An enum is four bytes, so an enum against something that is not four
bytes is a real disagreement and stays one. *)
check "an enum against a field that is not 32 bits is still reported"
(match
Cimport.check_structs ~env
~structs:(structs_of (program "(defstruct Feel [mood i64 n i32])\n")) dump
with
| [ ("Feel", why) ] -> contains why "i64"
| _ -> false);
(* ── The constants (Cimport.check_constants) ───────────────────── *)
(* The half of generate-c's claim that used to be missing. A wrong flag bit
and a wrong enum member are the two errors here that are completely
silent no link error, no type error which is exactly the class the
header read exists to catch.
Shading is anonymous in sample.h and its typedef carries the name, which
is how raylib writes every one of its enums; SHADE_DARK has no
initialiser, so 6 is counted rather than read. *)
let const_fixture ?(mood = "[calm 0 cross 1]")
?(shading = "[light 4 mid 5 dark 6 half-dark 9]") ?(fancy = "4")
?(extra = "") () =
Printf.sprintf
"(defenum Mood %s)\n\
(defenum Shading %s)\n\
(defconst opt-loud u32 1)\n\
(defconst opt-fast u32 2)\n\
(defconst opt-fancy-mode u32 %s)\n\
(defconst lucky u32 7)\n\
%s"
mood shading fancy extra
in
let enums_of ds =
List.filter_map
(fun (d : Ast.decl) ->
match d.Ast.d with Ast.Defenum (n, ms) -> Some (n, ms) | _ -> None)
ds
and pconsts_of ds =
List.filter_map
(fun (d : Ast.decl) ->
match d.Ast.d with Ast.Defconst (n, _, e) -> Some (n, e) | _ -> None)
ds
in
let mapping =
{ Cimport.no_config with
Cimport.enum_prefixes = [ ("Mood", "MOOD_"); ("Shading", "SHADE_") ];
const_prefixes = [ ("opt-", "OPT_") ];
(* SHADE_HALFDARK is one word where its siblings are underscored, so the
prefix rule cannot reach it. The narrow exception, said once. *)
constants = [ ("Shading/half-dark", "SHADE_HALFDARK") ] }
in
let constants ?(config = mapping) src =
Cimport.check_constants ~config ~enums:(enums_of (program src))
~consts:(pconsts_of (program src)) dump
in
check "constants that agree with the header are not reported"
(constants (const_fixture ()) = []);
(* The value is compared, which is the whole point: 340 is KEY_LEFT_SHIFT
and 341 is a key that never fires. *)
check "a wrong enum member value is reported, by its C name"
(match constants (const_fixture ~mood:"[calm 0 cross 2]" ()) with
| [ ("Mood/cross", why) ] ->
contains why "MOOD_CROSS" && contains why "is 2 here"
| _ -> false);
(* An enumerator with no [= n] carries no value in clang's dump at all, so
it has to be counted the way C counts it. raylib's TraceLogLevel is eight
members with one initialiser between them. *)
check "an implicitly-numbered enumerator is counted, not skipped"
(match
constants
(const_fixture ~shading:"[light 4 mid 5 dark 7 half-dark 9]" ())
with
| [ ("Shading/dark", why) ] ->
contains why "SHADE_DARK" && contains why "is 6 in the header"
| _ -> false);
(* A name the rule builds and the header does not have is reported and not
skipped. A mapping that quietly matched nothing would read as coverage
and provide none, which would be worse than no check. *)
check "a member the header has no constant for is reported"
(match constants (const_fixture ~mood:"[calm 0 cross 1 murky 2]" ()) with
| [ ("Mood/murky", why) ] -> contains why "no constant named MOOD_MURKY"
| _ -> false);
(* The defconst half. These are raylib's 16 ConfigFlags bits. *)
check "a wrong defconst value is reported"
(match constants (const_fixture ~fancy:"8" ()) with
| [ ("opt-fancy-mode", why) ] ->
contains why "OPT_FANCY_MODE" && contains why "is 4 in the header"
| _ -> false);
(* And a defconst no rule reaches is not reported: a package's constants are
mostly its own, and raylib's 26 colours have no enumerator behind them. *)
check "a defconst no rule reaches is left alone"
(constants (const_fixture ~extra:"(defconst unmapped u32 99)\n" ()) = []);
(* The [constant] line is what reaches a name the prefix rule gets wrong. *)
check "without the constant line the odd name is reported"
(match
constants ~config:{ mapping with Cimport.constants = [] }
(const_fixture ())
with
| [ ("Shading/half-dark", why) ] ->
contains why "no constant named SHADE_HALF_DARK"
| _ -> false);
(* Coverage itself must not go quiet. A defenum nobody mapped would be
silently unchecked, which is the same hole one level up. *)
check "a defenum with no enum line is itself a finding"
(match constants (const_fixture ~extra:"(defenum Nobody [a 0])\n" ()) with
| [ ("Nobody", why) ] -> contains why "no `enum` line"
| _ -> false);
(* And the way to say so deliberately, for an enum the header cannot check. *)
check "enum - excuses an enum the header says nothing about"
(constants
~config:
{ mapping with
Cimport.enum_prefixes = ("Nobody", "-") :: mapping.Cimport.enum_prefixes }
(const_fixture ~extra:"(defenum Nobody [a 0])\n" ())
= []);
(* A rule that reaches nothing is a typo, and it would otherwise read as
coverage. *)
check "an enum rule naming no defenum is reported"
(match
constants
~config:
{ mapping with
Cimport.enum_prefixes =
("Ghost", "G_") :: mapping.Cimport.enum_prefixes }
(const_fixture ())
with
| [ ("Ghost", why) ] -> contains why "names no defenum"
| _ -> false);
check "a const rule matching no defconst is reported"
(match
constants
~config:
{ Cimport.no_config with Cimport.const_prefixes = [ ("zzz-", "ZZZ_") ] }
"(defconst lucky u32 7)\n"
with
| [ ("zzz-", why) ] -> contains why "matches no defconst"
| _ -> false);
check "a constant line naming nothing is reported"
(match
constants
~config:
{ Cimport.no_config with
Cimport.constants = [ ("Mood/nope", "MOOD_NOPE") ] }
"(defconst lucky u32 7)\n"
with
| [ ("Mood/nope", why) ] -> contains why "names no defconst"
| _ -> false);
(* The name rule, which is not an inverse of kebab and does not need to be:
a constant has no declaration to store its C spelling in. *)
List.iter
(fun (flan, c) ->
check
(Printf.sprintf "screaming %s -> %s" flan c)
(String.equal (Cimport.screaming flan) c))
[ ("left-shift", "LEFT_SHIFT"); ("msaa-4x-hint", "MSAA_4X_HINT");
("a", "A"); ("window-mouse-passthrough", "WINDOW_MOUSE_PASSTHROUGH") ];
(* diff_bound: a hand-written declare-c against the header's own signature.
This is the check with no other source a wrong declare-c is wrong in the
generated prototype too, so the two halves agree with each other and only

View File

@ -114,3 +114,52 @@ exclude DrawGrid
exclude SetExitKey
exclude UpdateCamera
exclude GetWorldToScreen
# ── What the package's constants are called in C ────────────────────
#
# enum <FlanEnum> <C_PREFIX> every member of that defenum
# const <flan-prefix> <C_PREFIX> every defconst whose name starts so
# constant <flan-name> <C_NAME> one name, exactly
#
# Why any of this is needed. `generate-c` used to claim only that every
# defstruct and every hand-written declare-c agreed with raylib.h. It said
# nothing about a defconst or a defenum member — so a wrong flag bit or a
# wrong enum value was *silent*: no link error, no type error, just a window
# that does not open. These lines are what let the check reach them.
#
# The Flan member name becomes the C one by uppercasing and turning `-` into
# `_`, which gets KEY_LEFT_SHIFT out of `left-shift` and FLAG_MSAA_4X_HINT out
# of `msaa-4x-hint`. What it cannot get is the prefix, because the prefix is
# nowhere in the Flan name — so the prefix is said here rather than guessed. A
# name the rule builds and the header does not have is REPORTED and not
# skipped; a mapping that quietly matched nothing would read as coverage and
# provide none.
#
# Every defenum needs a line, including one the header cannot check, which
# says so with `-`. That is the same rule one level up: an enum nobody mapped
# would be silently unchecked, which is the hole this closes.
enum Key KEY_
enum MouseButton MOUSE_BUTTON_
enum TraceLogLevel LOG_
enum CameraProjection CAMERA_
enum CameraMode CAMERA_
enum GamepadButton GAMEPAD_BUTTON_
enum GamepadAxis GAMEPAD_AXIS_
enum Gesture GESTURE_
# raylib writes GESTURE_DOUBLETAP as one word where every other member of that
# enum is underscored. This is the narrow exception and not a general escape
# hatch: one name the prefix rule gets wrong, said once.
constant Gesture/double-tap GESTURE_DOUBLETAP
# The 16 ConfigFlags bits. These are the values sand.flan and the ported
# window-flags example pass to set-config-flags, set-window-state and
# clear-window-state, and each is a single bit read off raylib.h by hand —
# exactly the transcription this check exists to second-guess.
const flag- FLAG_
# What is deliberately NOT mapped: the 26 colours (a Color is a struct, not an
# enumerator), the examples' screen sizes, and `gesture-all`. That last one is
# 1023, the OR of all ten Gesture members, and raylib has no enumerator with
# that value — there is nothing in the header to compare it against, so
# nothing claims to.

View File

@ -303,30 +303,25 @@
;; the first three fields are the same type and the same size, so a permuted
;; Camera3D has the identical layout and every acceptance case that could
;; exist would pass. A camera that looks from the wrong place is a picture,
;; not a number. `projection` is `int` in the header and i32 here rather than
;; CameraProjection, because the layout is what a defstruct states and no
;; other field in this file is enum-typed; the enum below is for the value a
;; caller writes into it.
;; not a number.
;;
;; `projection` is `int` in the header and `CameraProjection` here, which is
;; the same four bytes with a face on it: a Flan defenum lowers to int32_t in
;; a struct field exactly as it does in a parameter, and the layout check
;; knows that, so it accepts an enum where the header says `int` and still
;; refuses an `f64` where the header says `float`. What it buys is at the
;; construction site — `.projection :perspective` resolves against the members
;; below and a typo is a compile error there, where an i32 field would have
;; taken any number at all.
(defenum CameraProjection [perspective 0 orthographic 1])
;; A member's value, as the i32 the field above is. Writing the field's type
;; as CameraProjection instead was tried first and is refused by the check
;; that compares this file against raylib.h — "field projection is
;; CameraProjection in the defstruct and i32 (int)" — which is the right
;; answer even though the two have the same representation: a defstruct is a
;; statement about a C layout and the C says `int`. And a bare `:perspective`
;; cannot be written into an i32 field either, because a keyword only resolves
;; where an enum type is expected. So the conversion is said out loud, once,
;; here: (camera-projection :perspective) is 0 and a typo is still an error.
(defn camera-projection [p CameraProjection] i32 (i32 p))
;; UpdateCamera's mode. :custom means it does nothing and the program moves
;; the camera itself.
(defenum CameraMode
[custom 0 free 1 orbital 2 first-person 3 third-person 4])
(defstruct Camera3D
[position Vector3 target Vector3 up Vector3 fovy f32 projection i32])
[position Vector3 target Vector3 up Vector3 fovy f32 projection CameraProjection])
;; The mode's built-in controls, applied to the camera in place — hence
;; (Ptr Camera3D), on the rule the Images section states: a call that mutates