Allow statement boundary to include end-of-group, so long as the group is toplevel or braces

This commit is contained in:
Tony Garnock-Jones 2024-03-21 16:04:03 +01:00
parent 658f324e76
commit 73b7759816
7 changed files with 95 additions and 39 deletions

View File

@ -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`,

View File

@ -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',

View File

@ -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)) {

View File

@ -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);
}

View File

@ -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));
};

View File

@ -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, () => {

View File

@ -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', '']);
});
});