From aad2c4db07803eab5622dfc6188ba5c86383467c Mon Sep 17 00:00:00 2001 From: uzmoi Date: Tue, 24 Oct 2023 23:48:13 +0900 Subject: [PATCH] add source type parameter --- examples/json.ts | 6 ++-- examples/parser-by-do.ts | 38 ++++++++++++----------- examples/script.ts | 27 ++++++++++++----- src/combinator.ts | 34 ++++++++++----------- src/context.ts | 4 +-- src/do.ts | 12 ++++---- src/error.ts | 6 ++-- src/parser.ts | 65 +++++++++++++++++++++++----------------- src/primitive.ts | 30 +++++++++---------- src/string.ts | 19 +++++++----- 10 files changed, 134 insertions(+), 107 deletions(-) diff --git a/examples/json.ts b/examples/json.ts index bd0a498..0115c61 100644 --- a/examples/json.ts +++ b/examples/json.ts @@ -1,8 +1,8 @@ import type { JsonValue } from "emnorst"; import { EOI, choice, el, lazy, literal, many, qo, regex, type Parser } from "../src"; -const sepBy = (parser: Parser, sep: Parser) => - qo(perform => { +const sepBy = (parser: Parser, sep: Parser) => + qo(perform => { const head = perform(parser); const rest = perform(sep.then(parser).apply(many)); return [head, ...rest]; @@ -12,7 +12,7 @@ const sepBy = (parser: Parser, sep: Parser) => const ws = regex(/[ \n\r\t]*/); -const jsonValue: Parser = lazy(() => +const jsonValue: Parser = lazy(() => choice([ object, array, diff --git a/examples/parser-by-do.ts b/examples/parser-by-do.ts index d6b6a58..e805d99 100644 --- a/examples/parser-by-do.ts +++ b/examples/parser-by-do.ts @@ -4,50 +4,52 @@ import { qo, type Config, type Parser } from "../src"; const pure = (value: T) => qo(() => value); -const map = (parser: Parser, f: (value: T, config: Config) => U) => - qo((perform, config) => f(perform(parser), config)); +const map = (parser: Parser, f: (value: T, config: Config) => U) => + qo((perform, config) => f(perform(parser), config)); -const flatMap = (parser: Parser, f: (value: T, config: Config) => Parser) => - qo((perform, config) => perform(f(perform(parser), config))); +const flatMap = ( + parser: Parser, + f: (value: T, config: Config) => Parser, +) => qo((perform, config) => perform(f(perform(parser), config))); -const and = (left: Parser, right: Parser) => - qo(perform => (perform(left), perform(right))); +const and = (left: Parser, right: Parser) => + qo(perform => (perform(left), perform(right))); -const skip = (left: Parser, right: Parser) => - qo(perform => { +const skip = (left: Parser, right: Parser) => + qo(perform => { const leftValue = perform(left); perform(right); return leftValue; }); -const between = (parser: Parser, pre: Parser, post = pre): Parser => - qo(perform => { +const between = (parser: Parser, pre: Parser, post = pre) => + qo(perform => { perform(pre); const value = perform(parser); perform(post); return value; }); -const or = (left: Parser, right: Parser) => - qo(perform => { +const or = (left: Parser, right: Parser) => + qo(perform => { const leftResult = perform.try(() => ({ value: perform(left), })); return leftResult ? leftResult.value : perform(right); }); -const option = (parser: Parser, value: U) => - qo(perform => { +const option = (parser: Parser, value: U) => + qo(perform => { const result = perform.try(() => ({ value: perform(parser), })); return result ? result.value : value; }); -const seq = ( - parsers: readonly Parser[], +const seq = ( + parsers: readonly Parser[], options?: { allowPartial?: boolean }, -): Parser => +): Parser => qo(perform => { const accum: T[] = []; const fullSeq = () => { @@ -63,7 +65,7 @@ const seq = ( return accum; }); -const many = (parser: Parser): Parser => +const many = (parser: Parser): Parser => qo(perform => { const xs: T[] = []; perform.try(() => { diff --git a/examples/script.ts b/examples/script.ts index 95e052a..0f1f8c3 100644 --- a/examples/script.ts +++ b/examples/script.ts @@ -11,11 +11,14 @@ export type Expr = | { type: "Call"; callee: Expr; arguments: readonly Expr[] } | { type: "Property"; target: Expr; name: string }; -export const expr: P.Parser = P.lazy(() => +export const expr: P.Parser = P.lazy(() => P.choice([Bool, Number, String, Tuple, Block, If, Ident]).between(ws).flatMap(tail), ); -const sepBy = (parser: P.Parser, sep: P.Parser): P.Parser => { +const sepBy = ( + parser: P.Parser, + sep: P.Parser, +): P.Parser => { return P.qo(perform => { const xs: T[] = []; perform.try(() => { @@ -30,7 +33,7 @@ const sepBy = (parser: P.Parser, sep: P.Parser): P.Parser => { const ws = P.regex(/\s*/); -const keyword = (keyword: string): P.Parser => { +const keyword = (keyword: string): P.Parser => { return P.literal(keyword).then(P.notFollowedBy(P.regex(/\w/))); }; @@ -78,7 +81,14 @@ const Break = keyword("break").return({ type: "Break" }).skip(ws); const Expr = expr.map(expr => ({ type: "Expr", expr })); -export const stat: P.Parser = P.choice([Let, DefFn, Return, While, Break, Expr]) +export const stat: P.Parser = P.choice([ + Let, + DefFn, + Return, + While, + Break, + Expr, +]) .skip(P.el(";")) .between(ws); @@ -89,7 +99,7 @@ const Bool = P.choice([ const digit = P.oneOf("0123456789"); const digits = digit.apply( - P.manyAccum, + P.manyAccum, (accum, digit) => accum + digit, () => "", { min: 1 }, @@ -140,8 +150,11 @@ const If = P.seq([ const tail = (expr: Expr) => P.choice([Call, Property]) .skip(ws) - .apply(P.many) - .map(tails => tails.reduce((expr, tail) => tail(expr), expr)); + .apply( + P.manyAccum<(callee: Expr) => Expr, Expr, string>, + (expr, tail) => tail(expr), + () => expr, + ); const Call = Tuple.map<(callee: Expr) => Expr>(({ elements }) => callee => ({ type: "Call", diff --git a/src/combinator.ts b/src/combinator.ts index b5cf83e..b52205f 100644 --- a/src/combinator.ts +++ b/src/combinator.ts @@ -1,13 +1,13 @@ import { MAX_INT32, clamp } from "emnorst"; import type { Config } from "."; -import { Parser, type Parsed } from "./parser"; +import { Parser, type Parsed, type Source } from "./parser"; import { updateState, type ParseState } from "./state"; /** * Delays variable references until the parser runs. */ -export const lazy = (getParser: () => Parser): Parser => { - let parser: Parser; +export const lazy = (getParser: () => Parser): Parser => { + let parser: Parser; return new Parser((state, context) => { if (parser == null) { parser = getParser(); @@ -16,7 +16,7 @@ export const lazy = (getParser: () => Parser): Parser => { }); }; -export const notFollowedBy = (parser: Parser): Parser => +export const notFollowedBy = (parser: Parser): Parser => new Parser((state, context) => { const newState = parser.run(state, context); if (newState == null) { @@ -26,7 +26,7 @@ export const notFollowedBy = (parser: Parser): Parser => return null; }); -export const lookAhead = (parser: Parser): Parser => +export const lookAhead = (parser: Parser): Parser => new Parser((state, context) => { const newState = parser.run(state, context); return newState && updateState(state, newState.v); @@ -40,16 +40,16 @@ export const seq: { ( parsers: T, options?: { allowPartial?: false }, - ): Parser>; + ): Parser, Source>; ( parsers: T, options: { allowPartial: boolean }, - ): Parser>>; + ): Parser>, Source>; } = (parsers, options) => new Parser((state, context) => { const values: unknown[] = []; for (const parser of parsers) { - const newState = parser.run(state, context); + const newState = parser.run(state, context as never); if (newState == null) { if (options?.allowPartial) break; return null; @@ -59,12 +59,12 @@ export const seq: { return updateState(state, values); }); -type Choice = Parser>; +type Choice = Parser, Source>; export const choice = (parsers: T): Choice => new Parser((state, context) => { for (const parser of parsers) { - const newState = parser.run(state, context); + const newState = parser.run(state, context as never); if (newState != null) { return newState as ParseState>; } @@ -72,12 +72,12 @@ export const choice = (parsers: T): Choice return null; }); -export const manyAccum = ( - parser: Parser, +export const manyAccum = ( + parser: Parser, f: (accum: U, cur: T, config: Config) => U | void, init: (config: Config) => U, options?: { min?: number; max?: number }, -): Parser => { +): Parser => { const clampedMin = clamp(options?.min || 0, 0, MAX_INT32) | 0; const clampedMax = clamp(options?.max || MAX_INT32, clampedMin, MAX_INT32) | 0; @@ -95,11 +95,11 @@ export const manyAccum = ( }); }; -export const many = ( - parser: Parser, +export const many = ( + parser: Parser, options?: { min?: number; max?: number }, -): Parser => { - return manyAccum( +): Parser => { + return manyAccum( parser, (array, value) => { array.push(value); diff --git a/src/context.ts b/src/context.ts index 09319ce..26fda7a 100644 --- a/src/context.ts +++ b/src/context.ts @@ -9,8 +9,8 @@ export interface Config { readonly [key: string]: unknown; } -export class Context { - constructor(readonly src: Source, readonly cfg: Config) { +export class Context { + constructor(readonly src: ArrayLike, readonly cfg: Config) { if (!isArrayLike(src)) { throw new TypeError("source is not ArrayLike."); } diff --git a/src/do.ts b/src/do.ts index ec1ad85..fda2efe 100644 --- a/src/do.ts +++ b/src/do.ts @@ -5,14 +5,16 @@ import { updateState } from "./state"; const ParseaDoErrorSymbol = /* #__PURE__ */ Symbol(); -interface Perform { - (parser: Parser): T; +export type Perform = { + (parser: Parser): T; try(runner: () => T, allowPartialCommit?: boolean): T | null; -} +}; -export const qo = (runner: (perform: Perform, config: Config) => T): Parser => +export const qo = ( + runner: (perform: Perform, config: Config) => T, +): Parser => new Parser((state, context) => { - const perform: Perform = parser => { + const perform: Perform = parser => { const newState = parser.run(state, context); if (newState == null) { throw { [ParseaDoErrorSymbol]: null }; diff --git a/src/error.ts b/src/error.ts index 98862fd..f85602c 100644 --- a/src/error.ts +++ b/src/error.ts @@ -1,10 +1,8 @@ -import type { Source } from "./context"; - export type ParseError = - | { type: "Expected"; value: Source } + | { type: "Expected"; value: ArrayLike } | { type: "Label"; name: string; length: number }; -export const expected = (value: Source): ParseError => ({ +export const expected = (value: ArrayLike): ParseError => ({ type: "Expected", value, }); diff --git a/src/parser.ts b/src/parser.ts index 895d7bc..ee0ce81 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,32 +1,38 @@ import { many, manyAccum } from "./combinator"; -import { Context, type Config, type Source } from "./context"; +import { Context, type Config } from "./context"; import * as error from "./error"; import { createParseResult, type ParseResult } from "./result"; import { initState, updateState, type ParseState } from "./state"; export type Parsed = T extends Parser ? U : never; -export type ParseRunner = ( +export type Source = [T] extends [Parser] ? U : never; + +export type ParseRunner = ( this: void, state: ParseState, - context: Context, + context: Context, ) => ParseState | null; -export class Parser { - constructor(readonly run: ParseRunner) {} - parse(this: Parser, source: Source, config: Config = {}): ParseResult { +/** + * @template T result + * @template S source + */ +export class Parser { + constructor(readonly run: ParseRunner) {} + parse(this: this, source: ArrayLike, config: Config = {}): ParseResult { const context = new Context(source, config); const finalState = this.run(initState, context); return createParseResult(finalState, context); } - apply( - this: Parser, - f: (parser: Parser, ...args: A) => Parser, + apply( + this: this, + f: (parser: Parser, ...args: A) => Parser, ...args: A - ): Parser { + ): Parser { return f(this, ...args); } - label(this: Parser, label: string): Parser { + label(this: this, label: string): Parser { return new Parser((state, context) => { const labelStart = context.group(); const newState = this.run(state, context); @@ -36,52 +42,55 @@ export class Parser { return newState; }); } - return(this: Parser, value: U): Parser { + return(this: this, value: U): Parser { return new Parser((state, context) => { const newState = this.run(state, context); return newState && updateState(newState, value); }); } - map(this: Parser, f: (value: T, config: Config) => U): Parser { + map(this: this, f: (value: T, config: Config) => U): Parser { return new Parser((state, context) => { const newState = this.run(state, context); return newState && updateState(newState, f(newState.v, context.cfg)); }); } - flatMap(this: Parser, f: (value: T, config: Config) => Parser): Parser { + flatMap( + this: this, + f: (value: T, config: Config) => Parser, + ): Parser { return new Parser((state, context) => { const newState = this.run(state, context); return newState && f(newState.v, context.cfg).run(newState, context); }); } - then(this: Parser, parser: Parser): Parser { + then(this: this, parser: Parser): Parser { return new Parser((state, context) => { const newState = this.run(state, context); return newState && parser.run(newState, context); }); } - skip(this: Parser, parser: Parser): Parser { + skip(this: this, parser: Parser): Parser { return new Parser((state, context) => { const newStateA = this.run(state, context); const newStateB = newStateA && parser.run(newStateA, context); return newStateB && updateState(newStateB, newStateA.v); }); } - and(this: Parser, parser: Parser): Parser<[T, U]> { + and(this: this, parser: Parser): Parser<[T, U], S & S2> { return this.andMap(parser, (a, b) => [a, b]); } - andMap( - this: Parser, - parser: Parser, + andMap( + this: this, + parser: Parser, zip: (left: T, right: U) => V, - ): Parser { + ): Parser { return new Parser((state, context) => { const newStateA = this.run(state, context); const newStateB = newStateA && parser.run(newStateA, context); return newStateB && updateState(newStateB, zip(newStateA.v, newStateB.v)); }); } - between(this: Parser, pre: Parser, post = pre): Parser { + between(this: this, pre: Parser, post = pre): Parser { return new Parser((state, context) => { const newStateA = pre.run(state, context); const newStateB = newStateA && this.run(newStateA, context); @@ -89,15 +98,15 @@ export class Parser { return newStateC && updateState(newStateC, newStateB.v); }); } - or(this: Parser, parser: Parser): Parser { - return new Parser((state, context) => { + or(this: this, parser: Parser): Parser { + return new Parser((state, context) => { return this.run(state, context) ?? parser.run(state, context); }); } - option(this: Parser): Parser; - option(this: Parser, value: U): Parser; - option(this: Parser, value?: U): Parser { - return new Parser((state, context) => { + option(this: this): Parser; + option(this: this, value: U): Parser; + option(this: this, value?: U): Parser { + return new Parser((state, context) => { return this.run(state, context) ?? updateState(state, value as U); }); } diff --git a/src/primitive.ts b/src/primitive.ts index 8ef2860..22ea391 100644 --- a/src/primitive.ts +++ b/src/primitive.ts @@ -1,5 +1,5 @@ import { equals } from "emnorst"; -import type { Config, Source } from "./context"; +import type { Config } from "./context"; import * as error from "./error"; import { Parser } from "./parser"; import { updateState } from "./state"; @@ -7,10 +7,10 @@ import { updateState } from "./state"; /** * Always succeed with the value of the argument. */ -export const pure = (value: T): Parser => +export const pure = (value: T): Parser => new Parser(state => updateState(state, value)); -export const fail = (): Parser => +export const fail = (): Parser => new Parser((state, context) => { context.addError(state.i); return null; @@ -19,7 +19,7 @@ export const fail = (): Parser => /** * end of input */ -export const EOI = /* #__PURE__ */ new Parser((state, context) => { +export const EOI = /* #__PURE__ */ new Parser((state, context) => { if (state.i < context.src.length) { context.addError(state.i); return null; @@ -33,7 +33,7 @@ export const EOI = /* #__PURE__ */ new Parser((state, context) => { * @example any.parse([someValue]).value === someValue; * @example any.parse([]); // parse fail */ -export const ANY_EL = /* #__PURE__ */ new Parser((state, context) => { +export const ANY_EL = /* #__PURE__ */ new Parser((state, context) => { if (state.i < context.src.length) { return updateState(state, context.src[state.i], 1); } @@ -41,42 +41,42 @@ export const ANY_EL = /* #__PURE__ */ new Parser((state, context) => { return null; }); -export const el = (value: T): Parser => +export const el = (value: T): Parser => satisfy(srcEl => equals(srcEl, value), { error: error.expected( typeof value === "string" && value.length === 1 ? value : [value], ), }); -export const oneOf = (values: Iterable): Parser => { +export const oneOf = (values: Iterable): Parser => { const set = new Set(values); return satisfy(el => set.has(el)); }; -export const noneOf = (values: Iterable): Parser => { +export const noneOf = (values: Iterable): Parser => { const set = new Set(values); return satisfy(el => !set.has(el)); }; -export const satisfy = ( - f: - | ((el: unknown, config: Config) => boolean) - | ((el: unknown, config: Config) => el is T), +export const satisfy = ( + f: ((el: S, config: Config) => boolean) | ((el: S, config: Config) => el is T), options?: { error?: error.ParseError }, -): Parser => +): Parser => new Parser((state, context) => { let srcEl: unknown; if ( state.i < context.src.length && f((srcEl = context.src[state.i]), context.cfg) ) { - return updateState(state, srcEl, 1); + return updateState(state, srcEl as T, 1); } context.addError(state.i, options?.error); return null; }); -export const literal = (chunk: T): Parser => +export const literal = >( + chunk: T, +): Parser => new Parser((state, context) => { if (state.i + chunk.length > context.src.length) { context.addError(state.i, error.expected(chunk)); diff --git a/src/string.ts b/src/string.ts index 78615cc..612c97e 100644 --- a/src/string.ts +++ b/src/string.ts @@ -2,7 +2,7 @@ import * as error from "./error"; import { Parser } from "./parser"; import { updateState } from "./state"; -export const string = (string: T): Parser => { +export const string = (string: T): Parser => { return new Parser((state, context) => { if (typeof context.src !== "string") { context.addError(state.i); @@ -19,7 +19,7 @@ export const string = (string: T): Parser => { const graphemeSegmenter = /* @__PURE__ */ new Intl.Segmenter(); -export const graphemeString = (string: string): Parser => { +export const graphemeString = (string: string): Parser => { const normalizedString = string.normalize(); return new Parser((state, context) => { if (typeof context.src !== "string") { @@ -60,7 +60,7 @@ export const graphemeString = (string: string): Parser => { }); }; -export const CODE_POINT = /* @__PURE__ */ new Parser((state, context) => { +export const CODE_POINT = /* @__PURE__ */ new Parser((state, context) => { if (typeof context.src !== "string") { context.addError(state.i); return null; @@ -92,7 +92,7 @@ export const CODE_POINT = /* @__PURE__ */ new Parser((state, context) => { return updateState(state, context.src[state.i], 1); }); -export const ANY_CHAR = /* @__PURE__ */ new Parser((state, context) => { +export const ANY_CHAR = /* @__PURE__ */ new Parser((state, context) => { if (typeof context.src !== "string") { context.addError(state.i); return null; @@ -109,7 +109,7 @@ export const ANY_CHAR = /* @__PURE__ */ new Parser((state, context) => { return updateState(state, segmentData.segment, segmentData.segment.length); }); -export const regexGroup = (re: RegExp): Parser => { +export const regexGroup = (re: RegExp): Parser => { const fixedRegex = new RegExp(`^(?:${re.source})`, re.flags.replace("g", "")); return new Parser((state, context) => { @@ -127,9 +127,12 @@ export const regexGroup = (re: RegExp): Parser => { }; export const regex: { - (re: RegExp): Parser; - (re: RegExp, groupId: number | string): Parser; - (re: RegExp, groupId: number | string, defaultValue: T): Parser; + (re: RegExp): Parser; + (re: RegExp, groupId: number | string): Parser; + (re: RegExp, groupId: number | string, defaultValue: T): Parser< + string | T, + string + >; } = (re: RegExp, groupId: number | string = 0, defaultValue?: undefined) => regexGroup(re).map( matchResult =>