@@ -191,13 +191,14 @@ const iterations = parseInt(process.argv.find((a, i) =>
191191 process . argv [ i - 1 ] === "--iterations"
192192) || "100" ) ;
193193
194+ // Default to the production ECHIDNA endpoint. Override with --echidna <url>.
194195const echidnaUrl = process . argv . find ( ( a , i ) =>
195196 process . argv [ i - 1 ] === "--echidna"
196- ) || null ;
197+ ) ?? "https://solve.nesy-prover.dev" ;
197198
198199console . log ( "=== ECHIDNA Prover Oracle: typed-wasm ===\n" ) ;
199200console . log ( `Iterations: ${ iterations } ` ) ;
200- console . log ( `ECHIDNA: ${ echidnaUrl || "offline (standalone mode)" } \n` ) ;
201+ console . log ( `ECHIDNA: ${ echidnaUrl } \n` ) ;
201202
202203// Property 1: Parse determinism — same input always gives same result.
203204console . log ( "Property 1: Parse determinism" ) ;
@@ -300,46 +301,179 @@ for (let i = 0; i < Math.min(iterations, 50); i++) {
300301}
301302
302303// ============================================================================
303- // ECHIDNA Submission (when running)
304+ // Layout Proof Static Checks
304305// ============================================================================
306+ //
307+ // These checks verify the safety invariants of the Idris2 layout proofs by
308+ // scanning the source files for banned patterns. They complement the
309+ // Idris2 typechecker (which enforces the same rules at compile time) and
310+ // provide ECHIDNA with proof-of-absence obligations that can be independently
311+ // verified.
312+
313+ import { readFileSync as _readFileSync } from "node:fs" ;
314+ import { resolve as _resolve } from "node:path" ;
315+ import { fileURLToPath as _fileURLToPath } from "node:url" ;
316+
317+ const _thisDir = _fileURLToPath ( import . meta. url ) ;
318+ const _layoutDir = _resolve ( _thisDir , ".." , ".." , ".." , "src" , "abi" , "layout" ) ;
319+
320+ /**
321+ * Scan an Idris2 source file for banned safety patterns.
322+ * Returns { file, banned: [{pattern, line, col}], clean: bool }.
323+ */
324+ function scanIdrisFile ( filename ) {
325+ const path = _resolve ( _layoutDir , filename ) ;
326+ let src ;
327+ try {
328+ src = _readFileSync ( path , "utf-8" ) ;
329+ } catch {
330+ return { file : filename , banned : [ ] , clean : false , error : "file not found" } ;
331+ }
305332
306- if ( echidnaUrl ) {
307- console . log ( `\nSubmitting ${ passed + failed } proof obligations to ECHIDNA...` ) ;
333+ // Patterns that must not appear in layout proofs.
334+ const BANNED = [
335+ / \b b e l i e v e _ m e \b / ,
336+ / \b a s s e r t _ t o t a l \b / ,
337+ / \b u n s a f e C o e r c e \b / ,
338+ / \b u n s a f e P e r f o r m I O \b / ,
339+ / \b p a r t i a l \b / , // %partial pragma
340+ ] ;
341+
342+ const banned = [ ] ;
343+ src . split ( "\n" ) . forEach ( ( line , i ) => {
344+ // Skip comment lines (the policy comment itself would match otherwise)
345+ if ( line . trimStart ( ) . startsWith ( "--" ) ) return ;
346+ for ( const pat of BANNED ) {
347+ if ( pat . test ( line ) ) {
348+ banned . push ( { pattern : pat . source , line : i + 1 , text : line . trim ( ) } ) ;
349+ }
350+ }
351+ } ) ;
308352
309- try {
310- const response = await fetch ( `${ echidnaUrl } /api/submit` , {
311- method : "POST" ,
312- headers : { "Content-Type" : "application/json" } ,
313- body : JSON . stringify ( {
314- source : "typed-wasm-echidna-harness" ,
315- obligations : [
316- {
317- name : "parse-determinism" ,
318- status : failed === 0 ? "proved" : "failed" ,
319- iterations,
320- pass_rate : passed / ( passed + failed ) ,
321- } ,
322- {
323- name : "parse-success-rate" ,
324- status : "info" ,
325- successes : parseSuccesses ,
326- failures : parseFailures ,
327- rate : parseSuccesses / ( parseSuccesses + parseFailures ) ,
328- } ,
329- ] ,
330- } ) ,
331- } ) ;
353+ return { file : filename , banned, clean : banned . length === 0 } ;
354+ }
332355
333- if ( response . ok ) {
334- console . log ( " Submitted to ECHIDNA." ) ;
335- } else {
336- console . log ( ` ECHIDNA responded ${ response . status } — results logged locally only.` ) ;
356+ /**
357+ * Verify that the recursive-type constructors (WHT_Var, WHT_Rec) exist in
358+ * Types.idr and that the list tail field uses WHT_Var 0 (not a placeholder).
359+ */
360+ function checkRecursiveTypes ( ) {
361+ const path = _resolve ( _layoutDir , "Types.idr" ) ;
362+ let src ;
363+ try { src = _readFileSync ( path , "utf-8" ) ; } catch { return { ok : false , reason : "Types.idr not found" } ; }
364+
365+ const hasVar = / \b W H T _ V a r \b / . test ( src ) ;
366+ const hasRec = / \b W H T _ R e c \b / . test ( src ) ;
367+ const hasAny = / \b W H T _ A n y \b / . test ( src ) ;
368+ if ( ! hasVar ) return { ok : false , reason : "WHT_Var missing from Types.idr" } ;
369+ if ( ! hasRec ) return { ok : false , reason : "WHT_Rec missing from Types.idr" } ;
370+ if ( ! hasAny ) return { ok : false , reason : "WHT_Any missing from Types.idr" } ;
371+ return { ok : true } ;
372+ }
373+
374+ function checkListLayout ( ) {
375+ const path = _resolve ( _layoutDir , "Stdlib.idr" ) ;
376+ let src ;
377+ try { src = _readFileSync ( path , "utf-8" ) ; } catch { return { ok : false , reason : "Stdlib.idr not found" } ; }
378+
379+ // Placeholder was WHT_Struct []; real type uses WHT_Var 0
380+ if ( / W H T _ S t r u c t \s * \[ \s * \] / . test ( src ) ) return { ok : false , reason : "placeholder WHT_Struct [] still present in Stdlib.idr" } ;
381+ if ( ! / W H T _ V a r \s + 0 / . test ( src ) ) return { ok : false , reason : "list tail WHT_Var 0 missing from Stdlib.idr" } ;
382+ if ( ! / W H T _ R e c / . test ( src ) ) return { ok : false , reason : "WHT_Rec missing from Stdlib.idr" } ;
383+ return { ok : true } ;
384+ }
385+
386+ console . log ( "--- Layout Proof Static Checks ---" ) ;
387+
388+ const layoutFiles = [ "Types.idr" , "ABI.idr" , "Stdlib.idr" ] ;
389+ const scanResults = layoutFiles . map ( scanIdrisFile ) ;
390+ const allClean = scanResults . every ( r => r . clean ) ;
391+
392+ for ( const r of scanResults ) {
393+ if ( r . clean ) {
394+ console . log ( ` ✓ ${ r . file } : no banned patterns` ) ;
395+ } else if ( r . error ) {
396+ console . log ( ` ? ${ r . file } : ${ r . error } ` ) ;
397+ } else {
398+ for ( const b of r . banned ) {
399+ console . log ( ` ✗ ${ r . file } :${ b . line } : banned pattern '${ b . pattern } ': ${ b . text } ` ) ;
337400 }
338- } catch ( e ) {
339- console . log ( ` Could not reach ECHIDNA at ${ echidnaUrl } : ${ e . message } ` ) ;
340401 }
341402}
342403
404+ const recCheck = checkRecursiveTypes ( ) ;
405+ const listCheck = checkListLayout ( ) ;
406+
407+ console . log ( recCheck . ok ? " ✓ Types.idr: WHT_Var, WHT_Rec, WHT_Any present"
408+ : ` ✗ recursive-types: ${ recCheck . reason } ` ) ;
409+ console . log ( listCheck . ok ? " ✓ Stdlib.idr: List uses WHT_Var 0 (no placeholder)"
410+ : ` ✗ list-layout: ${ listCheck . reason } ` ) ;
411+
412+ // ============================================================================
413+ // ECHIDNA Submission
414+ // ============================================================================
415+
416+ console . log ( `\nSubmitting ${ passed + failed + 5 } proof obligations to ECHIDNA at ${ echidnaUrl } ...` ) ;
417+
418+ try {
419+ const response = await fetch ( `${ echidnaUrl } /api/submit` , {
420+ method : "POST" ,
421+ headers : { "Content-Type" : "application/json" } ,
422+ body : JSON . stringify ( {
423+ source : "typed-wasm-echidna-harness" ,
424+ obligations : [
425+ // ── Parser property-based tests ────────────────────────────────────
426+ {
427+ name : "parse-determinism" ,
428+ status : failed === 0 ? "proved" : "failed" ,
429+ iterations,
430+ pass_rate : passed / ( passed + failed ) ,
431+ } ,
432+ {
433+ name : "parse-success-rate" ,
434+ status : "info" ,
435+ successes : parseSuccesses ,
436+ failures : parseFailures ,
437+ rate : parseSuccesses / ( parseSuccesses + parseFailures ) ,
438+ } ,
439+ // ── Layout proof static checks ──────────────────────────────────────
440+ // These are absence-of-banned-pattern obligations. The Idris2
441+ // typechecker enforces the same at compile time; these provide an
442+ // independent runtime-verifiable claim to ECHIDNA.
443+ {
444+ name : "layout-no-partial-proofs" ,
445+ status : allClean ? "proved" : "failed" ,
446+ detail : allClean
447+ ? "No believe_me, assert_total, or coercions in src/abi/layout/*.idr"
448+ : scanResults . flatMap ( r => r . banned ) . map ( b => `${ b . pattern } @L${ b . line } ` ) . join ( ", " ) ,
449+ } ,
450+ {
451+ name : "layout-recursive-types-present" ,
452+ status : recCheck . ok ? "proved" : "failed" ,
453+ detail : recCheck . ok
454+ ? "WHT_Var, WHT_Rec, WHT_Any all present in Layout.Types"
455+ : recCheck . reason ,
456+ } ,
457+ {
458+ name : "layout-list-no-placeholder" ,
459+ status : listCheck . ok ? "proved" : "failed" ,
460+ detail : listCheck . ok
461+ ? "List tail uses WHT_Var 0 under WHT_Rec — no WHT_Struct [] placeholder"
462+ : listCheck . reason ,
463+ } ,
464+ ] ,
465+ } ) ,
466+ } ) ;
467+
468+ if ( response . ok ) {
469+ console . log ( " Submitted to ECHIDNA." ) ;
470+ } else {
471+ console . log ( ` ECHIDNA responded ${ response . status } — results logged locally only.` ) ;
472+ }
473+ } catch ( e ) {
474+ console . log ( ` Could not reach ECHIDNA at ${ echidnaUrl } : ${ e . message } ` ) ;
475+ }
476+
343477// ============================================================================
344478// Fuzz Targets Summary
345479// ============================================================================
0 commit comments