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:
parent
781295e862
commit
9223c9002a
70
BUILT.md
70
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
|
||||
|
||||
23
NEXT.md
23
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
|
||||
|
||||
56
bin/main.ml
56
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)
|
||||
|
||||
|
||||
@ -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}))
|
||||
|
||||
|
||||
344
lib/cimport.ml
344
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 <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 }
|
||||
|
||||
43
lib/load.ml
43
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
|
||||
|
||||
@ -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 --- */
|
||||
|
||||
@ -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
|
||||
|
||||
49
vendor/raylib/bindings
vendored
49
vendor/raylib/bindings
vendored
@ -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.
|
||||
|
||||
27
vendor/raylib/raylib.flan
vendored
27
vendor/raylib/raylib.flan
vendored
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user