diff --git a/packages/parse/src/api.ts b/packages/parse/src/api.ts index 02c56de696..61a6d5bfb4 100644 --- a/packages/parse/src/api.ts +++ b/packages/parse/src/api.ts @@ -3,7 +3,7 @@ import { ParseContext } from "./context"; export interface ParseScope { id: string; - state: ParseState; + state: Nullable>; children: Nullable[]>; result: any; } diff --git a/packages/parse/src/context.ts b/packages/parse/src/context.ts index 65d7709704..7f683976d5 100644 --- a/packages/parse/src/context.ts +++ b/packages/parse/src/context.ts @@ -1,6 +1,7 @@ +import { isString } from "@thi.ng/checks"; import type { IReader, ParseScope } from "./api"; import { parseError } from "./error"; -import { defStringReader } from "./string-reader"; +import { defStringReader } from "./readers/string-reader"; interface ContextOpts { /** @@ -10,24 +11,34 @@ interface ContextOpts { */ maxDepth: number; /** - * True to enable parser debug output. + * True to enable parser debug output. Will emit details of each + * parse scope. * * @defaultValue false */ debug: boolean; + /** + * True to retain reader state for each AST node. State of root node + * is always available. + * + * @defaultValue false + */ + retain: boolean; } export class ParseContext { - maxDepth: number; - debug: boolean; - protected _scopes: ParseScope[]; protected _curr: ParseScope; + protected _maxDepth: number; + protected _debug: boolean; + protected _retain: boolean; + constructor(public reader: IReader, opts?: Partial) { - opts = { maxDepth: 32, debug: false, ...opts }; - this.maxDepth = opts.maxDepth!; - this.debug = opts.debug!; + opts = { maxDepth: 32, debug: false, retain: false, ...opts }; + this._maxDepth = opts.maxDepth!; + this._debug = opts.debug!; + this._retain = opts.retain!; this._curr = { id: "root", state: { p: 0, l: 1, c: 1 }, @@ -35,24 +46,24 @@ export class ParseContext { result: null, }; this._scopes = [this._curr]; - reader.isDone(this._curr.state); + reader.isDone(this._curr.state!); } start(id: string) { - if (this._scopes.length >= this.maxDepth) { - parseError(this, `recursion limit reached ${this.maxDepth}`); + if (this._scopes.length >= this._maxDepth) { + parseError(this, `recursion limit reached ${this._maxDepth}`); } const scopes = this._scopes; const scope: ParseScope = { id, - state: { ...scopes[scopes.length - 1].state }, + state: { ...scopes[scopes.length - 1].state! }, children: null, result: null, }; scopes.push(scope); - if (this.debug) { + if (this._debug) { console.log( - `${" ".repeat(scopes.length)}start: ${id} (${scope.state.p})` + `${" ".repeat(scopes.length)}start: ${id} (${scope.state!.p})` ); } return (this._curr = scope); @@ -62,7 +73,7 @@ export class ParseContext { const scopes = this._scopes; const child = scopes.pop()!; this._curr = scopes[scopes.length - 1]; - if (this.debug) { + if (this._debug) { console.log(`${" ".repeat(scopes.length + 1)}discard: ${child.id}`); } return false; @@ -74,12 +85,16 @@ export class ParseContext { const parent = scopes[scopes.length - 1]; const cstate = child.state; const pstate = parent.state; - if (this.debug) { + if (this._debug) { console.log( - `${" ".repeat(scopes.length + 1)}end: ${child.id} (${cstate.p})` + `${" ".repeat(scopes.length + 1)}end: ${child.id} (${ + cstate!.p + })` ); } - child.state = { p: pstate.p, l: pstate.l, c: pstate.c }; + child.state = this._retain + ? { p: pstate!.p, l: pstate!.l, c: pstate!.c } + : null; parent.state = cstate; const children = parent.children; children ? children.push(child) : (parent.children = [child]); @@ -92,7 +107,9 @@ export class ParseContext { const cstate = curr.state; const child: ParseScope = { id, - state: { p: cstate.p, l: cstate.l, c: cstate.c }, + state: this._retain + ? { p: cstate!.p, l: cstate!.l, c: cstate!.c } + : null, children: null, result, }; @@ -110,7 +127,7 @@ export class ParseContext { } get done() { - return this._curr.state.done; + return this._curr.state!.done; } /** @@ -138,10 +155,26 @@ export class ParseContext { } /** - * Creates new {@link ParseContext} for given input string and context options. + * Creates new {@link ParseContext} for given input string or reader and + * context options. * * @param input - * @param opts - */ -export const defContext = (input: string, opts?: Partial) => - new ParseContext(defStringReader(input), opts); +export function defContext( + input: string, + opts?: Partial +): ParseContext; +export function defContext( + input: IReader, + opts?: Partial +): ParseContext; +export function defContext( + input: string | IReader, + opts?: Partial +): ParseContext { + return new ParseContext( + isString(input) ? defStringReader(input) : input, + opts + ); +} diff --git a/packages/parse/src/error.ts b/packages/parse/src/error.ts index 44a39dcb1a..0fc7875635 100644 --- a/packages/parse/src/error.ts +++ b/packages/parse/src/error.ts @@ -4,6 +4,6 @@ import { ParseContext } from "./context"; const ParseError = defError(() => `ParseError`); export const parseError = (ctx: ParseContext, msg: string): never => { - const info = ctx.reader.format(ctx.scope.state); + const info = ctx.reader.format(ctx.scope.state!); throw new ParseError(msg + (info ? ` @ ${info}` : "")); }; diff --git a/packages/parse/src/prims/anchor.ts b/packages/parse/src/prims/anchor.ts index 6714473b37..aae5cca3e3 100644 --- a/packages/parse/src/prims/anchor.ts +++ b/packages/parse/src/prims/anchor.ts @@ -4,21 +4,21 @@ import type { Parser } from "../api"; export const anchor = ( fn: Fn2, Nullable, boolean> ): Parser => (ctx) => { - const state = ctx.state; + const state = ctx.state!; return fn(state.last, state.done ? null : ctx.reader.read(state)); }; -export const inputStart: Parser = (ctx) => ctx.state.last == null; +export const inputStart: Parser = (ctx) => ctx.state!.last == null; export const inputEnd: Parser = (ctx) => - ctx.state.done || !ctx.reader.read(ctx.state); + ctx.state!.done || !ctx.reader.read(ctx.state!); export const lineStart: Parser = (ctx) => { - const l = ctx.state.last; + const l = ctx.state!.last; return l == null || l === "\n"; }; export const lineEnd: Parser = (ctx) => { - const state = ctx.state; + const state = ctx.state!; return state.done || ctx.reader.read(state) === "\n"; }; diff --git a/packages/parse/src/prims/range.ts b/packages/parse/src/prims/range.ts index da5006aa08..b69665e244 100644 --- a/packages/parse/src/prims/range.ts +++ b/packages/parse/src/prims/range.ts @@ -3,3 +3,16 @@ import { satisfy } from "./satisfy"; export const range = (min: T, max: T, id = "lit") => satisfy((x) => x >= min && x <= max, id); + +/** + * Matches single char in given UTF-16 code range. + * + * @param min + * @param max + * @param id + */ +export const utf16Range = (min: number, max: number, id = "utfLit") => + satisfy((x) => { + const c = x.charCodeAt(0)!; + return c >= min && c <= max; + }, id); diff --git a/packages/parse/src/prims/satisfy.ts b/packages/parse/src/prims/satisfy.ts index 1107380e63..54b16676ef 100644 --- a/packages/parse/src/prims/satisfy.ts +++ b/packages/parse/src/prims/satisfy.ts @@ -6,12 +6,12 @@ export const satisfy = (fn: Predicate, id = "lit"): Parser => ( ) => { if (ctx.done) return false; const reader = ctx.reader; - const r = reader.read(ctx.state); + const r = reader.read(ctx.state!); if (!fn(r)) { return false; } const scope = ctx.start(id); - reader.next(scope.state); + reader.next(scope.state!); scope.result = r; return ctx.end(); }; diff --git a/packages/parse/src/prims/string.ts b/packages/parse/src/prims/string.ts index 991ccd84d9..3c8a420629 100644 --- a/packages/parse/src/prims/string.ts +++ b/packages/parse/src/prims/string.ts @@ -5,7 +5,7 @@ export const string = (str: ArrayLike, id = "string"): Parser => ( ) => { if (ctx.done) return false; const scope = ctx.start(id); - const state = scope.state; + const state = scope.state!; const reader = ctx.reader; for (let i = 0, n = str.length; i < n; i++) { if (state.done) return false; diff --git a/packages/parse/src/xform/print.ts b/packages/parse/src/xform/print.ts index a18eea3583..0b93bc8bcf 100644 --- a/packages/parse/src/xform/print.ts +++ b/packages/parse/src/xform/print.ts @@ -3,11 +3,9 @@ import { ScopeTransform } from "../api"; export const xfPrint: ScopeTransform = (scope, _, indent = 0) => { if (!scope) return; const prefix = indent > 0 ? " ".repeat(indent) : ""; - console.log( - `${prefix}${scope.id} (${scope.state.l}:${ - scope.state.c - }): ${JSON.stringify(scope.result)}` - ); + const state = scope.state; + const info = state ? ` (${state.l}:${state.c})` : ""; + console.log(`${prefix}${scope.id}${info}: ${JSON.stringify(scope.result)}`); if (scope.children) { for (let c of scope.children) { xfPrint(c, _, indent + 1); diff --git a/packages/parse/test/index.ts b/packages/parse/test/index.ts index 469762ba54..a889b806b4 100644 --- a/packages/parse/test/index.ts +++ b/packages/parse/test/index.ts @@ -17,7 +17,7 @@ const check = ( ) => { const ctx = defContext(src); assert.equal(parser(ctx), res, `src: '${src}'`); - assert.equal(ctx.state.p, pos, `src: '${src}' pos: ${ctx.state.p}`); + assert.equal(ctx.state!.p, pos, `src: '${src}' pos: ${ctx.state!.p}`); }; describe("parse", () => { diff --git a/packages/parse/test/rpn.ts b/packages/parse/test/rpn.ts index adcd207569..9ba4bcc4be 100644 --- a/packages/parse/test/rpn.ts +++ b/packages/parse/test/rpn.ts @@ -1,6 +1,6 @@ import { Fn2 } from "@thi.ng/api"; import * as assert from "assert"; -import { alt, defContext, INT, oneOf, WS, xform, zeroOrMore } from "../src"; +import { alt, defContext, FLOAT, oneOf, WS_0, xform, zeroOrMore } from "../src"; describe("parse", () => { it("RPN calc", () => { @@ -11,7 +11,7 @@ describe("parse", () => { "*": (a, b) => a * b, "/": (a, b) => a / b, }; - const value = xform(INT, (scope) => { + const value = xform(FLOAT, (scope) => { stack.push(scope!.result); return null; }); @@ -21,7 +21,7 @@ describe("parse", () => { stack.push(ops[scope!.result](a, b)); return null; }); - const program = zeroOrMore(alt([value, op, zeroOrMore(WS)])); + const program = zeroOrMore(alt([value, op, WS_0])); const ctx = defContext("10 5 3 * + -2 * 10 /"); assert(program(ctx)); assert(ctx.done); diff --git a/packages/parse/test/svg.ts b/packages/parse/test/svg.ts index ff5a7fa4ab..52a78cd129 100644 --- a/packages/parse/test/svg.ts +++ b/packages/parse/test/svg.ts @@ -8,7 +8,7 @@ import { oneOf, Parser, seq, - WS, + WS_0, xform, zeroOrMore, } from "../src"; @@ -21,18 +21,17 @@ const check = ( ) => { const ctx = defContext(src); assert.equal(parser(ctx), res, `src: '${src}'`); - assert.equal(ctx.state.p, pos, `src: '${src}' pos: ${ctx.state.p}`); + assert.equal(ctx.state!.p, pos, `src: '${src}' pos: ${ctx.state!.p}`); }; describe("parse", () => { it("SVG", () => { - const ws = discard(zeroOrMore(WS)); const wsc = discard(zeroOrMore(oneOf(" \n,"))); const point = collect(seq([INT, wsc, INT])); - const move = collect(seq([oneOf("Mm"), ws, point, ws])); - const line = collect(seq([oneOf("Ll"), ws, point, ws])); + const move = collect(seq([oneOf("Mm"), WS_0, point, WS_0])); + const line = collect(seq([oneOf("Ll"), WS_0, point, WS_0])); const curve = collect( - seq([oneOf("Cc"), ws, point, wsc, point, wsc, point, ws]) + seq([oneOf("Cc"), WS_0, point, wsc, point, wsc, point, WS_0]) ); const close = xform(oneOf("Zz"), ($) => (($!.result = [$!.result]), $)); const path = collect(zeroOrMore(alt([move, line, curve, close])));