Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 85 additions & 19 deletions packages/compiler/test/test-cstReader.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import test from 'ava';
import * as fc from 'fast-check';

import {createHandle, createReader, CstNodeType} from '../../runtime/src/cstReader.ts';
import {createReader, CstNodeType} from '../../runtime/src/cstReader.ts';
import {createHandle} from '../../runtime/src/cstReaderShared.ts';
import {compileAndLoad, matchWithInput} from './_helpers.js';

const childrenOf = (reader, handle, i) => {
const childrenOf = (reader, handle) => {
const arr = [];
reader.forEachChild(handle, c => arr.push(c));
return arr;
Expand All @@ -31,8 +32,8 @@ test('terminal children', async t => {
g.match('abcd').use(mr => {
const reader = createReader(mr);
const children = [];
reader.forEachChild(reader.root, (child, leadingSpaces, startIdx, index) => {
children.push({child, leadingSpaces, startIdx, index});
reader.forEachChild(reader.root, (child, leadingSpaces, index) => {
children.push({child, leadingSpaces, startIdx: reader.startIdx(child), index});
});
t.is(children.length, 2);

Expand All @@ -57,8 +58,8 @@ test('nonterminal children', async t => {
g.match('xy').use(mr => {
const reader = createReader(mr);
const children = [];
reader.forEachChild(reader.root, (child, ls, startIdx, i) => {
children.push({child, ls, startIdx, i});
reader.forEachChild(reader.root, (child, ls, i) => {
children.push({child, ls, startIdx: reader.startIdx(child), i});
});
t.is(children.length, 2);
t.is(reader.ctorName(children[0].child), 'a');
Expand Down Expand Up @@ -137,6 +138,71 @@ test('optional node: absent', async t => {
});
});

test('withChildren, tupleArity, forEachTuple, and isPresent', async t => {
const g = await compileAndLoad('G { start = ("a" "b"?)* }');
g.match('abab').use(mr => {
const reader = createReader(mr);
let list;
reader.forEachChild(reader.root, child => {
list = child;
});

t.is(reader.tupleArity(list), 2);

const tuples = [];
reader.forEachTuple(list, (a, b) => {
tuples.push(
reader.sourceString(a) +
reader.withChildren(b, (_handle, child) =>
reader.isPresent(b) ? reader.sourceString(child) : ''
)
);
});
t.deepEqual(tuples, ['ab', 'ab']);

let emptyOpt;
g.match('a').use(mr2 => {
const reader2 = createReader(mr2);
reader2.forEachChild(reader2.root, child => {
list = child;
});
reader2.forEachTuple(list, (_a, b) => {
emptyOpt = b;
});
t.false(reader2.isPresent(emptyOpt));
t.is(
reader2.withChildren(emptyOpt, (_handle, child) =>
child === undefined ? 'missing' : 'present'
),
'missing'
);
});
});
});

test('type-specific helpers assert on the wrong handle kind', async t => {
const g = await compileAndLoad('G { Start = ("a" "b"?)* }');
g.match('ab').use(mr => {
const reader = createReader(mr);
let list;
reader.forEachChild(reader.root, child => {
list = child;
});

let terminal;
let opt;
reader.forEachTuple(list, (a, b) => {
terminal = a;
opt = b;
});

t.throws(() => reader.ruleId(list), {message: 'Not a nonterminal'});
t.throws(() => reader.tupleArity(reader.root), {message: 'Not a list'});
t.throws(() => reader.isPresent(terminal), {message: 'Not an opt'});
t.true(reader.isPresent(opt));
});
});

// --- unparse via walk ---

test('unparse: simple terminals', async t => {
Expand Down Expand Up @@ -215,7 +281,7 @@ test('rootLeadingSpacesLen: present', async t => {
g.match(' x').use(mr => {
const reader = createReader(mr);
t.is(reader.rootLeadingSpacesLen, 2);
t.is(reader.sourceSlice(0, reader.rootLeadingSpacesLen), ' ');
t.is(reader.input.slice(0, reader.rootLeadingSpacesLen), ' ');
t.is(reader.startIdx(reader.root), 2);
});
});
Expand All @@ -233,14 +299,15 @@ test('child leadingSpaces in syntactic rule', async t => {
g.match('a b').use(mr => {
const reader = createReader(mr);
const spacesInfo = [];
reader.forEachChild(reader.root, (child, leadingSpacesLen, childStartIdx, index) => {
reader.forEachChild(reader.root, (child, leadingSpacesLen, index) => {
const childStartIdx = reader.startIdx(child);
spacesInfo.push({
index,
hasSpaces: leadingSpacesLen > 0,
spacesLen: leadingSpacesLen,
spacesStr:
leadingSpacesLen > 0
? reader.sourceSlice(childStartIdx - leadingSpacesLen, leadingSpacesLen)
? reader.input.slice(childStartIdx - leadingSpacesLen, childStartIdx)
: '',
});
});
Expand Down Expand Up @@ -272,8 +339,8 @@ const spaceMemoIgnored = test.macro(async (t, twoBody, input = '> xx') => {
const reader = createReader(mr);
const [two] = childrenOf(reader, reader.root);
const children = [];
reader.forEachChild(two, (child, leadingSpacesLen, childStartIdx) => {
children.push({child, leadingSpacesLen, childStartIdx});
reader.forEachChild(two, (child, leadingSpacesLen) => {
children.push({child, leadingSpacesLen, childStartIdx: reader.startIdx(child)});
});
t.deepEqual(
children.map(({leadingSpacesLen}) => leadingSpacesLen),
Expand Down Expand Up @@ -305,15 +372,13 @@ test(
'> x'
);

// --- details ---
// --- rule metadata ---

test('details returns ruleId for nonterminals', async t => {
test('ruleId returns a stable rule index for nonterminals', async t => {
const g = await compileAndLoad('G { start = a\na = "x" }');
g.match('x').use(mr => {
const reader = createReader(mr);
// Root is 'start', details should be its ruleId (>= 0).
const d = reader.details(reader.root);
t.true(d >= 0);
t.true(reader.ruleId(reader.root) >= 0);
});
});

Expand Down Expand Up @@ -433,7 +498,8 @@ function checkInvariants(reader, handle, isLexicalParent) {
let cursor = start;
let reconstructed = '';

reader.forEachChild(handle, (child, leadingSpacesLen, childStartIdx, index) => {
reader.forEachChild(handle, (child, leadingSpacesLen, index) => {
const childStartIdx = reader.startIdx(child);
indices.push(index);
callbackCount++;

Expand Down Expand Up @@ -467,7 +533,7 @@ function checkInvariants(reader, handle, isLexicalParent) {

// Round-trip reconstruction: interleave spaces + child text.
if (leadingSpacesLen > 0) {
reconstructed += reader.sourceSlice(childStartIdx - leadingSpacesLen, leadingSpacesLen);
reconstructed += reader.input.slice(childStartIdx - leadingSpacesLen, childStartIdx);
}
reconstructed += reader.sourceString(child);

Expand Down Expand Up @@ -528,7 +594,7 @@ function checkMatch(reader) {
}

// -- Root round-trip: leadingSpaces + render(root) === input --
const rootSpaces = reader.sourceSlice(0, rootLeadingSpacesLen);
const rootSpaces = input.slice(0, rootLeadingSpacesLen);
const rootText = reader.sourceString(root);
if (rootSpaces + rootText !== input) {
errors.push(
Expand Down
45 changes: 45 additions & 0 deletions packages/compiler/test/test-wasm.js
Original file line number Diff line number Diff line change
Expand Up @@ -2127,3 +2127,48 @@ test('edge flag: tagged terminal decoding with HAS_LEADING_SPACES bit', async t
t.is(letter.sourceString, 'c');
t.falsy(letter.leadingSpaces);
});

// Regression: MatchResult.input must reflect the input from *its* match,
// not the most recent match on the same grammar.
test('MatchResult.input is stable after a subsequent match', async t => {
const g = await compileAndLoad('G { start = letter+ }');
g.match('abc').use(r1 => {
g.match('xy').use(r2 => {
t.is(r1.input, 'abc');
t.is(r2.input, 'xy');
});
});
});

// Regression: getRightmostFailures() must not silently return wrong data
// when wasm state has been overwritten by a subsequent match().
test('FailedMatchResult.getRightmostFailures throws if not the most recent match', async t => {
const g = await compileAndLoad('G { start = "ok" end }');

g.match('bad').use(r1 => {
t.true(r1.failed());

// A subsequent match overwrites the wasm state.
g.match('ok').use(r2 => {
t.true(r2.succeeded());

// Accessing failures on the stale result should throw.
t.throws(() => r1.getRightmostFailures(), {
message: /not the most recent match/,
});
});
});
});

// getRightmostFailures() works when called on the most recent match.
test('FailedMatchResult.getRightmostFailures works on most recent match', async t => {
const g = await compileAndLoad('G { start = "ok" end }');

g.match('bad').use(r1 => {
t.true(r1.failed());

const failures = r1.getRightmostFailures();
t.true(failures.length > 0);
t.is(r1.getRightmostFailurePosition(), 0);
});
});
4 changes: 2 additions & 2 deletions packages/lang-python/convertToOhm.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import assert from 'node:assert';

import {grammar} from '@ohm-js/compiler/compat';
import type {Operation} from '@ohm-js/semantics/src/types.ts';
import {createOperation} from '@ohm-js/semantics/src/index.ts';
import {createOperation} from '@ohm-js/semantics';
import type {Operation} from '@ohm-js/semantics';

const hasOwn = (obj: object, prop: string) => Object.hasOwnProperty.call(obj, prop);

Expand Down
Loading
Loading