The config is tested by what it is for, not by its parser alone

Exclusion says why rather than going quiet, a pattern matches by prefix, an
override changes the Flan face and leaves the C symbol verbatim, and a rename
dissolves a kebab collision instead of leaving both halves refused — that last
one is why the kebab rule is consulted in exactly one place.

The file itself too: a line that is neither directive is an error, because a
typo in a name override would otherwise land a binding under the wrong name.
This commit is contained in:
Joseph Ferano 2026-09-13 08:09:59 +07:00
parent 85ef56f657
commit ecfe180d2e

View File

@ -1378,6 +1378,105 @@ let () =
check "nor is the other half of the collision"
(not (List.exists (fun l -> contains l "\"spin2d\"") produced));
(* ── The config beside the header (Cimport.read_config) ────────── *)
(* Why there is a config at all: the generated declarations are committed, so
a hand-edit to them is destroyed by the next regeneration and the edit has
to live somewhere regeneration reads instead. These are the two things it
can say. *)
let with_config config =
let taken = Hashtbl.create 16 in
List.iter
(fun d ->
match Ast.declared_name d with
| Some n -> Hashtbl.replace taken n ()
| None -> ())
fixture_ds;
let known_structs =
List.filter_map
(fun (d : Ast.decl) ->
match d.Ast.d with Ast.Defstruct (n, _) -> Some n | _ -> None)
fixture_ds
and known_enums =
List.filter_map
(fun (d : Ast.decl) ->
match d.Ast.d with Ast.Defenum (n, _) -> Some n | _ -> None)
fixture_ds
in
let i, _, _ =
Cimport.header ~loc:Loc.unknown ~header:"headers/sample.h" ~flags:[]
~known_structs ~known_enums ~taken ~bound_syms:[] ~config
in
(List.map Cimport.decl_source i.Cimport.decls, i.Cimport.hidden)
in
let lines, hidden =
with_config { Cimport.excludes = [ "set_seed" ]; renames = [] }
in
check "an excluded symbol is not generated"
(not (List.exists (fun l -> contains l "\"set_seed\"") lines));
(* Not silently absent. "there is no such binding" and "the package decided
against this binding" are different answers and a reader gets the second,
which is what [hidden] is for. *)
check "and it says it was excluded rather than going quiet"
(List.exists
(fun (n, why) -> n = "set-seed" && contains why "excluded by the package")
hidden);
let lines, _ =
with_config { Cimport.excludes = [ "add_*" ]; renames = [] }
in
check "an exclude pattern matches by prefix"
(not (List.exists (fun l -> contains l "\"add_ints\"") lines));
check "and leaves everything it does not match"
(List.exists (fun l -> contains l "\"set_seed\"") lines);
let lines, _ =
with_config
{ Cimport.excludes = []; renames = [ ("set_seed", "seed!") ] }
in
(* The C symbol is kept verbatim, so an override changes the Flan face and
nothing else which is what makes it safe to spell a predicate the way
Lisp spells one. *)
check "a name override is the Flan name, and the C symbol is untouched"
(List.mem "(declare-c seed! [seed u32] \"set_seed\")" lines);
(* The collision above is refused because neither Spin2D nor spin2d may take
[spin-2d] by an accident of header order. Naming one of them is the way
out, and it is the reason the kebab rule is consulted in exactly one
place: the groups are computed on the name a function will really take,
so the rename dissolves the group rather than leaving both refused. *)
let lines, hidden =
with_config
{ Cimport.excludes = []; renames = [ ("Spin2D", "spin-2d-upper") ] }
in
check "a rename resolves a collision for both halves"
(List.exists (fun l -> contains l "\"Spin2D\"") lines
&& List.exists (fun l -> contains l "\"spin2d\"") lines);
check "and the collision is no longer refused"
(not (List.mem_assoc "spin-2d" hidden));
(* The file, since a typo in it would otherwise show up as a binding under
the wrong name. A line that is neither directive is an error rather than a
line quietly skipped. *)
let config_file text =
let f = Filename.temp_file "flan-bindings" "" in
let ch = open_out f in
output_string ch text;
close_out ch;
f
in
let c = Cimport.read_config (config_file "# a comment\n\nexclude Mem*\nname IsWindowReady window-ready?\n") in
check "read_config reads an exclude" (c.Cimport.excludes = [ "Mem*" ]);
check "read_config reads a name override"
(c.Cimport.renames = [ ("IsWindowReady", "window-ready?") ]);
check "a missing config is no exclusions and no overrides"
(Cimport.read_config "no-such-bindings-file" = Cimport.no_config);
check "a line that is neither directive is refused"
(match Cimport.read_config (config_file "rename Foo bar\n") with
| _ -> false
| exception Loc.Error (_, m) -> contains m "and this is neither");
(* A refused name is a name that exists and cannot be had — Zig's failDecl,
which Load.refuse_hidden already implements for main. Nothing may be in
both lists, or asking for a name that works would report that it does