Skip to content

Commit

Permalink
feat(parse): add/update/rename parser primitives
Browse files Browse the repository at this point in the history
- add LitParser type to annotate single-char parsers
- add satisfyD()
- add stringOf() for predicated strings
- add wordBoundary anchor
- add/update/rename discarding parser prims:
  - litD(), stringD(), noneOfD(), oneOfD(), rangeD()
- export predicate versions:
  - litP(), noneOfP(), oneOfP(), rangeP()
- update skipWhile() behavior
  • Loading branch information
postspectacular committed Apr 19, 2020
1 parent d47c0a2 commit 328103f
Show file tree
Hide file tree
Showing 10 changed files with 177 additions and 66 deletions.
3 changes: 2 additions & 1 deletion packages/parse/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"dependencies": {
"@thi.ng/api": "^6.10.0",
"@thi.ng/checks": "^2.6.2",
"@thi.ng/errors": "^1.2.10"
"@thi.ng/errors": "^1.2.10",
"@thi.ng/strings": "^1.8.3"
},
"files": [
"*.js",
Expand Down
4 changes: 4 additions & 0 deletions packages/parse/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,16 @@ export interface IReader<T> {

export type Parser<T> = Fn<ParseContext<T>, boolean>;

export type LitParser<T> = Parser<T> & { __lit: true };

export type DynamicParser<T> = Parser<T> & {
set: Fn<Parser<T>, void>;
};

export type PassValue<T> = T | Fn0<T>;

export type CharSet = IObjectOf<boolean>;

export type ScopeTransform<T> = (
scope: Nullable<ParseScope<T>>,
ctx: ParseContext<T>,
Expand Down
21 changes: 16 additions & 5 deletions packages/parse/src/prims/anchor.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import type { Fn2, Nullable } from "@thi.ng/api";
import type { Nullable, Predicate2 } from "@thi.ng/api";
import { ALPHA_NUM } from "@thi.ng/strings";
import type { Parser } from "../api";

export const anchor = <T>(
fn: Fn2<Nullable<T>, Nullable<T>, boolean>
): Parser<T> => (ctx) => {
export const anchor = <T>(fn: Predicate2<Nullable<T>>): Parser<T> => (ctx) => {
const state = ctx.state;
return fn(state.last, state.done ? null : ctx.reader.read(state));
};

export const inputStart: Parser<any> = (ctx) => ctx.state.last == null;

export const inputEnd: Parser<any> = (ctx) =>
ctx.state.done || !ctx.reader.read(ctx.state);
ctx.state.done || ctx.reader.read(ctx.state) === undefined;

export const lineStart: Parser<string> = (ctx) => {
const l = ctx.state.last;
Expand All @@ -23,3 +22,15 @@ export const lineEnd: Parser<string> = (ctx) => {
let c: string;
return state.done || (c = ctx.reader.read(state)) === "\n" || c === "\r";
};

export const wordBoundaryP: Predicate2<Nullable<string>> = (prev, next) => {
return prev
? next
? ALPHA_NUM[prev] && !ALPHA_NUM[next]
: ALPHA_NUM[prev]
: next
? ALPHA_NUM[next]
: false;
};

export const wordBoundary = anchor(wordBoundaryP);
9 changes: 5 additions & 4 deletions packages/parse/src/prims/lit.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { discard } from "../combinators/discard";
import { satisfy } from "./satisfy";
import { satisfyD, satisfy } from "./satisfy";

export const litP = <T>(c: T) => (x: T) => x === c;

/**
* Matches single char/value `c`.
*
* @param c
* @param id
*/
export const lit = <T>(c: T, id = "lit") => satisfy<T>((x) => x === c, id);
export const lit = <T>(c: T, id = "lit") => satisfy<T>(litP(c), id);

/**
* Discarded literal. Same as {@link lit}, but result will be discarded.
*
* @param c
*/
export const dlit = <T>(c: T) => discard(lit(c));
export const litD = <T>(c: T) => satisfyD<T>(litP(c));
38 changes: 25 additions & 13 deletions packages/parse/src/prims/none-of.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
import { isSet } from "@thi.ng/checks";
import type { Parser } from "../api";
import { satisfy } from "./satisfy";
import type { Predicate } from "@thi.ng/api";
import { isPlainObject, isSet } from "@thi.ng/checks";
import type { CharSet, LitParser } from "../api";
import { satisfy, satisfyD } from "./satisfy";

export function noneOf(opts: string, id?: string): Parser<string>;
export function noneOf<T>(opts: T[], id?: string): Parser<T>;
export function noneOf<T>(opts: Set<T>, id?: string): Parser<T>;
export const noneOfP = (
opts: string | CharSet | any[] | Set<any>
): Predicate<any> =>
isSet(opts)
? (x) => !opts.has(x)
: isPlainObject(opts)
? (x) => !(<any>opts)[x]
: (x) => opts.indexOf(x) < 0;

export function noneOf(opts: string | CharSet, id?: string): LitParser<string>;
export function noneOf<T>(opts: T[] | Set<T>, id?: string): LitParser<T>;
export function noneOf(
opts: string | any[] | Set<any>,
id = "oneOf"
): Parser<any> {
return satisfy(
isSet(opts) ? (x) => !opts.has(x) : (x) => opts.indexOf(x) < 0,
id
);
opts: string | CharSet | any[] | Set<any>,
id = "noneOf"
) {
return satisfy(noneOfP(opts), id);
}

export function noneOfD(opts: string | CharSet): LitParser<string>;
export function noneOfD<T>(opts: T[] | Set<T>): LitParser<T>;
export function noneOfD(opts: string | CharSet | any[] | Set<any>) {
return satisfyD(noneOfP(opts));
}
37 changes: 23 additions & 14 deletions packages/parse/src/prims/one-of.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { isSet } from "@thi.ng/checks";
import type { Parser } from "../api";
import { satisfy } from "./satisfy";
import type { Predicate } from "@thi.ng/api";
import { isPlainObject, isSet } from "@thi.ng/checks";
import type { CharSet, LitParser } from "../api";
import { satisfy, satisfyD } from "./satisfy";

export function oneOf(opts: string, id?: string): Parser<string>;
export function oneOf<T>(opts: T[], id?: string): Parser<T>;
export function oneOf<T>(opts: Set<T>, id?: string): Parser<T>;
export function oneOf(
opts: string | any[] | Set<any>,
id = "oneOf"
): Parser<any> {
return satisfy(
isSet(opts) ? (x) => opts.has(x) : (x) => opts.indexOf(x) >= 0,
id
);
export const oneOfP = (
opts: string | any[] | Set<any> | CharSet
): Predicate<any> =>
isSet(opts)
? (x) => opts.has(x)
: isPlainObject(opts)
? (x) => (<any>opts)[x]
: (x) => opts.indexOf(x) >= 0;

export function oneOf(opts: string | CharSet, id?: string): LitParser<string>;
export function oneOf<T>(opts: T[] | Set<T>, id?: string): LitParser<T>;
export function oneOf(opts: string | CharSet | any[] | Set<any>, id = "oneOf") {
return satisfy(oneOfP(opts), id);
}

export function oneOfD(opts: string | CharSet): LitParser<string>;
export function oneOfD<T>(opts: T[] | Set<T>): LitParser<T>;
export function oneOfD(opts: string | CharSet | any[] | Set<any>) {
return satisfyD(oneOfP(opts));
}
44 changes: 36 additions & 8 deletions packages/parse/src/prims/range.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
import type { NumOrString } from "@thi.ng/api";
import { satisfy } from "./satisfy";
import type { NumOrString, Predicate } from "@thi.ng/api";
import { LitParser } from "../api";
import { satisfy, satisfyD } from "./satisfy";

export const range = <T extends NumOrString>(min: T, max: T, id = "lit") =>
satisfy<T>((x) => x >= min && x <= max, id);
export const rangeP = <T extends NumOrString>(min: T, max: T): Predicate<T> => (
x
) => x >= min && x <= max;

export const utf16RangeP = (min: number, max: number): Predicate<string> => (
x
) => {
const c = x.charCodeAt(0)!;
return c >= min && c <= max;
};

export function range(min: string, max: string, id?: string): LitParser<string>;
export function range(min: number, max: number, id?: string): LitParser<number>;
export function range(min: NumOrString, max: NumOrString, id = "lit") {
return satisfy(rangeP(min, max), id);
}

export function rangeD(min: string, max: string): LitParser<string>;
export function rangeD(min: number, max: number): LitParser<number>;
export function rangeD(min: NumOrString, max: NumOrString) {
return satisfyD(rangeP(min, max));
}

/**
* Matches single char in given UTF-16 code range.
Expand All @@ -12,7 +33,14 @@ export const range = <T extends NumOrString>(min: T, max: T, id = "lit") =>
* @param id
*/
export const utf16Range = (min: number, max: number, id = "utfLit") =>
satisfy<string>((x) => {
const c = x.charCodeAt(0)!;
return c >= min && c <= max;
}, id);
satisfy(utf16RangeP(min, max), id);

/**
* Matches single char in given UTF-16 code range.
*
* @param min
* @param max
* @param id
*/
export const utf16RangeD = (min: number, max: number) =>
satisfyD(utf16RangeP(min, max));
33 changes: 25 additions & 8 deletions packages/parse/src/prims/satisfy.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
import type { Predicate } from "@thi.ng/api";
import type { Parser } from "../api";
import type { Parser, LitParser } from "../api";

export const satisfy = <T>(fn: Predicate<T>, id = "lit"): Parser<T> => (
ctx
) => {
if (ctx.done) return false;
const r = ctx.reader.read(ctx.state);
return fn(r) ? ctx.addChild(id, r, true) : false;
};
export function satisfy<T>(pred: Predicate<T>, id = "lit") {
const parser: Parser<T> = (ctx) => {
if (ctx.done) return false;
const r = ctx.reader.read(ctx.state);
return pred(r) ? ctx.addChild(id, r, true) : false;
};
return <LitParser<T>>parser;
}

/**
* Like {@link satisfy}, but avoids creating AST node and discards
* result.
*
* @param pred
*/
export function satisfyD<T>(pred: Predicate<T>) {
const parser: Parser<T> = (ctx) => {
if (ctx.done) return false;
const state = ctx.state!;
const reader = ctx.reader;
return pred(reader.read(state)) ? (reader.next(state), true) : false;
};
return <LitParser<T>>parser;
}
19 changes: 8 additions & 11 deletions packages/parse/src/prims/skip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ import type { Parser } from "../api";
import type { Predicate } from "@thi.ng/api";

/**
* Consumes input, but ignores it as long as given `pred` fn returns
* true. The char for which `pred` fails will NOT be consumed and the
* context state will be forwarded to that position. If `pred` never
* returns false before the end of the input is reached, this parser
* will return false and context state remains untouched.
* Consumes input, but ignores it as long as given `pred` predicate fn
* returns true. The char for which `pred` fails will NOT be consumed
* and the context state will be forwarded to that position. If the end
* of the input is reached, this parser will return true.
*
* @example
* ```ts
* const comment = dseq([lit("#"), skipWhile((x) => x !== "\n"), NL]);
* const comment = seqD([litD("#"), skipWhile(noneOfP("\n")), NL]);
*
* const ctx = defContext("# ignore more!\n");
* comment(ctx);
Expand All @@ -26,11 +25,9 @@ export const skipWhile = <T>(pred: Predicate<T>): Parser<T> => (ctx) => {
const state = { ...ctx.state };
const reader = ctx.reader;
while (!state.done) {
if (!pred(reader.read(state))) {
ctx.state = state;
return true;
}
if (!pred(reader.read(state))) break;
reader.next(state);
}
return false;
ctx.state = state;
return true;
};
35 changes: 33 additions & 2 deletions packages/parse/src/prims/string.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Parser } from "../api";
import { discard } from "../combinators/discard";
import { Predicate, Fn } from "@thi.ng/api";

export const string = <T>(str: ArrayLike<T>, id = "string"): Parser<T> => (
ctx
Expand All @@ -20,4 +20,35 @@ export const string = <T>(str: ArrayLike<T>, id = "string"): Parser<T> => (
return ctx.end();
};

export const dstring = <T>(str: ArrayLike<T>) => discard(string(str));
export const stringD = <T>(str: ArrayLike<T>): Parser<T> => (ctx) => {
if (ctx.done) return false;
const state = { ...ctx.state! };
const reader = ctx.reader;
for (let i = 0, n = str.length; i < n; i++) {
if (state.done) return false;
const r = reader.read(state);
if (r !== str[i]) {
return false;
}
reader.next(state);
}
ctx.state = state;
return true;
};

export const stringOf = <T>(
pred: Predicate<T>,
id = "string",
reduce: Fn<T[], any> = (x) => x.join("")
): Parser<T> => (ctx) => {
const state = { ...ctx.state };
const reader = ctx.reader;
let acc: T[] = [];
while (!state.done) {
const r = reader.read(state);
if (!pred(r)) break;
acc.push(r);
reader.next(state);
}
return ctx.addChild(id, reduce(acc), state);
};

0 comments on commit 328103f

Please sign in to comment.