Allow statement boundary to include end-of-group, so long as the group is toplevel or braces
This commit is contained in:
parent
658f324e76
commit
73b7759816
|
@ -153,7 +153,7 @@ export function expand(tree: Items, ctx: ExpansionContext): Items {
|
|||
}
|
||||
|
||||
function x<T>(p: Pattern<T>, f: (v: T, t: TemplateFunction) => Items) {
|
||||
tree = replace(tree, p, (v, start) => f(v, macro.template(fixPos(start))));
|
||||
tree = replace(tree, null, p, (v, start) => f(v, macro.template(fixPos(start))));
|
||||
}
|
||||
|
||||
function xf<T extends TurnAction>(p: Pattern<T>, f: (v: T, t: TemplateFunction) => Items) {
|
||||
|
@ -168,7 +168,7 @@ export function expand(tree: Items, ctx: ExpansionContext): Items {
|
|||
// following transformations matters.
|
||||
|
||||
xf(ctx.parser.duringStatement, (s, t) => {
|
||||
let spawn = match(ctx.parser.spawn, s.body, null);
|
||||
let spawn = match(ctx.parser.spawn, s.body, null, null);
|
||||
if (spawn !== null) {
|
||||
if (spawn.linkedToken !== null) {
|
||||
ctx.emitError(`during ... spawn doesn't need "linked", it's always linked`,
|
||||
|
|
|
@ -181,7 +181,15 @@ export class SyndicateParser {
|
|||
return group('{', map(rest, items => (acc?.push(... items), items)));
|
||||
}
|
||||
|
||||
readonly statementBoundary = alt<any>(atom(';'), Matcher.newline);
|
||||
readonly statementBoundary = alt<any>(
|
||||
atom(';'),
|
||||
Matcher.newline,
|
||||
seq(Matcher.end, i => {
|
||||
if (i.context === null || i.context === '{') return discard(i);
|
||||
// ^ toplevel, or inside braces, so presumably statement context
|
||||
return fail(i); // otherwise, parens or brackets presumably, so not statement context
|
||||
}),
|
||||
);
|
||||
readonly exprBoundary = alt<any>(atom(';'), atom(','), group('{', discard), Matcher.end);
|
||||
|
||||
readonly identifier: Pattern<Identifier> = atom();
|
||||
|
@ -432,7 +440,7 @@ export class SyndicateParser {
|
|||
|
||||
hasCapturesOrDiscards(e: Expr): boolean {
|
||||
return foldItems(e,
|
||||
t => match(alt<any>(this.pCaptureBinder, this.pDiscard), [t], null) !== null,
|
||||
t => match(alt<any>(this.pCaptureBinder, this.pDiscard), [t], null, '(') !== null,
|
||||
(_g, b, _k) => b,
|
||||
bs => bs.some(b => b));
|
||||
}
|
||||
|
@ -495,7 +503,7 @@ export class SyndicateParser {
|
|||
// });
|
||||
// } else
|
||||
if (this.hasCapturesOrDiscards(o.ctor)) {
|
||||
const r = match(this.pCaptureBinder, o.ctor, null);
|
||||
const r = match(this.pCaptureBinder, o.ctor, null, '(');
|
||||
if (r !== null && o.arguments.length === 1) {
|
||||
return succeed({
|
||||
type: 'PCapture',
|
||||
|
|
|
@ -1,26 +1,27 @@
|
|||
/// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
/// SPDX-FileCopyrightText: Copyright © 2016-2024 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
||||
|
||||
export interface List<T> extends Iterable<T> {
|
||||
export interface List<T, C> extends Iterable<T> {
|
||||
item: T | null;
|
||||
next: List<T> | null;
|
||||
next: List<T, C> | null;
|
||||
context: C;
|
||||
|
||||
toArray(): Array<T>;
|
||||
}
|
||||
|
||||
export function atEnd<T>(xs: List<T>): xs is (List<T> & { item: null, next: null }) {
|
||||
export function atEnd<T, C>(xs: List<T, C>): xs is (List<T, C> & { item: null, next: null }) {
|
||||
return xs.item === null;
|
||||
}
|
||||
|
||||
export function notAtEnd<T>(xs: List<T>): xs is (List<T> & { item: T, next: List<T> }) {
|
||||
export function notAtEnd<T, C>(xs: List<T, C>): xs is (List<T, C> & { item: T, next: List<T, C> }) {
|
||||
return xs.item !== null;
|
||||
}
|
||||
|
||||
export class ArrayList<T> implements List<T> {
|
||||
export class ArrayList<T, C> implements List<T, C> {
|
||||
readonly items: Array<T>;
|
||||
readonly index: number = 0;
|
||||
|
||||
constructor(items: Array<T>, index = 0) {
|
||||
constructor(items: Array<T>, public context: C, index = 0) {
|
||||
this.items = items;
|
||||
this.index = index;
|
||||
}
|
||||
|
@ -29,9 +30,9 @@ export class ArrayList<T> implements List<T> {
|
|||
return this.items[this.index] ?? null;
|
||||
}
|
||||
|
||||
get next(): List<T> | null {
|
||||
get next(): List<T, C> | null {
|
||||
if (this.index >= this.items.length) return null;
|
||||
return new ArrayList(this.items, this.index + 1);
|
||||
return new ArrayList(this.items, this.context, this.index + 1);
|
||||
}
|
||||
|
||||
toArray(): Array<T> {
|
||||
|
@ -39,7 +40,7 @@ export class ArrayList<T> implements List<T> {
|
|||
}
|
||||
|
||||
[Symbol.iterator](): Iterator<T> {
|
||||
let i: List<T> = this;
|
||||
let i: List<T, C> = this;
|
||||
return {
|
||||
next(): IteratorResult<T> {
|
||||
if (notAtEnd(i)) {
|
||||
|
|
|
@ -11,26 +11,28 @@ import { List, ArrayList, atEnd, notAtEnd } from './list';
|
|||
//---------------------------------------------------------------------------
|
||||
// Patterns over Item
|
||||
|
||||
export type PatternResult<T> = [T, List<Item>] | null;
|
||||
export type Pattern<T> = (i: List<Item>) => PatternResult<T>;
|
||||
export type ItemContext = string /* the opener of the containing group, if any */ | null;
|
||||
export type ItemList = List<Item, ItemContext>;
|
||||
export type PatternResult<T> = [T, ItemList] | null;
|
||||
export type Pattern<T> = (i: ItemList) => PatternResult<T>;
|
||||
|
||||
export type PatternTypeArg<P> = P extends Pattern<infer T> ? T : never;
|
||||
|
||||
export function match<T,F>(p: Pattern<T>, items: Items, failure: F): T | F {
|
||||
const r = p(new ArrayList(items));
|
||||
export function match<T,F>(p: Pattern<T>, items: Items, failure: F, context: ItemContext): T | F {
|
||||
const r = p(new ArrayList(items, context));
|
||||
if (r === null) return failure;
|
||||
if (notAtEnd(skipSpace(r[1]))) return failure;
|
||||
return r[0];
|
||||
}
|
||||
|
||||
export const noItems = new ArrayList<Item>([]);
|
||||
export const noItems = (c: ItemContext) => new ArrayList([], c);
|
||||
|
||||
export const fail: Pattern<never> = _i => null;
|
||||
export function succeed<T>(t: T): Pattern<T> { return i => [t, i]; }
|
||||
|
||||
export const discard: Pattern<void> = _i => [void 0, noItems];
|
||||
export const rest: Pattern<Items> = i => [i.toArray(), noItems];
|
||||
export const end: Pattern<void> = i => atEnd(skipSpace(i)) ? [void 0, noItems] : null;
|
||||
export const discard: Pattern<void> = i => [void 0, noItems(i.context)];
|
||||
export const rest: Pattern<Items> = i => [i.toArray(), noItems(i.context)];
|
||||
export const end: Pattern<void> = i => atEnd(skipSpace(i)) ? [void 0, noItems(i.context)] : null;
|
||||
export const pos: Pattern<Pos> = i => notAtEnd(i) ? [i.item.start, i] : null;
|
||||
|
||||
export const newline: Pattern<Item> = i => {
|
||||
|
@ -39,12 +41,12 @@ export const newline: Pattern<Item> = i => {
|
|||
return [i.item, i.next];
|
||||
};
|
||||
|
||||
export function skipSpace(i: List<Item>): List<Item> {
|
||||
export function skipSpace(i: ItemList): ItemList {
|
||||
while (notAtEnd(i) && isSpace(i.item)) i = i.next;
|
||||
return i;
|
||||
}
|
||||
|
||||
export function collectSpace(i: List<Item>, acc: Array<Item>): List<Item> {
|
||||
export function collectSpace(i: ItemList, acc: Array<Item>): ItemList {
|
||||
while (notAtEnd(i) && isSpace(i.item)) {
|
||||
acc.push(i.item);
|
||||
i = i.next;
|
||||
|
@ -133,7 +135,7 @@ export function bind<T, K extends keyof T>(target: T, key: K, pattern: Pattern<T
|
|||
};
|
||||
}
|
||||
|
||||
export function exec(thunk: (i: List<Item>) => void): Pattern<void> {
|
||||
export function exec(thunk: (i: ItemList) => void): Pattern<void> {
|
||||
return i => {
|
||||
thunk(i);
|
||||
return [void 0, i];
|
||||
|
@ -174,7 +176,7 @@ export function group<T>(opener: string, items: Pattern<T>, options: GroupOption
|
|||
if (!notAtEnd(i)) return null;
|
||||
if (!isGroup(i.item)) return null;
|
||||
if (i.item.open.text !== opener) return null;
|
||||
const r = items(new ArrayList(i.item.items));
|
||||
const r = items(new ArrayList(i.item.items, opener));
|
||||
if (r === null) return null;
|
||||
if (!atEnd(r[1])) return null;
|
||||
return [r[0], (options.advance ?? true) ? i.next : i];
|
||||
|
@ -221,7 +223,7 @@ export function upTo(p: Pattern<any>): Pattern<Items> {
|
|||
export function separatedBy<T>(itemPattern: Pattern<T>, separator: Pattern<any>): Pattern<T[]> {
|
||||
return i => {
|
||||
const acc: T[] = [];
|
||||
if (end(i) !== null) return [acc, noItems];
|
||||
if (end(i) !== null) return [acc, noItems(i.context)];
|
||||
while (true) {
|
||||
{
|
||||
const r = itemPattern(i);
|
||||
|
@ -232,7 +234,7 @@ export function separatedBy<T>(itemPattern: Pattern<T>, separator: Pattern<any>)
|
|||
{
|
||||
const r = separator(i);
|
||||
if (r === null) {
|
||||
if (end(i) !== null) return [acc, noItems];
|
||||
if (end(i) !== null) return [acc, noItems(i.context)];
|
||||
return null;
|
||||
}
|
||||
i = r[1];
|
||||
|
@ -247,7 +249,7 @@ export function separatedOrTerminatedBy<T>(
|
|||
): Pattern<T[]> {
|
||||
return i => {
|
||||
const acc: T[] = [];
|
||||
if (end(i) !== null) return [acc, noItems];
|
||||
if (end(i) !== null) return [acc, noItems(i.context)];
|
||||
while (true) {
|
||||
{
|
||||
const r = itemPattern(i);
|
||||
|
@ -258,11 +260,11 @@ export function separatedOrTerminatedBy<T>(
|
|||
{
|
||||
const r = separator(i);
|
||||
if (r === null) {
|
||||
if (end(i) !== null) return [acc, noItems];
|
||||
if (end(i) !== null) return [acc, noItems(i.context)];
|
||||
return null;
|
||||
} else {
|
||||
i = r[1];
|
||||
if (end(i) !== null) return [acc, noItems];
|
||||
if (end(i) !== null) return [acc, noItems(i.context)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -308,12 +310,13 @@ export function option<T>(p: Pattern<T>): Pattern<T[]> {
|
|||
|
||||
export function replace<T>(
|
||||
items: Items,
|
||||
outerContext: ItemContext,
|
||||
p: Pattern<T>,
|
||||
f: (t: T, start: Pos, end: Pos) => Items,
|
||||
end: Pos = items.length > 0 ? items[items.length - 1].end : startPos(null)) : Items
|
||||
{
|
||||
const walkItems = (items: Items, end: Pos): Items => {
|
||||
let i: List<Item> = new ArrayList(items);
|
||||
end: Pos = items.length > 0 ? items[items.length - 1].end : startPos(null),
|
||||
) : Items {
|
||||
const walkItems = (items: Items, end: Pos, context: ItemContext): Items => {
|
||||
let i: ItemList = new ArrayList(items, context);
|
||||
const acc: Items = [];
|
||||
while (notAtEnd(i = collectSpace(i, acc))) {
|
||||
const r = p(i);
|
||||
|
@ -327,11 +330,14 @@ export function replace<T>(
|
|||
acc.push(i.item);
|
||||
i = i.next;
|
||||
} else {
|
||||
acc.push({ ... i.item, items: walkItems(i.item.items, i.item.end) });
|
||||
acc.push({
|
||||
... i.item,
|
||||
items: walkItems(i.item.items, i.item.end, i.item.open.text),
|
||||
});
|
||||
i = i.next;
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
};
|
||||
return walkItems(items, end);
|
||||
return walkItems(items, end, outerContext);
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ export class Templates {
|
|||
this.readOptions = readOptions;
|
||||
}
|
||||
|
||||
template(start0: Pos | string = this.defaultPos): TemplateFunction {
|
||||
template(start0: Pos | string = this.defaultPos, context: M.ItemContext = null): TemplateFunction {
|
||||
const start = (typeof start0 === 'string') ? startPos(start0) : start0;
|
||||
return (consts, ... vars) => {
|
||||
const sourcePieces = [consts[0]];
|
||||
|
@ -53,6 +53,7 @@ export class Templates {
|
|||
(this.readOptions.extraDelimiters ?? '') + '$',
|
||||
synthetic: true,
|
||||
}),
|
||||
context,
|
||||
substPat,
|
||||
sub => toItems(this.readOptions, vars[i++], sub.pos));
|
||||
};
|
||||
|
|
|
@ -74,7 +74,17 @@ __SYNDICATE__.Dataspace._spawn(() => {
|
|||
|
||||
describe('stop', () => {
|
||||
|
||||
it('non-statement', () => expectCodeEqual(`stop`, `stop`));
|
||||
it('non-statement', () => expectCodeEqual(`(stop)`, `(stop)`));
|
||||
it('toplevel end-delimited statement', () => expectCodeEqual(`stop`, `
|
||||
__SYNDICATE__.Turn.active._stop(currentSyndicateFacet, () => {
|
||||
const currentSyndicateFacet = __SYNDICATE__.Turn.activeFacet;
|
||||
});`));
|
||||
it('nested end-delimited statement', () => expectCodeEqual(`{ stop }`, `
|
||||
{
|
||||
__SYNDICATE__.Turn.active._stop(currentSyndicateFacet, () => {
|
||||
const currentSyndicateFacet = __SYNDICATE__.Turn.activeFacet;
|
||||
});
|
||||
}`));
|
||||
|
||||
it('without facet, without body', () => expectCodeEqual(`stop;`, `
|
||||
__SYNDICATE__.Turn.active._stop(currentSyndicateFacet, () => {
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
/// SPDX-FileCopyrightText: Copyright © 2024 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
||||
|
||||
import { Grammar, Syntax } from '../src/index';
|
||||
import './test-utils';
|
||||
|
||||
describe('statement boundary', () => {
|
||||
function stmt(input: string): [string, string] | null {
|
||||
const parser = new Grammar.SyndicateParser();
|
||||
const tree = Syntax.laxRead(input);
|
||||
const items: Syntax.Items = [];
|
||||
const r = parser.statement(items)(new Syntax.ArrayList(tree, '{'));
|
||||
if (r === null) return null;
|
||||
return [Syntax.itemText(items), Syntax.itemText(r[1].toArray())];
|
||||
}
|
||||
|
||||
it('should include semicolon', () => {
|
||||
expect(stmt('i am a statement ; ')).toEqual(['i am a statement;', ' ']);
|
||||
});
|
||||
|
||||
it('should include newline', () => {
|
||||
expect(stmt('i am a statement \n ')).toEqual(['i am a statement\n', ' ']);
|
||||
});
|
||||
|
||||
it('should include closing brace on the same line', () => {
|
||||
// Note that `" remainder is in outer group"` is discarded by `laxRead`.
|
||||
expect(stmt('i am a statement } remainder is in outer group'))
|
||||
.toEqual(['i am a statement', '']);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue