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) {
|
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) {
|
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.
|
// following transformations matters.
|
||||||
|
|
||||||
xf(ctx.parser.duringStatement, (s, t) => {
|
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 !== null) {
|
||||||
if (spawn.linkedToken !== null) {
|
if (spawn.linkedToken !== null) {
|
||||||
ctx.emitError(`during ... spawn doesn't need "linked", it's always linked`,
|
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)));
|
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 exprBoundary = alt<any>(atom(';'), atom(','), group('{', discard), Matcher.end);
|
||||||
|
|
||||||
readonly identifier: Pattern<Identifier> = atom();
|
readonly identifier: Pattern<Identifier> = atom();
|
||||||
|
@ -432,7 +440,7 @@ export class SyndicateParser {
|
||||||
|
|
||||||
hasCapturesOrDiscards(e: Expr): boolean {
|
hasCapturesOrDiscards(e: Expr): boolean {
|
||||||
return foldItems(e,
|
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,
|
(_g, b, _k) => b,
|
||||||
bs => bs.some(b => b));
|
bs => bs.some(b => b));
|
||||||
}
|
}
|
||||||
|
@ -495,7 +503,7 @@ export class SyndicateParser {
|
||||||
// });
|
// });
|
||||||
// } else
|
// } else
|
||||||
if (this.hasCapturesOrDiscards(o.ctor)) {
|
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) {
|
if (r !== null && o.arguments.length === 1) {
|
||||||
return succeed({
|
return succeed({
|
||||||
type: 'PCapture',
|
type: 'PCapture',
|
||||||
|
|
|
@ -1,26 +1,27 @@
|
||||||
/// SPDX-License-Identifier: GPL-3.0-or-later
|
/// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
/// SPDX-FileCopyrightText: Copyright © 2016-2024 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
|
/// 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;
|
item: T | null;
|
||||||
next: List<T> | null;
|
next: List<T, C> | null;
|
||||||
|
context: C;
|
||||||
|
|
||||||
toArray(): Array<T>;
|
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;
|
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;
|
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 items: Array<T>;
|
||||||
readonly index: number = 0;
|
readonly index: number = 0;
|
||||||
|
|
||||||
constructor(items: Array<T>, index = 0) {
|
constructor(items: Array<T>, public context: C, index = 0) {
|
||||||
this.items = items;
|
this.items = items;
|
||||||
this.index = index;
|
this.index = index;
|
||||||
}
|
}
|
||||||
|
@ -29,9 +30,9 @@ export class ArrayList<T> implements List<T> {
|
||||||
return this.items[this.index] ?? null;
|
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;
|
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> {
|
toArray(): Array<T> {
|
||||||
|
@ -39,7 +40,7 @@ export class ArrayList<T> implements List<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
[Symbol.iterator](): Iterator<T> {
|
[Symbol.iterator](): Iterator<T> {
|
||||||
let i: List<T> = this;
|
let i: List<T, C> = this;
|
||||||
return {
|
return {
|
||||||
next(): IteratorResult<T> {
|
next(): IteratorResult<T> {
|
||||||
if (notAtEnd(i)) {
|
if (notAtEnd(i)) {
|
||||||
|
|
|
@ -11,26 +11,28 @@ import { List, ArrayList, atEnd, notAtEnd } from './list';
|
||||||
//---------------------------------------------------------------------------
|
//---------------------------------------------------------------------------
|
||||||
// Patterns over Item
|
// Patterns over Item
|
||||||
|
|
||||||
export type PatternResult<T> = [T, List<Item>] | null;
|
export type ItemContext = string /* the opener of the containing group, if any */ | null;
|
||||||
export type Pattern<T> = (i: List<Item>) => PatternResult<T>;
|
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 type PatternTypeArg<P> = P extends Pattern<infer T> ? T : never;
|
||||||
|
|
||||||
export function match<T,F>(p: Pattern<T>, items: Items, failure: F): T | F {
|
export function match<T,F>(p: Pattern<T>, items: Items, failure: F, context: ItemContext): T | F {
|
||||||
const r = p(new ArrayList(items));
|
const r = p(new ArrayList(items, context));
|
||||||
if (r === null) return failure;
|
if (r === null) return failure;
|
||||||
if (notAtEnd(skipSpace(r[1]))) return failure;
|
if (notAtEnd(skipSpace(r[1]))) return failure;
|
||||||
return r[0];
|
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 const fail: Pattern<never> = _i => null;
|
||||||
export function succeed<T>(t: T): Pattern<T> { return i => [t, i]; }
|
export function succeed<T>(t: T): Pattern<T> { return i => [t, i]; }
|
||||||
|
|
||||||
export const discard: Pattern<void> = _i => [void 0, noItems];
|
export const discard: Pattern<void> = i => [void 0, noItems(i.context)];
|
||||||
export const rest: Pattern<Items> = i => [i.toArray(), noItems];
|
export const rest: Pattern<Items> = i => [i.toArray(), noItems(i.context)];
|
||||||
export const end: Pattern<void> = i => atEnd(skipSpace(i)) ? [void 0, noItems] : null;
|
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 pos: Pattern<Pos> = i => notAtEnd(i) ? [i.item.start, i] : null;
|
||||||
|
|
||||||
export const newline: Pattern<Item> = i => {
|
export const newline: Pattern<Item> = i => {
|
||||||
|
@ -39,12 +41,12 @@ export const newline: Pattern<Item> = i => {
|
||||||
return [i.item, i.next];
|
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;
|
while (notAtEnd(i) && isSpace(i.item)) i = i.next;
|
||||||
return i;
|
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)) {
|
while (notAtEnd(i) && isSpace(i.item)) {
|
||||||
acc.push(i.item);
|
acc.push(i.item);
|
||||||
i = i.next;
|
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 => {
|
return i => {
|
||||||
thunk(i);
|
thunk(i);
|
||||||
return [void 0, 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 (!notAtEnd(i)) return null;
|
||||||
if (!isGroup(i.item)) return null;
|
if (!isGroup(i.item)) return null;
|
||||||
if (i.item.open.text !== opener) 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 (r === null) return null;
|
||||||
if (!atEnd(r[1])) return null;
|
if (!atEnd(r[1])) return null;
|
||||||
return [r[0], (options.advance ?? true) ? i.next : i];
|
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[]> {
|
export function separatedBy<T>(itemPattern: Pattern<T>, separator: Pattern<any>): Pattern<T[]> {
|
||||||
return i => {
|
return i => {
|
||||||
const acc: T[] = [];
|
const acc: T[] = [];
|
||||||
if (end(i) !== null) return [acc, noItems];
|
if (end(i) !== null) return [acc, noItems(i.context)];
|
||||||
while (true) {
|
while (true) {
|
||||||
{
|
{
|
||||||
const r = itemPattern(i);
|
const r = itemPattern(i);
|
||||||
|
@ -232,7 +234,7 @@ export function separatedBy<T>(itemPattern: Pattern<T>, separator: Pattern<any>)
|
||||||
{
|
{
|
||||||
const r = separator(i);
|
const r = separator(i);
|
||||||
if (r === null) {
|
if (r === null) {
|
||||||
if (end(i) !== null) return [acc, noItems];
|
if (end(i) !== null) return [acc, noItems(i.context)];
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
i = r[1];
|
i = r[1];
|
||||||
|
@ -247,7 +249,7 @@ export function separatedOrTerminatedBy<T>(
|
||||||
): Pattern<T[]> {
|
): Pattern<T[]> {
|
||||||
return i => {
|
return i => {
|
||||||
const acc: T[] = [];
|
const acc: T[] = [];
|
||||||
if (end(i) !== null) return [acc, noItems];
|
if (end(i) !== null) return [acc, noItems(i.context)];
|
||||||
while (true) {
|
while (true) {
|
||||||
{
|
{
|
||||||
const r = itemPattern(i);
|
const r = itemPattern(i);
|
||||||
|
@ -258,11 +260,11 @@ export function separatedOrTerminatedBy<T>(
|
||||||
{
|
{
|
||||||
const r = separator(i);
|
const r = separator(i);
|
||||||
if (r === null) {
|
if (r === null) {
|
||||||
if (end(i) !== null) return [acc, noItems];
|
if (end(i) !== null) return [acc, noItems(i.context)];
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
i = r[1];
|
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>(
|
export function replace<T>(
|
||||||
items: Items,
|
items: Items,
|
||||||
|
outerContext: ItemContext,
|
||||||
p: Pattern<T>,
|
p: Pattern<T>,
|
||||||
f: (t: T, start: Pos, end: Pos) => Items,
|
f: (t: T, start: Pos, end: Pos) => Items,
|
||||||
end: Pos = items.length > 0 ? items[items.length - 1].end : startPos(null)) : Items
|
end: Pos = items.length > 0 ? items[items.length - 1].end : startPos(null),
|
||||||
{
|
) : Items {
|
||||||
const walkItems = (items: Items, end: Pos): Items => {
|
const walkItems = (items: Items, end: Pos, context: ItemContext): Items => {
|
||||||
let i: List<Item> = new ArrayList(items);
|
let i: ItemList = new ArrayList(items, context);
|
||||||
const acc: Items = [];
|
const acc: Items = [];
|
||||||
while (notAtEnd(i = collectSpace(i, acc))) {
|
while (notAtEnd(i = collectSpace(i, acc))) {
|
||||||
const r = p(i);
|
const r = p(i);
|
||||||
|
@ -327,11 +330,14 @@ export function replace<T>(
|
||||||
acc.push(i.item);
|
acc.push(i.item);
|
||||||
i = i.next;
|
i = i.next;
|
||||||
} else {
|
} 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;
|
i = i.next;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
};
|
};
|
||||||
return walkItems(items, end);
|
return walkItems(items, end, outerContext);
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ export class Templates {
|
||||||
this.readOptions = readOptions;
|
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;
|
const start = (typeof start0 === 'string') ? startPos(start0) : start0;
|
||||||
return (consts, ... vars) => {
|
return (consts, ... vars) => {
|
||||||
const sourcePieces = [consts[0]];
|
const sourcePieces = [consts[0]];
|
||||||
|
@ -53,6 +53,7 @@ export class Templates {
|
||||||
(this.readOptions.extraDelimiters ?? '') + '$',
|
(this.readOptions.extraDelimiters ?? '') + '$',
|
||||||
synthetic: true,
|
synthetic: true,
|
||||||
}),
|
}),
|
||||||
|
context,
|
||||||
substPat,
|
substPat,
|
||||||
sub => toItems(this.readOptions, vars[i++], sub.pos));
|
sub => toItems(this.readOptions, vars[i++], sub.pos));
|
||||||
};
|
};
|
||||||
|
|
|
@ -74,7 +74,17 @@ __SYNDICATE__.Dataspace._spawn(() => {
|
||||||
|
|
||||||
describe('stop', () => {
|
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;`, `
|
it('without facet, without body', () => expectCodeEqual(`stop;`, `
|
||||||
__SYNDICATE__.Turn.active._stop(currentSyndicateFacet, () => {
|
__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