Skip to content

Commit

Permalink
perf(parse): update ParseState & ParseScope handling (result: 1.2-1.6…
Browse files Browse the repository at this point in the history
…x faster)

- refactor ParseState/Scope as data classes (keep same structure)
- minor update scope transforms
- update tests
  • Loading branch information
postspectacular committed Aug 29, 2024
1 parent 4dcdff7 commit c94b5cf
Show file tree
Hide file tree
Showing 14 changed files with 80 additions and 81 deletions.
17 changes: 1 addition & 16 deletions packages/parse/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,5 @@
import type { Fn, Fn0, IObjectOf, Nullable } from "@thi.ng/api";
import type { ParseContext } from "./context.js";

export interface ParseScope<T> {
id: string;
state: Nullable<ParseState<T>>;
children: Nullable<ParseScope<T>[]>;
result: any;
}

export interface ParseState<T> {
p: number;
l: number;
c: number;
done?: boolean;
last?: T;
}
import type { ParseContext, ParseScope, ParseState } from "./context.js";

export interface IReader<T> {
read(state: ParseState<T>): T;
Expand Down
3 changes: 2 additions & 1 deletion packages/parse/src/combinators/check.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Predicate } from "@thi.ng/api";
import type { Parser, ParseScope } from "../api.js";
import type { Parser } from "../api.js";
import type { ParseScope } from "../context.js";
import { parseError } from "../error.js";
import { xform } from "./xform.js";

Expand Down
2 changes: 1 addition & 1 deletion packages/parse/src/combinators/lookahead.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export const lookahead =
ctx.start(id);
let pass = false;
while (true) {
const state = capture ? null : { ...ctx.state };
const state = capture ? null : ctx.state.copy();
if (ahead(ctx)) {
!capture && (ctx.state = state!);
return pass ? ctx.end() : ctx.discard();
Expand Down
79 changes: 49 additions & 30 deletions packages/parse/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,44 @@
import type { ICopy, Nullable } from "@thi.ng/api";
import { isArrayLike } from "@thi.ng/checks/is-arraylike";
import { isString } from "@thi.ng/checks/is-string";
import type { ContextOpts, IReader, ParseScope, ParseState } from "./api.js";
import type { ContextOpts, IReader } from "./api.js";
import { parseError } from "./error.js";
import { defArrayReader } from "./readers/array-reader.js";
import { defStringReader } from "./readers/string-reader.js";
import { __indent } from "./utils.js";

export class ParseState<T> implements ICopy<ParseState<T>> {
constructor(
public p: number,
public l: number,
public c: number,
public done?: boolean,
public last?: T
) {}

copy() {
return new ParseState<T>(this.p, this.l, this.c, this.done, this.last);
}
}

export class ParseScope<T> implements ICopy<ParseScope<T>> {
constructor(
public id: string,
public state?: Nullable<ParseState<T>>,
public children?: Nullable<ParseScope<T>[]>,
public result?: any
) {}

copy() {
return new ParseScope<T>(
this.id,
this.state,
this.children,
this.result
);
}
}

export class ParseContext<T> {
public opts: ContextOpts;
protected _scopes!: ParseScope<T>[];
Expand All @@ -25,29 +58,22 @@ export class ParseContext<T> {
}

reset() {
this._curr = {
id: "root",
state: { p: 0, l: 1, c: 1 },
children: null,
result: null,
};
this._curr = new ParseScope("root", new ParseState(0, 1, 1));
this._scopes = [this._curr];
this._peakDepth = 1;
this.reader.isDone(this._curr.state!);
return this;
}

start(id: string) {
if (this._scopes.length >= this._maxDepth) {
parseError(this, `recursion limit reached ${this._maxDepth}`);
const { _scopes: scopes, _maxDepth } = this;
if (scopes.length >= _maxDepth) {
parseError(this, `recursion limit reached ${_maxDepth}`);
}
const scopes = this._scopes;
const scope: ParseScope<T> = {
const scope = new ParseScope<T>(
id,
state: { ...scopes[scopes.length - 1].state! },
children: null,
result: null,
};
scopes[scopes.length - 1].state!.copy()
);
scopes.push(scope);
this._peakDepth = Math.max(this._peakDepth, scopes.length);
this._debug &&
Expand All @@ -71,15 +97,11 @@ export class ParseContext<T> {
const child = scopes.pop()!;
const parent = scopes[scopes.length - 1];
const cstate = child.state;
let pstate: ParseState<T>;
this._debug &&
console.log(
`${__indent(scopes.length + 1)}end: ${child.id} (${cstate!.p})`
);
child.state = this._retain
? ((pstate = parent.state!),
{ p: pstate.p, l: pstate.l, c: pstate.c })
: null;
child.state = this._retain ? parent.state!.copy() : null;
parent.state = cstate;
const children = parent.children;
children ? children.push(child) : (parent.children = [child]);
Expand All @@ -93,26 +115,23 @@ export class ParseContext<T> {
newState: ParseState<T> | boolean = false
) {
const curr = this._curr;
const cstate = curr.state;
const child: ParseScope<T> = {
const child = new ParseScope<T>(
id,
state: this._retain
? { p: cstate!.p, l: cstate!.l, c: cstate!.c }
: null,
children: null,
result,
};
this._retain ? curr.state!.copy() : null,
null,
result
);
this._debug &&
console.log(
`${__indent(this._scopes.length + 1)}addChild: ${id} (${
cstate!.p
curr.state!.p
})`
);
const children = curr.children;
children ? children.push(child) : (curr.children = [child]);
if (newState !== false) {
newState === true
? this.reader.next(cstate!)
? this.reader.next(curr.state!)
: (this._curr.state = newState);
}
return true;
Expand Down
3 changes: 1 addition & 2 deletions packages/parse/src/grammar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type {
DynamicParser,
GrammarOpts,
Language,
ParseScope,
Parser,
RuleTransforms,
} from "./api.js";
Expand All @@ -19,7 +18,7 @@ import { not } from "./combinators/not.js";
import { oneOrMore, repeat, zeroOrMore } from "./combinators/repeat.js";
import { seq, seqD } from "./combinators/seq.js";
import { xform } from "./combinators/xform.js";
import { defContext } from "./context.js";
import { defContext, type ParseScope } from "./context.js";
import { ALPHA, ALPHA_NUM } from "./presets/alpha.js";
import { BIT } from "./presets/bits.js";
import { DIGIT } from "./presets/digits.js";
Expand Down
2 changes: 1 addition & 1 deletion packages/parse/src/prims/skip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import type { Parser } from "../api.js";
export const skipWhile =
<T>(pred: Predicate<T>): Parser<T> =>
(ctx) => {
const state = { ...ctx.state };
const state = ctx.state.copy();
const reader = ctx.reader;
while (!state.done) {
if (!pred(reader.read(state))) break;
Expand Down
4 changes: 2 additions & 2 deletions packages/parse/src/prims/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const stringD =
<T>(str: ArrayLike<T>): Parser<T> =>
(ctx) => {
if (ctx.done) return false;
const state = { ...ctx.state! };
const state = ctx.state.copy();
const reader = ctx.reader;
for (let i = 0, n = str.length; i < n; i++) {
if (state.done) return false;
Expand All @@ -45,7 +45,7 @@ export const stringOf =
reduce: Fn<T[], any> = (x) => x.join("")
): Parser<T> =>
(ctx) => {
const state = { ...ctx.state };
const state = ctx.state.copy();
const reader = ctx.reader;
let acc: T[] = [];
while (!state.done) {
Expand Down
3 changes: 2 additions & 1 deletion packages/parse/src/readers/array-reader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ParseState, IReader } from "../api.js";
import type { IReader } from "../api.js";
import type { ParseState } from "../context.js";

export class ArrayReader<T> implements IReader<T> {
constructor(protected _src: ArrayLike<T>) {}
Expand Down
3 changes: 2 additions & 1 deletion packages/parse/src/readers/string-reader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ParseState, IReader } from "../api.js";
import type { IReader } from "../api.js";
import type { ParseState } from "../context.js";

export class StringReader implements IReader<string> {
constructor(protected _src: string) {}
Expand Down
2 changes: 1 addition & 1 deletion packages/parse/src/xform/count.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { xform } from "../combinators/xform.js";
* @param scope -
*/
export const xfCount: ScopeTransform<any> = (scope) => {
scope!.result = scope!.children ? scope!.children.length : 0;
scope!.result = scope!.children?.length || 0;
scope!.children = null;
return scope;
};
Expand Down
3 changes: 2 additions & 1 deletion packages/parse/src/xform/join.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Nullable } from "@thi.ng/api";
import type { Parser, ParseScope } from "../api.js";
import type { Parser } from "../api.js";
import { xform } from "../combinators/xform.js";
import type { ParseScope } from "../context.js";

/**
* Recursively joins non-null results of all children into a single
Expand Down
4 changes: 2 additions & 2 deletions packages/parse/src/xform/nest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { xfJoin } from "./join.js";
*
* @remarks
* The nested parser is applied to a separate {@link ParseContext} and if
* successful, the resulting AST will be transplated into the current parse
* successful, the resulting AST will be transplanted into the current parse
* scope. If the nested parser fails, the scope will remain untouched.
*
* If the current parse context retains line/column details, the inner parse
Expand All @@ -22,7 +22,7 @@ export const xfNest =
(parser: Parser<string>): ScopeTransform<string> =>
(scope, ctx) => {
if (!scope) return;
const src = scope.result || xfJoin({ ...scope })!.result;
const src = scope.result || xfJoin(scope.copy())!.result;
const inner = defContext(src, ctx.opts);
const state = scope.state;
if (state) {
Expand Down
21 changes: 11 additions & 10 deletions packages/parse/test/grammar.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { assert } from "@thi.ng/errors";
import { expect, test } from "bun:test";
import { defContext, defGrammar, type Parser } from "../src/index.js";
import {
defContext,
defGrammar,
ParseScope,
ParseState,
type Parser,
} from "../src/index.js";

const check = (
parser: Parser<string>,
Expand Down Expand Up @@ -81,14 +87,9 @@ test("rule ref xform", () => {
const ctx = defContext("abc,def,g,hij,", { retain: true });
expect(lang!.rules.b(ctx)).toBeTrue();
expect(ctx.children).toEqual([
{ id: "a", state: { p: 0, l: 1, c: 1 }, children: null, result: "abc" },
{ id: "a", state: { p: 4, l: 1, c: 5 }, children: null, result: "def" },
{ id: "a", state: { p: 8, l: 1, c: 9 }, children: null, result: "g" },
{
id: "a",
state: { p: 10, l: 1, c: 11 },
children: null,
result: "hij",
},
new ParseScope("a", new ParseState(0, 1, 1, false), null, "abc"),
new ParseScope("a", new ParseState(4, 1, 5, false, ","), null, "def"),
new ParseScope("a", new ParseState(8, 1, 9, false, ","), null, "g"),
new ParseScope("a", new ParseState(10, 1, 11, false, ","), null, "hij"),
]);
});
15 changes: 3 additions & 12 deletions packages/parse/test/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { assert } from "@thi.ng/errors";
import { expect, test } from "bun:test";
import {
DIGIT,
ParseState,
WS,
defContext,
oneOrMore,
Expand All @@ -22,18 +23,8 @@ const check = (
};

test("initial ctx", () => {
expect(defContext("").state).toEqual({
p: 0,
l: 1,
c: 1,
done: true,
});
expect(defContext(" ").state).toEqual({
p: 0,
l: 1,
c: 1,
done: false,
});
expect(defContext("").state).toEqual(new ParseState(0, 1, 1, true));
expect(defContext(" ").state).toEqual(new ParseState(0, 1, 1, false));
});

test("zeroOrMore", () => {
Expand Down

0 comments on commit c94b5cf

Please sign in to comment.