@@ -126,12 +126,8 @@ export const toyShape: ShapeSpec = {
126126 } ,
127127 Expr : {
128128 kind : 'pratt' ,
129- // keep yields bare leafScalars; toy golden needs Number|Identifier products.
130- atom : {
131- kind : 'custom' ,
132- fn : 'atom' ,
133- reason : 'Pratt atom products are Number|Identifier nodes; keep+leafValue only yields scalars' ,
134- } ,
129+ // Delegate to Atom choice so Number|Identifier nodes need no runtime custom.
130+ atom : { kind : 'rule' , name : 'Atom' } ,
135131 group : { kind : 'inline' } ,
136132 prefix : {
137133 kind : 'node' ,
@@ -159,18 +155,42 @@ export const toyShape: ShapeSpec = {
159155 ] ,
160156 } ,
161157 } ,
158+ // Three node arms (FIRST-overlap + true backtrack). Multi-alt single custom arm
159+ // remains proven via shape-parity's taggedCustomShape emit (altPath witness).
162160 Tagged : {
163161 kind : 'choice' ,
164162 arms : [
165163 {
166- name : 'Tagged ' ,
167- altIndices : [ 0 , 1 , 2 ] ,
164+ name : 'ColonTag ' ,
165+ altIndices : [ 0 ] ,
168166 shape : {
169- kind : 'custom' ,
170- fn : 'Tagged' ,
171- reason :
172- 'Three alts share FIRST "tag" and yield ColonTag|EqualsTag|BareTag; ' +
173- 'one arm must try alts sequentially and hand altPath to custom' ,
167+ kind : 'node' ,
168+ type : 'ColonTag' ,
169+ fields : [
170+ { name : 'name' , bind : { at : 0 } , typeHint : 'string' } ,
171+ { name : 'value' , bind : { at : 1 } , typeHint : 'ExprShape' } ,
172+ ] ,
173+ } ,
174+ } ,
175+ {
176+ name : 'EqualsTag' ,
177+ altIndices : [ 1 ] ,
178+ shape : {
179+ kind : 'node' ,
180+ type : 'EqualsTag' ,
181+ fields : [
182+ { name : 'name' , bind : { at : 0 } , typeHint : 'string' } ,
183+ { name : 'value' , bind : { at : 1 } , typeHint : 'ExprShape' } ,
184+ ] ,
185+ } ,
186+ } ,
187+ {
188+ name : 'BareTag' ,
189+ altIndices : [ 2 ] ,
190+ shape : {
191+ kind : 'node' ,
192+ type : 'BareTag' ,
193+ fields : [ { name : 'name' , bind : { at : 0 } , typeHint : 'string' } ] ,
174194 } ,
175195 } ,
176196 ] ,
@@ -230,7 +250,7 @@ export type ToyAstCustoms = Record<string, (ctx: ToyAstCustomCtx) => unknown>;
230250const I = ( name : string ) => ( { type : 'Identifier' , name } ) ;
231251const N = ( value : number ) => ( { type : 'Number' , value } ) ;
232252
233- /** Default customs so golden rows work without per-call overrides . */
253+ /** Optional customs for override/witness tests (default toy shape needs none) . */
234254export const toyCustoms : ToyAstCustoms = {
235255 atom : ( ctx ) => {
236256 const t = ctx . kids [ 0 ] ;
@@ -247,6 +267,30 @@ export const toyCustoms: ToyAstCustoms = {
247267 } ,
248268} ;
249269
270+ /** Multi-alt single custom arm — used by shape-parity altPath witness emit. */
271+ export const toyTaggedCustomShape : ShapeSpec = {
272+ ...toyShape ,
273+ rules : {
274+ ...toyShape . rules ,
275+ Tagged : {
276+ kind : 'choice' ,
277+ arms : [
278+ {
279+ name : 'Tagged' ,
280+ altIndices : [ 0 , 1 , 2 ] ,
281+ shape : {
282+ kind : 'custom' ,
283+ fn : 'Tagged' ,
284+ reason :
285+ 'Three alts share FIRST "tag" and yield ColonTag|EqualsTag|BareTag; ' +
286+ 'one arm must try alts sequentially and hand altPath to custom' ,
287+ } ,
288+ } ,
289+ ] ,
290+ } ,
291+ } ,
292+ } ;
293+
250294/** Proto2 golden (10), including custom-ctx row. */
251295export const toyGolden : { src : string ; expect : unknown ; customs ?: ToyAstCustoms } [ ] = [
252296 { src : 'bang !x;' , expect : { type : 'Program' , body : [ { type : 'BangOne' , arg : I ( 'x' ) } ] } } ,
@@ -282,7 +326,7 @@ export const toyGolden: { src: string; expect: unknown; customs?: ToyAstCustoms
282326 } ,
283327] ;
284328
285- /** Seeded corpus builder identical to proto2-harness (seed 0x5a2_2026 → 520 ). */
329+ /** Seeded corpus: SH2-0 base + SH2-0b multi-stmt / nested-group / adv-112 (seed → 800 ). */
286330export function buildToyCorpus ( seed = 0x5a2_2026 ) : { src : string ; source : string } [ ] {
287331 function rng32 ( s : number ) {
288332 return ( ) => {
@@ -305,11 +349,24 @@ export function buildToyCorpus(seed = 0x5a2_2026): { src: string; source: string
305349 if ( r < .56 ) return `${ expr ( depth + 1 ) } (${ expr ( depth + 1 ) } ,${ rng ( ) < .25 ? '' : expr ( depth + 1 ) } )` ;
306350 return `${ expr ( depth + 1 ) } ${ pick ( [ '+' , '-' , '*' , '/' ] ) } ${ expr ( depth + 1 ) } ` ;
307351 }
352+ /** Nested grouping for choice-arm Expr tails. */
353+ function groupedExpr ( depth = 1 ) : string {
354+ let e = atom ( ) ;
355+ for ( let i = 0 ; i < depth ; i ++ ) e = `(${ e } )` ;
356+ return e ;
357+ }
308358 function validItem ( ) : string {
309359 const r = rng ( ) ;
310360 if ( r < .18 ) return `bang ${ rng ( ) < .5 ? '!' : '!!' } ${ atom ( ) } ` ;
311361 if ( r < .40 ) {
312- const tail = pick ( [ `:${ expr ( ) } ` , `=${ expr ( ) } ` , '' ] ) ;
362+ const nest = 1 + Math . floor ( rng ( ) * 4 ) ;
363+ const tail = pick ( [
364+ `:${ expr ( ) } ` ,
365+ `=${ expr ( ) } ` ,
366+ `:${ groupedExpr ( nest ) } ` ,
367+ `=${ groupedExpr ( nest ) } ` ,
368+ '' ,
369+ ] ) ;
313370 return `tag ${ pick ( ids ) } ${ tail } ` ;
314371 }
315372 if ( r < .56 ) return `guard ${ pick ( ids ) } ${ rng ( ) < .5 ? `:${ pick ( ids ) } ` : '' } ` ;
@@ -320,6 +377,11 @@ export function buildToyCorpus(seed = 0x5a2_2026): { src: string; source: string
320377 }
321378 return expr ( ) ;
322379 }
380+ /** Always 2–5 statements (SH2-0b multi-stmt coverage). */
381+ function multiProgram ( ) : string {
382+ const n = 2 + Math . floor ( rng ( ) * 4 ) ;
383+ return Array . from ( { length : n } , ( ) => validItem ( ) + ';' ) . join ( rng ( ) < .3 ? '\n' : ' ' ) ;
384+ }
323385 function validProgram ( ) : string {
324386 const n = Math . floor ( rng ( ) * 5 ) ;
325387 return Array . from ( { length : n } , ( ) => validItem ( ) + ';' ) . join ( rng ( ) < .3 ? '\n' : ' ' ) ;
@@ -331,14 +393,34 @@ export function buildToyCorpus(seed = 0x5a2_2026): { src: string; source: string
331393 'tag x' , 'bang !!x' , 'unknown unknown;' , 'guard ;' , 'args(1 2);' ,
332394 ] ) ;
333395 }
396+ // Planner adversarial 112 cases (depth groups + fragment cross-product).
397+ const advCases : string [ ] = [ ] ;
398+ for ( let d = 1 ; d <= 40 ; d += 3 ) {
399+ advCases . push ( 'tag x' + '=' . repeat ( 1 ) + '(' . repeat ( d ) + '1' + ')' . repeat ( d ) + ';' ) ;
400+ }
401+ const frag = [
402+ 'tag x:1;' , 'tag y=2;' , 'tag z;' , 'bang!x;' , 'bang x;' , 'f(1,2,)( );' , 'a:(-b);' ,
403+ 'tag x:1' , 'tag x=;' , 'tag :1;' , 'bang !;' , 'f(,);' , 'f(1,,2);' , 'tag x:1;tag y=2;tag z;' ,
404+ ] ;
405+ for ( const a of frag ) for ( const b of frag . slice ( 0 , 7 ) ) advCases . push ( a + b ) ;
406+
334407 const anchors = [
335408 '' , 'bang !x;' , 'bang !!x;' , 'bang !!!x;' , 'tag x:1;' , 'tag x=1;' , 'tag x;' ,
336409 'tag x:;' , 'tag x=;' , 'guard bad;' , 'guard good;' , 'guard good:a;' ,
337410 'args();' , 'args(1,);' , 'args(1,2);' , 'f();' , 'f(1,);' , 'f(1)(2);' ,
338411 '1+2*3;' , '-x(1);' , 'tag t:f(1,2); bang !!7;' ,
412+ // SH2-0b: choice-arm nested groups + multi-stmt
413+ 'tag x=(1);' , 'tag x=((1));' , 'tag y=(((2)));' , 'tag z:(3);' , 'tag z:((a));' ,
414+ 'tag x:1;tag y=2;' , 'bang!x;tag z;' , 'tag x=(1);tag y=2;tag z;' ,
415+ 'tag a=(1);tag b=((2));tag c:(((3)));bang!x;guard ok;' ,
339416 ] . map ( ( src ) => ( { src, source : 'boundary' } ) ) ;
340- const corpus = [ ...anchors ] ;
341- while ( corpus . length < 360 ) corpus . push ( { src : validProgram ( ) , source : 'random-valid' } ) ;
342- while ( corpus . length < 520 ) corpus . push ( { src : invalidProgram ( ) , source : 'random-invalid' } ) ;
417+
418+ const corpus = [
419+ ...anchors ,
420+ ...advCases . map ( ( src ) => ( { src, source : 'adv-112' as const } ) ) ,
421+ ] ;
422+ while ( corpus . length < 560 ) corpus . push ( { src : multiProgram ( ) , source : 'multi-stmt' } ) ;
423+ while ( corpus . length < 680 ) corpus . push ( { src : validProgram ( ) , source : 'random-valid' } ) ;
424+ while ( corpus . length < 800 ) corpus . push ( { src : invalidProgram ( ) , source : 'random-invalid' } ) ;
343425 return corpus ;
344426}
0 commit comments