@@ -191,3 +191,286 @@ data SchemaMismatch : Type where
191191 AlignmentMismatch : (exportAlign : Nat ) -> (importAlign : Nat ) -> SchemaMismatch
192192 ||| Instance counts disagree.
193193 CountMismatch : (exportCount : Nat ) -> (importCount : Nat ) -> SchemaMismatch
194+
195+ -- ============================================================================
196+ -- A6 — Strengthened Propositional Layer (PROOF-NEEDS §P0.5, flagship)
197+ -- ============================================================================
198+ --
199+ -- The data declarations above (ExportedRegion, ImportedRegion,
200+ -- SchemaAgreement, CompatCertificate, LinkEdge, LinkGraph) are
201+ -- preserved in their original shape. They capture the *data* of a
202+ -- multi-module link but do not by themselves manipulate any witnesses
203+ -- — `SchemaAgreement` in particular only records module and region
204+ -- names, not the per-field correspondence that "compatibility" really
205+ -- depends on.
206+ --
207+ -- This section adds the propositional layer on top of those data
208+ -- types. The new types — `FieldMatches`, `SchemaSub`, `ModuleCompat`
209+ -- — are indexed and carry witnesses, and the theorems `compatRefl`,
210+ -- `compatTrans`, `noSpoofing` and `noTypeSpoofing` are statable only
211+ -- at this strengthened level. A worked Rust-exports-ReScript-imports
212+ -- example at the end of the file constructs a live certificate and
213+ -- applies the flagship theorem to it, so the proof is not vacuous.
214+
215+ ||| FieldMatches f s — witness that a field `f` (with exactly this
216+ ||| (name, type) pair) appears somewhere in schema `s`.
217+ |||
218+ ||| Stronger than a plain name-only membership proof (like `FieldIn`
219+ ||| in Region.idr): the entire Field constructor must match, so both
220+ ||| the name and the WasmType are transported by the witness. If
221+ ||| `FieldMatches f s` holds, then `s` really contains `f` itself —
222+ ||| not merely something with the same name but a different type.
223+ |||
224+ ||| Constructors are prefixed `FM` to avoid a clash with Region.idr's
225+ ||| `FieldHere` / `FieldThere`, which witness the weaker name-only
226+ ||| relation `FieldIn`.
227+ public export
228+ data FieldMatches : Field -> Schema -> Type where
229+ ||| `f` matches the schema's head field (both sides are literally the
230+ ||| same Field constructor application — identical name AND type).
231+ FMHere : FieldMatches f (f :: fs)
232+ ||| `f` matches somewhere deeper in the schema's tail.
233+ FMThere : FieldMatches f fs -> FieldMatches f (f' :: fs)
234+
235+ ||| SchemaSub imp exp — every field in `imp` appears in `exp` with
236+ ||| matching (name, type). This is the structural-subtype witness
237+ ||| that lets an importer consume a subset (or reordering) of an
238+ ||| exporter's schema.
239+ |||
240+ ||| Crucially this is NOT list equality: `imp` may omit fields,
241+ ||| duplicate references (though in practice a well-formed schema
242+ ||| won't), or occur in a different order than `exp`. The only
243+ ||| requirement is that each imported field is a legitimate export.
244+ |||
245+ ||| Constructors are prefixed `SS` to avoid a clash with Effects.idr's
246+ ||| `SubNil` / `SubCons`, which live in the parallel `EffectSubsumes`
247+ ||| universe.
248+ public export
249+ data SchemaSub : Schema -> Schema -> Type where
250+ ||| The empty schema is a subschema of any schema (vacuously).
251+ SSNil : SchemaSub [] expS
252+ ||| A non-empty `imp` is a subschema of `expS` if its head matches
253+ ||| somewhere in `expS` AND its tail is itself a subschema of `expS`.
254+ SSCons : FieldMatches f expS
255+ -> SchemaSub fs expS
256+ -> SchemaSub (f :: fs) expS
257+
258+ -- ---- Internal lemmas used to build the preorder ----
259+
260+ ||| Weaken a SchemaSub by adding a new field to the *front* of the
261+ ||| exporter side. Every field that previously matched inside `exp`
262+ ||| still matches inside `f' :: exp`, just one step deeper. Used by
263+ ||| `schemaSubRefl` below.
264+ private
265+ weakenSchemaSub : SchemaSub xs expS -> SchemaSub xs (f' :: expS)
266+ weakenSchemaSub SSNil = SSNil
267+ weakenSchemaSub (SSCons fm tl) = SSCons (FMThere fm) (weakenSchemaSub tl)
268+
269+ ||| If `f` matches in `y` and `y` is a subschema of `z`, then `f`
270+ ||| matches in `z`. This is pointwise transitivity for FieldMatches,
271+ ||| and is the work-horse for both `schemaSubTrans` and `noSpoofing`.
272+ private
273+ fieldMatchesLift : FieldMatches f y -> SchemaSub y z -> FieldMatches f z
274+ fieldMatchesLift FMHere (SSCons fm _ ) = fm
275+ fieldMatchesLift (FMThere deeper) (SSCons _ tl) = fieldMatchesLift deeper tl
276+
277+ -- ---- SchemaSub preorder (PROOF-NEEDS §P0.5 — reflexivity + transitivity) ----
278+
279+ ||| SchemaSub is reflexive: every schema is a subschema of itself.
280+ |||
281+ ||| The proof proceeds by structural induction on the schema. The
282+ ||| head matches via `FMHere`, and the reflexivity of the tail is
283+ ||| weakened by `weakenSchemaSub` to account for the one-step deeper
284+ ||| context on the exporter side.
285+ public export
286+ schemaSubRefl : (s : Schema) -> SchemaSub s s
287+ schemaSubRefl [] = SSNil
288+ schemaSubRefl (f :: rest) =
289+ SSCons FMHere (weakenSchemaSub (schemaSubRefl rest))
290+
291+ ||| SchemaSub is transitive. If `imp` ⊆ `mid` and `mid` ⊆ `exp`,
292+ ||| then `imp` ⊆ `exp` — each field witness in `imp → mid` is lifted
293+ ||| through `mid → exp` to produce a field witness in `imp → exp`.
294+ public export
295+ schemaSubTrans : SchemaSub x y -> SchemaSub y z -> SchemaSub x z
296+ schemaSubTrans SSNil _ = SSNil
297+ schemaSubTrans (SSCons fm tl) yz =
298+ SSCons (fieldMatchesLift fm yz) (schemaSubTrans tl yz)
299+
300+ -- ============================================================================
301+ -- Module-Indexed Compatibility (the core relation for the flagship theorem)
302+ -- ============================================================================
303+
304+ ||| `ModuleCompat from to imp exp` — Module `from` is compatible with
305+ ||| importing schema `imp` from Module `to` which actually exports
306+ ||| schema `exp`. Compatibility is witnessed by a SchemaSub proof.
307+ |||
308+ ||| This is the `Compat m1 m2` relation from PROOF-NEEDS §P0.5,
309+ ||| strengthened with the two schemas so that structural claims can be
310+ ||| transported along the certificate. The existing `CompatCertificate`
311+ ||| above is preserved as a weaker data-only form; `ModuleCompat` is
312+ ||| the propositional form over which the flagship theorem is stated.
313+ |||
314+ ||| The module identifiers are carried purely as type-level tags —
315+ ||| they appear in error messages and diagnostics but the proof
316+ ||| content lives entirely in the `SchemaSub` witness.
317+ public export
318+ data ModuleCompat : (from : ModuleId)
319+ -> (to : ModuleId)
320+ -> (imp : Schema)
321+ -> (expS : Schema)
322+ -> Type where
323+ MkModuleCompat : SchemaSub imp expS -> ModuleCompat from to imp expS
324+
325+ ||| ModuleCompat is reflexive: any module is trivially compatible
326+ ||| with itself when the imported schema IS the exported schema.
327+ |||
328+ ||| PROOF-NEEDS §P0.5 spells this out as
329+ ||| `compatRefl : (m : ModuleId) -> Compat m m`
330+ ||| — the schema-indexed form here additionally records *which*
331+ ||| schema is being transported.
332+ public export
333+ compatRefl : (m : ModuleId) -> (s : Schema) -> ModuleCompat m m s s
334+ compatRefl _ s = MkModuleCompat (schemaSubRefl s)
335+
336+ ||| ModuleCompat is transitive: if m1 imports `s1` from m2 (which
337+ ||| actually exports `s2`), and m2 imports `s2` from m3 (which
338+ ||| actually exports `s3`), then m1 imports `s1` from m3 (which
339+ ||| exports `s3`). The SchemaSub witnesses chain via
340+ ||| `schemaSubTrans`.
341+ public export
342+ compatTrans : ModuleCompat m1 m2 s1 s2
343+ -> ModuleCompat m2 m3 s2 s3
344+ -> ModuleCompat m1 m3 s1 s3
345+ compatTrans (MkModuleCompat sub12) (MkModuleCompat sub23) =
346+ MkModuleCompat (schemaSubTrans sub12 sub23)
347+
348+ -- ============================================================================
349+ -- The Flagship: No-Spoofing Theorem (PROOF-NEEDS §P0.5)
350+ -- ============================================================================
351+
352+ ||| No-spoofing theorem — given a module-compatibility certificate,
353+ ||| every field the importer references is a bona-fide field of the
354+ ||| exporter with matching (name, type). There is no way to import
355+ ||| a field with a name/type combination that the exporter did not
356+ ||| actually export.
357+ |||
358+ ||| PROOF-NEEDS §P0.5 calls this "the actual multi-module memory
359+ ||| safety invariant — the paper's killer feature". It is the
360+ ||| formal counterpart of the informal claim made throughout
361+ ||| typed-wasm's docs: "cross-module memory safety across language
362+ ||| boundaries".
363+ |||
364+ ||| Reading: if you can produce a `ModuleCompat from to imp exp`
365+ ||| certificate and a `FieldMatches f imp` witness, then the proof
366+ ||| term constructively transports the import evidence into
367+ ||| `FieldMatches f exp` — a witness that `f` really is in `exp`.
368+ |||
369+ ||| The theorem is NOT vacuous. The certificate's `SchemaSub`
370+ ||| witness is unpacked field-by-field via `fieldMatchesLift`
371+ ||| (which does the recursive search). The proof cannot be
372+ ||| constructed from thin air — it requires an actual subschema
373+ ||| relation.
374+ public export
375+ noSpoofing : ModuleCompat from to imp expS
376+ -> (f : Field)
377+ -> FieldMatches f imp
378+ -> FieldMatches f expS
379+ noSpoofing (MkModuleCompat sub) _ fm = fieldMatchesLift fm sub
380+
381+ ||| Type-preservation corollary. If the importer sees a field
382+ ||| `(name, ty)`, the exporter has exactly that `(name, ty)` —
383+ ||| the WasmType is transported unchanged.
384+ |||
385+ ||| This is a direct specialisation of `noSpoofing` at a known
386+ ||| `(name, ty)` pair, but stating it separately makes the
387+ ||| type-safety guarantee explicit at the API level. A caller
388+ ||| worried about type drift can quote this lemma directly
389+ ||| without unpacking the `FieldMatches` witness.
390+ public export
391+ noTypeSpoofing : ModuleCompat from to imp expS
392+ -> (name : String)
393+ -> (ty : WasmType)
394+ -> FieldMatches (MkField name ty) imp
395+ -> FieldMatches (MkField name ty) expS
396+ noTypeSpoofing cert name ty fm = noSpoofing cert (MkField name ty) fm
397+
398+ -- ============================================================================
399+ -- Worked Example: Rust exports, ReScript imports (strict subset)
400+ -- ============================================================================
401+ --
402+ -- PROOF-NEEDS §P0.5 recommends writing a small example multi-module
403+ -- program to sanity-check the statement before proving it. This
404+ -- namespace constructs a concrete Rust-exports / ReScript-imports
405+ -- link: the backend exports a 4-field `UserProfile` region and the
406+ -- frontend imports just the first two fields. The compatibility
407+ -- certificate is then built explicitly and `noSpoofing` is applied
408+ -- to it, demonstrating that the theorem has real computational
409+ -- content (not just an unused abstract witness).
410+
411+ namespace Example
412+ ||| Rust backend module identifier.
413+ public export
414+ rustBackend : ModuleId
415+ rustBackend = MkModuleId " rust_backend"
416+
417+ ||| ReScript frontend module identifier.
418+ public export
419+ rescriptFrontend : ModuleId
420+ rescriptFrontend = MkModuleId " rescript_frontend"
421+
422+ ||| Rust exports a full user profile: id, age, score, banned.
423+ public export
424+ rustExportSchema : Schema
425+ rustExportSchema =
426+ [ MkField " id" U64
427+ , MkField " age" U8
428+ , MkField " score" F32
429+ , MkField " banned" WBool
430+ ]
431+
432+ ||| ReScript only needs the first two fields to render the UI.
433+ ||| Note this is a strict subset — `score` and `banned` are
434+ ||| hidden from the frontend.
435+ public export
436+ rescriptImportSchema : Schema
437+ rescriptImportSchema =
438+ [ MkField " id" U64
439+ , MkField " age" U8
440+ ]
441+
442+ ||| Each imported field is witnessed by an explicit FieldMatches
443+ ||| proof. `id` is at the head of the export; `age` is the
444+ ||| second element so its proof is `FMThere FMHere`.
445+ |||
446+ ||| The fully-qualified schema names (`Example.rescriptImportSchema`
447+ ||| etc.) are required because Idris2 treats lowercase identifiers
448+ ||| in type signatures as implicit binders.
449+ public export
450+ exampleSchemaSub : SchemaSub Example.rescriptImportSchema
451+ Example . rustExportSchema
452+ exampleSchemaSub =
453+ SSCons FMHere -- id at head
454+ (SSCons (FMThere FMHere ) SSNil ) -- age at position 1
455+
456+ ||| The live compatibility certificate. Its construction requires
457+ ||| the SchemaSub witness above; without it, the certificate
458+ ||| cannot be built.
459+ public export
460+ exampleCompat : ModuleCompat Example.rescriptFrontend
461+ Example . rustBackend
462+ Example . rescriptImportSchema
463+ Example . rustExportSchema
464+ exampleCompat = MkModuleCompat exampleSchemaSub
465+
466+ ||| Apply the no-spoofing theorem: starting from the import-side
467+ ||| witness `FMHere : FieldMatches (MkField "id" U64)
468+ ||| rescriptImportSchema`, the theorem produces an export-side
469+ ||| witness that `(MkField "id" U64)` really lives in
470+ ||| `rustExportSchema`. The theorem carries real content — it
471+ ||| walks the SchemaSub to locate the export-side position.
472+ public export
473+ exampleNoSpoofing : FieldMatches (MkField "id" U64)
474+ Example . rustExportSchema
475+ exampleNoSpoofing =
476+ noSpoofing exampleCompat (MkField " id" U64 ) FMHere
0 commit comments