|
| 1 | +-- SPDX-License-Identifier: MPL-2.0 |
| 2 | +-- Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> |
| 3 | +-- |
| 4 | +||| Memory Layout Proofs for Iseriser |
| 5 | + |
| 6 | +module Iseriser.ABI.Layout |
| 7 | + |
| 8 | +import Iseriser.ABI.Types |
| 9 | +import Data.Vect |
| 10 | +import Data.So |
| 11 | +import Data.Nat |
| 12 | +import Decidable.Equality |
| 13 | + |
| 14 | +%default total |
| 15 | + |
| 16 | +public export |
| 17 | +paddingFor : (offset : Nat) -> (alignment : Nat) -> Nat |
| 18 | +paddingFor offset alignment = |
| 19 | + if offset `mod` alignment == 0 |
| 20 | + then 0 |
| 21 | + else minus alignment (offset `mod` alignment) |
| 22 | + |
| 23 | +public export |
| 24 | +alignUp : (size : Nat) -> (alignment : Nat) -> Nat |
| 25 | +alignUp size alignment = |
| 26 | + size + paddingFor size alignment |
| 27 | + |
| 28 | +||| Proof that alignment divides aligned size: `m = k * n`. |
| 29 | +public export |
| 30 | +data Divides : Nat -> Nat -> Type where |
| 31 | + DivideBy : (k : Nat) -> {n : Nat} -> {m : Nat} -> (m = k * n) -> Divides n m |
| 32 | + |
| 33 | +||| Sound decision procedure for divisibility. Returns a genuine |
| 34 | +||| `Divides n m` witness when `n` evenly divides `m`, otherwise Nothing. |
| 35 | +||| Division by zero is undecidable here and yields Nothing. |
| 36 | +public export |
| 37 | +decDivides : (n : Nat) -> (m : Nat) -> Maybe (Divides n m) |
| 38 | +decDivides Z _ = Nothing |
| 39 | +decDivides (S k) m = |
| 40 | + let q = m `div` (S k) in |
| 41 | + case decEq m (q * (S k)) of |
| 42 | + Yes prf => Just (DivideBy q prf) |
| 43 | + No _ => Nothing |
| 44 | + |
| 45 | +||| Sound divisibility check for an aligned size. The general theorem |
| 46 | +||| "alignUp size align is always divisible by align" needs div/mod lemmas |
| 47 | +||| from Data.Nat and is tracked as residual proof work; here we *decide* it |
| 48 | +||| via `decDivides`, which returns a genuine witness when it holds. For the |
| 49 | +||| concrete ABI layouts below, divisibility is proven outright (`DivideBy`). |
| 50 | +||| (Previously `alignUpCorrect … = DivideBy … Refl`, whose `Refl` cannot |
| 51 | +||| typecheck for symbolic inputs.) |
| 52 | +public export |
| 53 | +alignUpDivides : (size : Nat) -> (align : Nat) -> |
| 54 | + Maybe (Divides align (alignUp size align)) |
| 55 | +alignUpDivides size align = decDivides align (alignUp size align) |
| 56 | + |
| 57 | +public export |
| 58 | +record Field where |
| 59 | + constructor MkField |
| 60 | + name : String |
| 61 | + offset : Nat |
| 62 | + size : Nat |
| 63 | + alignment : Nat |
| 64 | + |
| 65 | +public export |
| 66 | +nextFieldOffset : Field -> Nat |
| 67 | +nextFieldOffset f = alignUp (f.offset + f.size) f.alignment |
| 68 | + |
| 69 | +public export |
| 70 | +record StructLayout where |
| 71 | + constructor MkStructLayout |
| 72 | + fields : Vect n Field |
| 73 | + totalSize : Nat |
| 74 | + alignment : Nat |
| 75 | + {auto 0 sizeCorrect : So (totalSize >= sum (map (\f => f.size) fields))} |
| 76 | + {auto 0 aligned : Divides alignment totalSize} |
| 77 | + |
| 78 | +public export |
| 79 | +calcStructSize : Vect k Field -> Nat -> Nat |
| 80 | +calcStructSize [] align = 0 |
| 81 | +calcStructSize (f :: fs) align = |
| 82 | + let lastOffset = foldl (\acc, field => nextFieldOffset field) f.offset fs |
| 83 | + lastSize = foldr (\field, _ => field.size) f.size fs |
| 84 | + in alignUp (lastOffset + lastSize) align |
| 85 | + |
| 86 | +||| C-compatible layout for the language model data passed through FFI. |
| 87 | +public export |
| 88 | +languageModelDataLayout : StructLayout |
| 89 | +languageModelDataLayout = |
| 90 | + MkStructLayout |
| 91 | + [ MkField "name_ptr" 0 8 8 |
| 92 | + , MkField "name_len" 8 4 4 |
| 93 | + , MkField "num_features" 12 4 4 |
| 94 | + , MkField "features_ptr" 16 8 8 |
| 95 | + , MkField "target" 24 4 4 |
| 96 | + , MkField "padding" 28 4 4 |
| 97 | + ] |
| 98 | + 32 |
| 99 | + 8 |
| 100 | + {sizeCorrect = Oh} |
| 101 | + {aligned = DivideBy 4 Refl} |
| 102 | + |
| 103 | +||| C-compatible layout for template data passed to the expansion engine. |
| 104 | +public export |
| 105 | +templateDataLayout : StructLayout |
| 106 | +templateDataLayout = |
| 107 | + MkStructLayout |
| 108 | + [ MkField "template_ptr" 0 8 8 |
| 109 | + , MkField "template_len" 8 4 4 |
| 110 | + , MkField "padding1" 12 4 4 |
| 111 | + , MkField "output_ptr" 16 8 8 |
| 112 | + , MkField "output_len" 24 4 4 |
| 113 | + , MkField "is_lang_spec" 28 4 4 |
| 114 | + ] |
| 115 | + 32 |
| 116 | + 8 |
| 117 | + {sizeCorrect = Oh} |
| 118 | + {aligned = DivideBy 4 Refl} |
| 119 | + |
| 120 | +||| C-compatible layout for the generation context handle. |
| 121 | +public export |
| 122 | +generationContextLayout : StructLayout |
| 123 | +generationContextLayout = |
| 124 | + MkStructLayout |
| 125 | + [ MkField "model_ptr" 0 8 8 |
| 126 | + , MkField "templates_ptr" 8 8 8 |
| 127 | + , MkField "num_templates" 16 4 4 |
| 128 | + , MkField "artifacts_count" 20 4 4 |
| 129 | + , MkField "output_dir_ptr" 24 8 8 |
| 130 | + , MkField "output_dir_len" 32 4 4 |
| 131 | + , MkField "initialized" 36 4 4 |
| 132 | + , MkField "error_code" 40 4 4 |
| 133 | + , MkField "padding" 44 4 4 |
| 134 | + ] |
| 135 | + 48 |
| 136 | + 8 |
| 137 | + {sizeCorrect = Oh} |
| 138 | + {aligned = DivideBy 6 Refl} |
| 139 | + |
| 140 | +||| Proof that every field offset in a layout is correctly aligned. |
| 141 | +public export |
| 142 | +data FieldsAligned : Vect k Field -> Type where |
| 143 | + NoFields : FieldsAligned [] |
| 144 | + ConsField : |
| 145 | + (f : Field) -> |
| 146 | + (rest : Vect k Field) -> |
| 147 | + Divides f.alignment f.offset -> |
| 148 | + FieldsAligned rest -> |
| 149 | + FieldsAligned (f :: rest) |
| 150 | + |
| 151 | +||| Decide field alignment for every field, building a real `FieldsAligned` |
| 152 | +||| witness from per-field divisibility proofs. |
| 153 | +public export |
| 154 | +decFieldsAligned : (fs : Vect k Field) -> Maybe (FieldsAligned fs) |
| 155 | +decFieldsAligned [] = Just NoFields |
| 156 | +decFieldsAligned (f :: fs) = |
| 157 | + case decDivides f.alignment f.offset of |
| 158 | + Nothing => Nothing |
| 159 | + Just dvd => case decFieldsAligned fs of |
| 160 | + Nothing => Nothing |
| 161 | + Just rest => Just (ConsField f fs dvd rest) |
| 162 | + |
| 163 | +||| Proof that a struct layout follows C ABI alignment rules. |
| 164 | +public export |
| 165 | +data CABICompliant : StructLayout -> Type where |
| 166 | + CABIOk : |
| 167 | + (layout : StructLayout) -> |
| 168 | + FieldsAligned layout.fields -> |
| 169 | + CABICompliant layout |
| 170 | + |
| 171 | +||| Verify a layout against the C ABI alignment rules, returning a genuine |
| 172 | +||| `CABICompliant` proof (built from real per-field divisibility witnesses) |
| 173 | +||| or an error when some field offset is misaligned. |
| 174 | +public export |
| 175 | +checkCABI : (layout : StructLayout) -> Either String (CABICompliant layout) |
| 176 | +checkCABI layout = |
| 177 | + case decFieldsAligned layout.fields of |
| 178 | + Just prf => Right (CABIOk layout prf) |
| 179 | + Nothing => Left "Field offsets are not correctly aligned for the C ABI" |
| 180 | + |
| 181 | +||| Verify that all iseriser layouts are C-ABI compliant. This now fails |
| 182 | +||| (Left) if any concrete layout is misaligned, rather than asserting it. |
| 183 | +public export |
| 184 | +verifyAllLayouts : Either String () |
| 185 | +verifyAllLayouts = do |
| 186 | + _ <- checkCABI languageModelDataLayout |
| 187 | + _ <- checkCABI templateDataLayout |
| 188 | + _ <- checkCABI generationContextLayout |
| 189 | + Right () |
| 190 | + |
| 191 | +||| All 64-bit platforms (Linux, Windows, MacOS, BSD) use 8-byte pointers and |
| 192 | +||| 4-byte ints, so the LanguageModelData layout is identical across them. |
| 193 | +public export |
| 194 | +verifyLanguageModelPortability : Either String () |
| 195 | +verifyLanguageModelPortability = Right () |
| 196 | + |
| 197 | +||| Look up a field's offset by name in a layout. |
| 198 | +public export |
| 199 | +fieldOffset : (layout : StructLayout) -> (fieldName : String) -> Maybe (Nat, Field) |
| 200 | +fieldOffset layout name = |
| 201 | + case findIndex (\f => f.name == name) layout.fields of |
| 202 | + Just idx => Just (finToNat idx, index idx layout.fields) |
| 203 | + Nothing => Nothing |
| 204 | + |
| 205 | +||| Decide whether a field lies within a struct's byte bounds, returning a |
| 206 | +||| genuine proof when `offset + size <= totalSize`. The previous signature |
| 207 | +||| asserted this for *every* field unconditionally, which is false (a field |
| 208 | +||| need not belong to the layout); this honest version decides it. |
| 209 | +public export |
| 210 | +offsetInBounds : (layout : StructLayout) -> (f : Field) -> |
| 211 | + Maybe (So (f.offset + f.size <= layout.totalSize)) |
| 212 | +offsetInBounds layout f = |
| 213 | + case choose (f.offset + f.size <= layout.totalSize) of |
| 214 | + Left ok => Just ok |
| 215 | + Right _ => Nothing |
0 commit comments