From e08b3914fb1badefe095a62207e7b6a58bc9b47d Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 11 Sep 2026 20:31:55 +0700 Subject: [PATCH] Padding is a closed case, and a made-up name can still collide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- NEXT.md | 6 +++++- lib/shim.ml | 32 +++++++++++++++++++++++++++++--- test/test_acceptance.ml | 5 +++++ vendor/raylib/raylib.flan | 2 +- 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/NEXT.md b/NEXT.md index fafe3e1..c6c7453 100644 --- a/NEXT.md +++ b/NEXT.md @@ -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 diff --git a/lib/shim.ml b/lib/shim.ml index dfe6c99..4c8b8a0 100644 --- a/lib/shim.ml +++ b/lib/shim.ml @@ -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 ]) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index cb3f018..04b5ed8 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -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"; diff --git a/vendor/raylib/raylib.flan b/vendor/raylib/raylib.flan index a84dc2b..a2d4d9a 100644 --- a/vendor/raylib/raylib.flan +++ b/vendor/raylib/raylib.flan @@ -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