Skip to content

Commit e8ab178

Browse files
SH3-1b: fix shape parseAst over-suppressing prec-binary ops.
exclude/suppress only gates literal-headed LEDs; shape Pratt binary wrongly consulted suppressCur, rejecting noplus 1*2 while CST/interp accepted. Drop the binary-path check in TS+Rust; extend toy corpus. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent deecd26 commit e8ab178

5 files changed

Lines changed: 60 additions & 11 deletions

File tree

src/target-rust.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3456,10 +3456,9 @@ function emitRustPrattMethod(
34563456
const binaryArms = rule.binary.map((b) =>
34573457
`Some(${lidOf(ids, b.op)}) => (${b.lbp}, ${b.rbp}),`,
34583458
).join('\n ');
3459+
// Binary `$ op $` must NOT consult suppress_cur — exclude only disables
3460+
// literal-headed LEDs (≡ CST / interpreter / TS shape).
34593461
binaryBody = `{
3460-
if let Some(t) = self.toks.get(self.pos) {
3461-
if self.suppress_cur.contains(&t.lid) { break; }
3462-
}
34633462
let (_lbp, _rbp) = match self.peek_lid() {
34643463
${binaryArms}
34653464
_ => break,

src/target-ts.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3429,12 +3429,13 @@ function emitAstPrattRule(r: PrattRule, sir: ShapeIRRule, ids: LexIdPlan, shapeI
34293429
}
34303430
}
34313431

3432+
// Binary `$ op $` path must NOT consult `_suppressCur` — exclude/suppress only
3433+
// disables literal-headed LEDs (e.g. `[$, 'in', $]`), matching CST / interpreter.
34323434
let binaryCode = ' break;';
34333435
if (binarySlot) {
34343436
if (binarySlot.kind === 'node') {
34353437
const binaryNode = binarySlot as NodeShape;
3436-
binaryCode = ` if (_suppressCur !== null && _suppressCur.has(t.lid)) break;
3437-
const info = ${r.name}_BIN[t.lid];
3438+
binaryCode = ` const info = ${r.name}_BIN[t.lid];
34383439
if (info === undefined || info.lbp <= minBp) break;
34393440
const ledSave = pos;
34403441
const opText = _src.slice(t.off, t.end);
@@ -3448,8 +3449,7 @@ function emitAstPrattRule(r: PrattRule, sir: ShapeIRRule, ids: LexIdPlan, shapeI
34483449
leftOff = spOff;
34493450
leftEnd = end;`;
34503451
} else if (binarySlot.kind === 'custom') {
3451-
binaryCode = ` if (_suppressCur !== null && _suppressCur.has(t.lid)) break;
3452-
const info = ${r.name}_BIN[t.lid];
3452+
binaryCode = ` const info = ${r.name}_BIN[t.lid];
34533453
if (info === undefined || info.lbp <= minBp) break;
34543454
const ledSave = pos;
34553455
const opText = _src.slice(t.off, t.end);
@@ -3459,8 +3459,7 @@ function emitAstPrattRule(r: PrattRule, sir: ShapeIRRule, ids: LexIdPlan, shapeI
34593459
left = ${customFinish((binarySlot as CustomShape).fn, '[right]', 'ledSave', '[]', 'left', 'opText')} as ${retType};
34603460
leftEnd = pos > 0 ? toks[pos - 1]!.end : leftEnd;`;
34613461
} else {
3462-
binaryCode = ` if (_suppressCur !== null && _suppressCur.has(t.lid)) break;
3463-
const info = ${r.name}_BIN[t.lid];
3462+
binaryCode = ` const info = ${r.name}_BIN[t.lid];
34643463
if (info === undefined || info.lbp <= minBp) break;
34653464
const ledSave = pos;
34663465
const opText = _src.slice(t.off, t.end);

test/fixtures/shape-toy.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,16 @@ export function buildToyCorpus(seed = 0x5a2_2026): { src: string; source: string
582582
}
583583
if (r < .50) return `txn ${pick(ids)}${rng() < .6 ? `:${pick(ids)}` : ''}${pick(['!', '.', '?'])}`;
584584
if (r < .58) return `line ${pick(ids)} ${pick(ids)}`;
585-
if (r < .66) return `noplus ${atom()}+${atom()}`;
585+
// SH3-1b: suppress only disables literal LEDs — binary `*`/`/` must still parse
586+
// under exclude('*', Expr). Mix ops + grouping so the arm is not `+`-only.
587+
if (r < .66) {
588+
const op = pick(['+', '-', '*', '/', '+', '*']);
589+
const form = rng();
590+
if (form < .35) return `noplus ${atom()}${op}${atom()}`;
591+
if (form < .55) return `noplus ${atom()}${op}${atom()}${op}${atom()}`;
592+
if (form < .75) return `noplus (${atom()}${op}${atom()})`;
593+
return `noplus ${atom()}${op}(${atom()}${op}${atom()})`;
594+
}
586595
if (r < .75) {
587596
const n = 2 + Math.floor(rng() * 5);
588597
return `repeat ${Array.from({ length: n }, atom).join(' ')}`;
@@ -591,9 +600,16 @@ export function buildToyCorpus(seed = 0x5a2_2026): { src: string; source: string
591600
const n = Math.floor(rng() * 5);
592601
return `maybe${n ? ' ' + Array.from({ length: n }, atom).join(' ') : ''}`;
593602
}
603+
// SH3-1b: sep(alt([Ident,':',Number], Number)) — include multi-pair + trailing
604+
// delim; incomplete `id:` forms are CST-over-accept (see SH3-1b reply), not emitted.
594605
if (r < .90) {
595606
const n = Math.floor(rng() * 4);
596-
const pairs = Array.from({ length: n }, () => rng() < .55 ? `${pick(ids)}:${pick(nums)}` : pick(nums));
607+
const pairs = Array.from({ length: n }, () => {
608+
const k = rng();
609+
if (k < .45) return `${pick(ids)}:${pick(nums)}`;
610+
if (k < .75) return pick(nums);
611+
return `${pick(ids)} : ${pick(nums)}`;
612+
});
597613
return `pairs(${pairs.join(',')}${n && rng() < .3 ? ',' : ''})`;
598614
}
599615
if (r < .95) return `notany ${pick(ids.filter((x) => x !== 'bad' && x !== 'worse'))}`;
@@ -637,6 +653,11 @@ export function buildToyCorpus(seed = 0x5a2_2026): { src: string; source: string
637653
'txn a:b?;', 'txn a:b!;', 'txn a.;', 'line a b;', 'line a\nb;',
638654
'noplus 1+2;', 'repeat a 1 b 2;', 'maybe;', 'maybe a 1;',
639655
'pairs();', 'pairs(a:1,2,b:3,);', 'notany good;', 'notany bad;',
656+
// SH3-1b: suppress must not block prec-binary `*` (LED-only exclude)
657+
'noplus 1 * 2;', 'noplus 1 * 2 * 3;', 'noplus (1*2);', 'noplus 1*2;',
658+
'noplus 1/2;', 'noplus 1*2+3;', 'noplus (1*2)*3;', 'noplus 1*(2*3);',
659+
// SH3-1b: well-formed sep+alt (incomplete `pairs(a:)` is CST-over-accept — not here)
660+
'pairs(a:1);', 'pairs(1, a:2);', 'pairs(a:1, 2, b:3);', 'pairs( a : 1 , );',
640661
// SH2-0b: choice-arm nested groups + multi-stmt
641662
'tag x=(1);', 'tag x=((1));', 'tag y=(((2)));', 'tag z:(3);', 'tag z:((a));',
642663
'tag x:1;tag y=2;', 'bang!x;tag z;', 'tag x=(1);tag y=2;tag z;',

test/shape-parity.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,18 @@ async function main(): Promise<void> {
10061006

10071007
}
10081008

1009+
// ── SH3-1b: suppress is LED-only; prec-binary survives exclude('*', Expr) ─
1010+
const sh31bNoplus = [
1011+
'noplus 1 * 2;', 'noplus 1 * 2 * 3;', 'noplus (1*2);', 'noplus 1*2;',
1012+
'noplus 1/2;', 'noplus 1*2+3;', 'noplus (1*2)*3;', 'noplus 1*(2*3);',
1013+
];
1014+
for (const src of sh31bNoplus) {
1015+
check(
1016+
accepts(toyMod, src, false) && accepts(toyMod, src, true),
1017+
`SH3-1b suppress/binary accept ${JSON.stringify(src)}`,
1018+
);
1019+
}
1020+
10091021
// ── Guard + capped witnesses (toy) ────────────────────────────────────────
10101022
check(
10111023
accepts(toyMod, 'a::b;', false) && accepts(toyMod, 'a::b;', true),

test/shape-rust.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,24 @@ async function main(): Promise<void> {
286286
check('toy CST≡AST accept equivalence', toyAcceptDiv === 0, `${toyCorpus.length} cases, ${toyAcceptDiv} divergences`);
287287
check('toy TS↔Rust AST isomorphism', toyIsoBad === 0, `${toyIsoN} compared, ${toyIsoBad} divergences`);
288288

289+
// SH3-1b: suppress is LED-only — prec-binary under exclude('*') must accept + iso
290+
const sh31bNoplus = [
291+
'noplus 1 * 2;', 'noplus 1 * 2 * 3;', 'noplus (1*2);', 'noplus 1*2;',
292+
'noplus 1/2;', 'noplus 1*2+3;', 'noplus (1*2)*3;', 'noplus 1*(2*3);',
293+
];
294+
const sh31bLines = runBatch(toyBin, sh31bNoplus);
295+
let sh31bBad = 0;
296+
for (let i = 0; i < sh31bNoplus.length; i++) {
297+
const src = sh31bNoplus[i]!;
298+
const tsCst = toyTs.parse(toyTs.tokenize(src)) !== null;
299+
const tsAst = toyTs.parseAst(src);
300+
const rustOk = sh31bLines[i]!.startsWith('A\t');
301+
if (!tsCst || tsAst === null || !rustOk) { sh31bBad++; continue; }
302+
const rust = stripSpans(JSON.parse(sh31bLines[i]!.slice(2)));
303+
if (JSON.stringify(rust) !== JSON.stringify(stripSpans(tsAst))) sh31bBad++;
304+
}
305+
check('SH3-1b noplus suppress/binary TS↔Rust', sh31bBad === 0, `${sh31bNoplus.length - sh31bBad}/${sh31bNoplus.length}`);
306+
289307
let failFast = '';
290308
try {
291309
emitRust(typescriptGrammar, { shape: typescriptShape });

0 commit comments

Comments
 (0)