Padding is a closed case, and a made-up name can still collide

Two gaps in what was claimed. The first is prose: "the typedef follows the
defstruct" answers field order and field types but says nothing about
padding, which reads like the remaining hazard. It is not one. Every field
type the generator admits has the same layout under LLVM as under C, and
emit.ml writes no datalayout, so clang applies the target's own rules to
both halves; everything where they could diverge — an array, a slice, an
Option, a map, a union — is already refused at the field.

The second is real. The flattened declaration's name is invented by
appending -c, so a hand-written foo-c beside (declare-c foo ...) came out
as the checker complaining that a name not in the file was declared twice.
Refused now where it happens, naming both and saying to rename one.
This commit is contained in:
Joseph Ferano 2026-09-11 20:31:55 +07:00
parent dbf5748c56
commit e08b3914fb
4 changed files with 40 additions and 5 deletions

View File

@ -271,7 +271,11 @@ type-checks the wrapper against the generated prototype. Trusted: that the
signature is the function's real signature — no header is read, deliberately,
so nothing can check either. A `_Static_assert` on `sizeof`/`offsetof` was
considered and rejected as circular: both sides would come from the same field
list.
list. Padding is not a separate hazard: for every field type the generator
admits — the machine integers, the two floats, `bool`, a pointer and a nested
struct — LLVM's layout is C's, and `emit.ml` writes no datalayout, so clang
applies the target's rules to both halves. Everything where they could diverge
is refused at the field.
**One thing got sharper and should be said plainly:** the prototype is now
generated *from the declaration*, so a scalar's width carries ABI weight it did

View File

@ -40,6 +40,14 @@
typedef permutes with it. And clang type-checks the wrapper against the
[extern] prototype, so the flattening cannot disagree with the prototype.
Padding is not a separate hazard, which is worth saying because it reads
like one. For every field type this generator admits the machine
integers, the two floats, [bool], a pointer and a nested struct LLVM's
struct layout is C's, and [emit.ml] writes no datalayout, so clang applies
the target's own rules to both halves and they land in the same place.
Everything where the two could diverge a fixed array, a slice, an
[Option], a map, a union is refused at the field, by name.
Trusted: that the [defstruct] describes the library's real struct, and that
the [declare-c] signature is the function's real signature. No library
header is read deliberately, so a build needs the shared library and not
@ -520,7 +528,7 @@ let flan_wrapper (fn : Ast.fn) (s : shim) raw : Ast.decl_kind =
(* ── Expansion ──────────────────────────────────────────────────────── *)
let one env (fn : Ast.fn) csym loc =
let one env ~taken (fn : Ast.fn) csym loc =
let needed = ref [] in
let sargs =
List.map
@ -559,7 +567,15 @@ let one env (fn : Ast.fn) csym loc =
let decls =
if needs_flan then
let raw = raw_name fn.Ast.name in
[ Ast.Declare (flattened fn s raw, s.swrap); flan_wrapper fn s raw ]
(* The flattened declaration's name is made up, so it can collide with
one somebody wrote. Refused here, naming both, rather than arriving as
the checker's "declared twice" about a name not in the file. *)
if Hashtbl.mem taken raw then
fail loc
"the declare-c of %s needs the name %s for the declaration it \
generates, and %s is declared already rename one of them"
fn.Ast.name raw raw
else [ Ast.Declare (flattened fn s raw, s.swrap); flan_wrapper fn s raw ]
else [ Ast.Declare (flattened fn s fn.Ast.name, s.swrap) ]
in
(decls, s, !needed)
@ -569,6 +585,16 @@ let one env (fn : Ast.fn) csym loc =
no C compile. *)
let expand (decls : Ast.decl list) : Ast.decl list * string option =
let env = scan decls in
(* The flattened declaration's name is made up, so it can collide with one
somebody wrote. Refused here, naming both, rather than surfacing as the
checker's "declared twice" about a name that is not in the file. *)
let taken = Hashtbl.create 64 in
List.iter
(fun (d : Ast.decl) ->
match Ast.declared_name d with
| Some n -> Hashtbl.replace taken n d.Ast.dloc
| None -> ())
decls;
let shims = ref [] in
let needed = ref [] in
let out =
@ -576,7 +602,7 @@ let expand (decls : Ast.decl list) : Ast.decl list * string option =
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.DeclareC (fn, csym) ->
let ds, s, n = one env fn csym d.Ast.dloc in
let ds, s, n = one env ~taken fn csym d.Ast.dloc in
shims := !shims @ [ s ];
List.iter
(fun x -> if not (List.mem x !needed) then needed := !needed @ [ x ])

View File

@ -680,6 +680,11 @@ let () =
shim_refuses "declare-c: a field C cannot hold"
"(defstruct S [xs [i32]])\n(declare-c f [s S] \"F\")"
"field xs of S is a slice";
shim_refuses "declare-c: the generated name is already taken"
(v2
^ "(defn mid-c [a (Ptr Vector2) out (Ptr Vector2)])\n\
(declare-c mid [a Vector2] Vector2 \"Mid\")")
"needs the name mid-c for the declaration it generates";
shim_refuses "declare-c: two Flan names for one C function"
"(declare-c a [] \"Same\")\n(declare-c b [] \"Same\")"
"one declare-c per C function";

View File

@ -391,7 +391,7 @@
[center Vector2 radius f32 color Color]
"DrawCircleLinesV")
;; Two radii, horizontal then vertical. Equal radii is a circle, so a wrapper
;; Two radii, horizontal then vertical. Equal radii is a circle, so a binding
;; that exchanged them would be invisible unless they differ — which is why
;; sand.flan's ellipse is deliberately wider than it is tall.
(declare-c draw-ellipse