diff --git a/packages/compiler/src/compiler/codegen.ts b/packages/compiler/src/compiler/codegen.ts index d365bfe..116de21 100644 --- a/packages/compiler/src/compiler/codegen.ts +++ b/packages/compiler/src/compiler/codegen.ts @@ -153,7 +153,7 @@ export function expand(tree: Items, ctx: ExpansionContext): Items { } function x(p: Pattern, 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(p: Pattern, 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`, diff --git a/packages/compiler/src/compiler/grammar.ts b/packages/compiler/src/compiler/grammar.ts index 2550214..699eb49 100644 --- a/packages/compiler/src/compiler/grammar.ts +++ b/packages/compiler/src/compiler/grammar.ts @@ -181,7 +181,15 @@ export class SyndicateParser { return group('{', map(rest, items => (acc?.push(... items), items))); } - readonly statementBoundary = alt(atom(';'), Matcher.newline); + readonly statementBoundary = alt( + 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(atom(';'), atom(','), group('{', discard), Matcher.end); readonly identifier: Pattern = atom(); @@ -432,7 +440,7 @@ export class SyndicateParser { hasCapturesOrDiscards(e: Expr): boolean { return foldItems(e, - t => match(alt(this.pCaptureBinder, this.pDiscard), [t], null) !== null, + t => match(alt(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', diff --git a/packages/compiler/src/syntax/list.ts b/packages/compiler/src/syntax/list.ts index 3d95278..411ebae 100644 --- a/packages/compiler/src/syntax/list.ts +++ b/packages/compiler/src/syntax/list.ts @@ -1,26 +1,27 @@ /// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright © 2016-2024 Tony Garnock-Jones -export interface List extends Iterable { +export interface List extends Iterable { item: T | null; - next: List | null; + next: List | null; + context: C; toArray(): Array; } -export function atEnd(xs: List): xs is (List & { item: null, next: null }) { +export function atEnd(xs: List): xs is (List & { item: null, next: null }) { return xs.item === null; } -export function notAtEnd(xs: List): xs is (List & { item: T, next: List }) { +export function notAtEnd(xs: List): xs is (List & { item: T, next: List }) { return xs.item !== null; } -export class ArrayList implements List { +export class ArrayList implements List { readonly items: Array; readonly index: number = 0; - constructor(items: Array, index = 0) { + constructor(items: Array, public context: C, index = 0) { this.items = items; this.index = index; } @@ -29,9 +30,9 @@ export class ArrayList implements List { return this.items[this.index] ?? null; } - get next(): List | null { + get next(): List | 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 { @@ -39,7 +40,7 @@ export class ArrayList implements List { } [Symbol.iterator](): Iterator { - let i: List = this; + let i: List = this; return { next(): IteratorResult { if (notAtEnd(i)) { diff --git a/packages/compiler/src/syntax/matcher.ts b/packages/compiler/src/syntax/matcher.ts index 67665fe..e8a5e6e 100644 --- a/packages/compiler/src/syntax/matcher.ts +++ b/packages/compiler/src/syntax/matcher.ts @@ -11,26 +11,28 @@ import { List, ArrayList, atEnd, notAtEnd } from './list'; //--------------------------------------------------------------------------- // Patterns over Item -export type PatternResult = [T, List] | null; -export type Pattern = (i: List) => PatternResult; +export type ItemContext = string /* the opener of the containing group, if any */ | null; +export type ItemList = List; +export type PatternResult = [T, ItemList] | null; +export type Pattern = (i: ItemList) => PatternResult; export type PatternTypeArg

= P extends Pattern ? T : never; -export function match(p: Pattern, items: Items, failure: F): T | F { - const r = p(new ArrayList(items)); +export function match(p: Pattern, 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([]); +export const noItems = (c: ItemContext) => new ArrayList([], c); export const fail: Pattern = _i => null; export function succeed(t: T): Pattern { return i => [t, i]; } -export const discard: Pattern = _i => [void 0, noItems]; -export const rest: Pattern = i => [i.toArray(), noItems]; -export const end: Pattern = i => atEnd(skipSpace(i)) ? [void 0, noItems] : null; +export const discard: Pattern = i => [void 0, noItems(i.context)]; +export const rest: Pattern = i => [i.toArray(), noItems(i.context)]; +export const end: Pattern = i => atEnd(skipSpace(i)) ? [void 0, noItems(i.context)] : null; export const pos: Pattern = i => notAtEnd(i) ? [i.item.start, i] : null; export const newline: Pattern = i => { @@ -39,12 +41,12 @@ export const newline: Pattern = i => { return [i.item, i.next]; }; -export function skipSpace(i: List): List { +export function skipSpace(i: ItemList): ItemList { while (notAtEnd(i) && isSpace(i.item)) i = i.next; return i; } -export function collectSpace(i: List, acc: Array): List { +export function collectSpace(i: ItemList, acc: Array): ItemList { while (notAtEnd(i) && isSpace(i.item)) { acc.push(i.item); i = i.next; @@ -133,7 +135,7 @@ export function bind(target: T, key: K, pattern: Pattern) => void): Pattern { +export function exec(thunk: (i: ItemList) => void): Pattern { return i => { thunk(i); return [void 0, i]; @@ -174,7 +176,7 @@ export function group(opener: string, items: Pattern, 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): Pattern { export function separatedBy(itemPattern: Pattern, separator: Pattern): Pattern { 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(itemPattern: Pattern, separator: Pattern) { 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( ): Pattern { 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( { 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(p: Pattern): Pattern { export function replace( items: Items, + outerContext: ItemContext, p: Pattern, 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 = 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( 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); } diff --git a/packages/compiler/src/syntax/template.ts b/packages/compiler/src/syntax/template.ts index 4eaa6b3..dabd3fc 100644 --- a/packages/compiler/src/syntax/template.ts +++ b/packages/compiler/src/syntax/template.ts @@ -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)); }; diff --git a/packages/compiler/test/compiler.test.ts b/packages/compiler/test/compiler.test.ts index 1f1de12..b74f0b3 100644 --- a/packages/compiler/test/compiler.test.ts +++ b/packages/compiler/test/compiler.test.ts @@ -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, () => { diff --git a/packages/compiler/test/grammar.test.ts b/packages/compiler/test/grammar.test.ts new file mode 100644 index 0000000..6ff8fda --- /dev/null +++ b/packages/compiler/test/grammar.test.ts @@ -0,0 +1,30 @@ +/// SPDX-License-Identifier: GPL-3.0-or-later +/// SPDX-FileCopyrightText: Copyright © 2024 Tony Garnock-Jones + +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', '']); + }); +});