From 9223c9002a004d59e51aa9b0251d3440c244e668 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sun, 13 Sep 2026 14:11:35 +0700 Subject: [PATCH] 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. --- BUILT.md | 70 +++++++ NEXT.md | 23 +-- bin/main.ml | 56 +++++- examples/core-world-screen.flan | 10 +- lib/cimport.ml | 344 +++++++++++++++++++++++++++++--- lib/load.ml | 43 +++- test/headers/sample.h | 25 +++ test/test_flan.ml | 195 +++++++++++++++++- vendor/raylib/bindings | 49 +++++ vendor/raylib/raylib.flan | 27 +-- 10 files changed, 757 insertions(+), 85 deletions(-) diff --git a/BUILT.md b/BUILT.md index 421fd9d..1b98fb9 100644 --- a/BUILT.md +++ b/BUILT.md @@ -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 diff --git a/NEXT.md b/NEXT.md index b0afb1a..bbd426b 100644 --- a/NEXT.md +++ b/NEXT.md @@ -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 diff --git a/bin/main.ml b/bin/main.ml index 812482d..25578fa 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -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) diff --git a/examples/core-world-screen.flan b/examples/core-world-screen.flan index 6c30f40..f0aab54 100644 --- a/examples/core-world-screen.flan +++ b/examples/core-world-screen.flan @@ -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})) diff --git a/lib/cimport.ml b/lib/cimport.ml index 2f99f6e..080340b 100644 --- a/lib/cimport.ml +++ b/lib/cimport.ml @@ -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 ` or `name `, and this is neither: %s" t + "a line here is `exclude `, `name `, `enum `, `const \ + ` or `constant `, \ + 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 \ + `, 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 } diff --git a/lib/load.ml b/lib/load.ml index fe52b52..96af62c 100644 --- a/lib/load.ml +++ b/lib/load.ml @@ -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 diff --git a/test/headers/sample.h b/test/headers/sample.h index de56002..a006ebb 100644 --- a/test/headers/sample.h +++ b/test/headers/sample.h @@ -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 --- */ diff --git a/test/test_flan.ml b/test/test_flan.ml index 0bb7a3a..96a5b50 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -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 diff --git a/vendor/raylib/bindings b/vendor/raylib/bindings index 2fad847..8d01bb6 100644 --- a/vendor/raylib/bindings +++ b/vendor/raylib/bindings @@ -114,3 +114,52 @@ exclude DrawGrid exclude SetExitKey exclude UpdateCamera exclude GetWorldToScreen + +# ── What the package's constants are called in C ──────────────────── +# +# enum every member of that defenum +# const every defconst whose name starts so +# constant 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. diff --git a/vendor/raylib/raylib.flan b/vendor/raylib/raylib.flan index e07d2a1..47eae98 100644 --- a/vendor/raylib/raylib.flan +++ b/vendor/raylib/raylib.flan @@ -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