From 4ff50dcffaf0fe163bb0d223d32e3f101b4deb06 Mon Sep 17 00:00:00 2001 From: sinclair Date: Fri, 22 Nov 2024 15:55:20 +0900 Subject: [PATCH] Array and Optional Combinators --- example/index.ts | 74 ++++----------------- readme.md | 141 +++++++++++++++++++++++++++++++++++----- src/runtime/guard.ts | 42 +++++------- src/runtime/module.ts | 23 ++++--- src/runtime/parse.ts | 131 ++++++++++++++++++++++--------------- src/runtime/types.ts | 147 ++++++++++++++++++++++++++++++------------ src/static/parse.ts | 67 ++++++++++++------- src/static/types.ts | 85 ++++++++++++++++-------- test/runtime/parse.ts | 17 +++++ test/static/parse.ts | 18 ++++++ 10 files changed, 491 insertions(+), 254 deletions(-) diff --git a/example/index.ts b/example/index.ts index 67b873a..85d1a4b 100644 --- a/example/index.ts +++ b/example/index.ts @@ -1,71 +1,25 @@ -import { Static } from '@sinclair/parsebox' +import { Static, Runtime } from '@sinclair/parsebox' import { Parse } from './typebox' -// ------------------------------------------------------------------ -// Example: TypeBox -// ------------------------------------------------------------------ +// "[" [ element { "," element } [ "," ] ] "]" -const T = Parse(`{ - x: number, - y: number, - z: number -}`) +const Element = Runtime.Number() -console.log(T) - -// ------------------------------------------------------------------ -// Example: Expression Parser -// ------------------------------------------------------------------ - -type Result = Static.Parse // hover - -// prettier-ignore -type BinaryReduce = ( - Right extends [infer Operator, infer Right, infer Rest extends unknown[]] - ? { left: Left; operator: Operator; right: BinaryReduce } - : Left -) - -// prettier-ignore -interface BinaryMapping extends Static.IMapping { - output: this['input'] extends [infer Left, infer Right extends unknown[]] - ? BinaryReduce - : never +const ElementsMapping = (_0: unknown, _1: [',', unknown][], _2: [','] | []) => { + return [_0, ..._1.map((value) => value[1])] as const } -// prettier-ignore -interface FactorMapping extends Static.IMapping { - output: ( - this['input'] extends ['(', infer Expr, ')'] ? Expr : - this['input'] extends [infer Operand] ? Operand : - never - ) -} - -type Operand = Static.Ident +// elements ::= element { "," element } [ "," ] -// prettier-ignore -type Factor = Static.Union<[ - Static.Tuple<[Static.Const<'('>, Expr, Static.Const<')'>]>, - Static.Tuple<[Operand]> -], FactorMapping> +const Elements = Runtime.Tuple([Element, Runtime.Array(Runtime.Tuple([Runtime.Const(','), Element])), Runtime.Optional(Runtime.Const(','))], (values) => ElementsMapping(...values)) -// prettier-ignore -type TermTail = Static.Union<[ - Static.Tuple<[Static.Const<'*'>, Factor, TermTail]>, - Static.Tuple<[Static.Const<'/'>, Factor, TermTail]>, - Static.Tuple<[]> -]> +// array ::= "[" [ elements ] "]" -// prettier-ignore -type ExprTail = Static.Union<[ - Static.Tuple<[Static.Const<'+'>, Term, ExprTail]>, - Static.Tuple<[Static.Const<'-'>, Term, ExprTail]>, - Static.Tuple<[]> -]> +const ArrayMapping = (_0: '[', _1: unknown[], _2: ']') => { + return _1 +} +const Array = Runtime.Tuple([Runtime.Const('['), Runtime.Optional(Elements), Runtime.Const(']')], (values) => ArrayMapping(...values)) -// prettier-ignore -type Term = Static.Tuple<[Factor, TermTail], BinaryMapping> +const R = Runtime.Parse(Array, '[1, 2, 3]') -// prettier-ignore -type Expr = Static.Tuple<[Term, ExprTail], BinaryMapping> +console.dir(R, { depth: 100 }) diff --git a/readme.md b/readme.md index 9132c5e..1c3ca8b 100644 --- a/readme.md +++ b/readme.md @@ -23,22 +23,24 @@ $ npm install @sinclair/parsebox ## Example -ParseBox provides symmetric parsing in Static and Runtime environments. +ParseBox provides encoding of BNF notation in Static and Runtime environments. -```typescript -import { Runtime, Static } from '@sinclair/parsebox' +**BNF** -// Runtime Parser +```bnf + ::= "X" "Y" "X" +``` -const T = Runtime.Tuple([ - Runtime.Const('X'), - Runtime.Const('Y'), - Runtime.Const('Z') -]) +**Type** -const R = Runtime.Parse(T, 'X Y Z W') // const R = [['X', 'Y', 'Z'], ' W'] +```typescript +const T = ["X", "Y", "Z"] +``` -// Static Parser +**Static** + +```typescript +import { Static } from '@sinclair/parsebox' type T = Static.Tuple<[ Static.Const<'X'>, @@ -49,35 +51,58 @@ type T = Static.Tuple<[ type R = Static.Parse // type R = [['X', 'Y', 'Z'], ' W'] ``` +**Runtime** + +```typescript + +import { Runtime } from '@sinclair/parsebox' + +const T = Runtime.Tuple([ + Runtime.Const('X'), + Runtime.Const('Y'), + Runtime.Const('Z') +]) + +const R = Runtime.Parse(T, 'X Y Z W') // const R = [['X', 'Y', 'Z'], ' W'] + +``` + Which can be used to construct [Expression](https://www.typescriptlang.org/play/?moduleResolution=99&module=199#code/JYWwDg9gTgLgBAbzgZRgQxsAxnAvnAMyghDgHIABAZ2ADssAbNYKAejDSioFMAjCAB5kAUMNas4AWmkzZc+QsVLlKyWIkBRAWnANu6qaqPGTq0TACeYbnABK3KgFcG8ALwp0mLADoACpx4AHi0wKAAacgE4ACo4AAoLOABqOAAvAEoyAD44XNzxOEtrOwdnNzgAbQRhPNq6+obGpuaC3L0CGAAuOAAiAR6AbhrmkdGxuta4CGsoDGhunujB4fHVtYmJXKhgAHMACy7EFfWT8cna9sOei2XTu7HzvOnuWZh53qTb++-Gx63dg4LVJfH6gvKTXBDMHQuAFXARHo9AC6ogMpnRGNMBhCUAcNAgtDg-i4LzRmPJFPkojoMBeBDQWBsADlHCAXtgALJoMBgOg7ODcAS02gAEyoHgw2G8AEkuTy+UdchBHDAwCrujA9sAqBUyHQ1TAyEiBULuKLxQADAAkCDoBBeJScLhNwrFcForN4L1wFrgAH5HWU4N1aNwAG6k3DCIrM1nsnDuVCSnwskBeqCBVPxuW82g7LLmKw2ADyMzQorgic8UoAqrRgATAhUk14ZSKzTAIlntlgkQXRAUAEJ0TgWHN87r2EWORnikAYLB7bgiuAAFRepHLK5xcBpEDgvBHUESgtCeIbhNoEHb0aLcGHtFHU5n3ECABluB0XWa3Y5aABrK8AHdaAiWwAXgQVXXFP9AIgECKj7St4mGcD9kg01zUqO0HVLF45nCXdaHtKA7AgiIcNI+wqAw6C4Fg4DaEQlFagDJBLm6D8OgiZ5XnePC+MI7Z0O6B8n2XF9AjQg4wIcGAcijWpOM-GBhHSYQaTpBkbDE49xzzb8sJbKVZW5XN+WqJUVQNDUtR1PVaANI1DLdCpKLgLjOyIkiyPQlyYIAxjmOOANdIsZ9GXfFSwIggslPdcNIwMAAxBk3igfSdm6OUBWATUHR2YhHGsbcBFCKZSIErcNNoWkoHpRk4FSrB0sy-yJVbUz5QMyypms9V4jgY5NW1XV9RVZyoJ-cVdTiMgKOIh0cQiMhMmNAMd06Ya7LGxyJuNKasLcxbKrLUV1rgKqKy22pQwjKBhnUqMYyatLoGQ4yfDrC8m2GT7vFXYq9Cbf6AGECRowIyDmrIImWjqpXB2hIdW7I+zCP7qx8QGwGBiorpFdHhCRCJmtasy+X7F71ygEBV2YBgPqx7xvsbCpMeTAGgdfZtmaRlHomyUm3sImm6YZoncn+nG8bBiGYCh1ghdelroAiMX6eABhJYR7HuabIm+0LYocU1xmq051naF+qXmZlnm5eRhWyCSZWxbhsqoDNnXpf13nOf553JDdjcPdCb3YY51t7YNyOjdvYoxaZzmY4qMm1bXDdvYiMLMqpu8dwt6O-fduBTYlnOjzHCm837IA) or even [Type Syntax](https://www.typescriptlang.org/play/?moduleResolution=99&module=199#code/JYWwDg9gTgLgBAbzgBQIZQM4FM4F84BmUEIcA5AAIbAB2AxgDarBQD0MAnmFgEYQAerDBxoxU-MgChQkWIjgBlMTGB08hYqUrV6TFuy68BUyXQg0M8AGIQIcALwp02ABQADEBAAmAVwY4bOwRJSTg4Tm44ACEHULCUVQBrAB4AQQAacmAvMgA+OAAyODiwtFhgVAZkgHkQYBg0zLJsvNyQsIicVIdEErhsgC44SyhaAHM+mm8sDCHUgG0AXTjcENw3AEoQswt4GMdAgDoASXBoGBcyKLItyU7onqVUFTpkzogCaPz4n9-WVnChgejmCvzB4IhkPi-1+g2GMFGNDGAG4+lD0Ri4DD8EVQZj8VCYT8pl4ZgB+IZ4gnUv4AiFwkbjVE0lnQungkkzIaHHlLZmsllE+K4JZwAA+cB8NFJBFoWC8-IF1OxQA) parsers at runtime and inside the type system. ## Overview -ParseBox is a TypeScript parsing library designed to embed domain-specific languages (DSLs) within the TypeScript type system. It provides a core set of declarative combinators for parsing in Static and Runtime environments. The parsing structures created with this library can be replicated across environments to ensure symmetric parsing logic in each environment (Static or Runtime). Declarative parsing structures help to mitigate many problems that arise when replicating explicit logic in different languages, with the declarative combinators leading to more managable code overall. +ParseBox is a parsing library designed to embed domain-specific languages (DSLs) within the TypeScript type system. It provides a set of type-level and runtime combinators that directly map to BNF notation. Once defined, these combinators can parse content either at runtime or statically within the type system. + +ParseBox is designed to be general-purpose, offering foundational combinators for expressing BNF grammars both at runtime and statically within the type system. These combinators are symmetric, allowing runtime parsers to be easily ported to operate statically by mirroring their structures. -This project is written as a parsing infrastructure for the [TypeBox](https://github.com/sinclairzx81/typebox) and [LinqBox](https://github.com/sinclairzx81/linqbox) projects. It is offered as a standalone package for experimentation, as well as to provide a foundation for advancing string DSL parsing in the TypeScript type system. +The project is written as parsing infrastructure for the [TypeBox](https://github.com/sinclairzx81/typebox) and [LinqBox](https://github.com/sinclairzx81/linqbox) projects, enabling these libraries to support advanced runtime parsing for their respective domains. License MIT ## Contents -- [Parsers](#Parsers) +- [Standard](#Standard) - [Const](#Const) - [Tuple](#Tuple) - [Union](#Union) +- [Extended](#Extended) + - [Array](#Array) + - [Optional](#Optional) - [Terminals](#Terminals) - [Number](#Number) - [String](#String) - [Ident](#Ident) +- [Extended](#Extended) - [Mapping](#Mapping) -- [Context](#Context) +- [Context](#Context) - [Modules](#Modules) +- [Extended](#Extended) - [Advanced](#Advanced) - [Contribute](#Contribute) -## Parsers +## Standard BNF -ParseBox provides three main combinators `Const`, `Tuple`, and `Union` which are named after the types of values they parse. These combinators are used to express BNF (Backus-Naur Form) grammar within the type system. These combinators service as fundamental building blocks for constructing higher order parsers. +ParseBox provides three standard combinators that map to notation expressable in standard BNF (Backus-Naur Form). These combinators are named Const, Tuple and Union respectively to describe the type of production each combinator produces (as observed by the type system). These combinators service as fundamental building blocks for constructing parsers. ### Const @@ -89,6 +114,12 @@ The Const combinator parses for the next occurrence of the specified string. Whi ::= "X" ``` +**Type** + +```typescript +type T = 'X' +``` + **TypeScript** ```typescript @@ -110,6 +141,12 @@ The Tuple parser matches a sequence of interior parsers. An empty tuple can be u ::= "X" "Y" "Z" ``` +**Type** + +```typescript +type T = ['X', 'Y', 'Z'] +``` + **TypeScript** ```typescript @@ -136,6 +173,12 @@ The Union combinator parses using one of the interior parsers, attempting each i ::= "X" | "Y" | "Z" ``` +**Type** + +```typescript +type T = 'X' | 'Y' | 'Z' +``` + **TypeScript** ```typescript @@ -155,6 +198,70 @@ const R2 = Runtime.Parse(P, 'Y E') // const R2 = ['Y', ' E'] const R3 = Runtime.Parse(P, 'Z E') // const R3 = ['Z', ' E'] ``` +## Extended BNF + +ParseBox provides support for the following Extended BNF combinators. + +### Array + +The Array combinator will parse for zero or more the interior parser. This combinator will always return a result with an empty array for no matches. + +**EBNF** + +``` + ::= { "X" } +``` + +**Type** + +```typescript +type T = 'X'[] +``` + +**TypeScript** + +```typescript +const T = Runtime.Array( // const T = { + Runtime.Const('X') // type: 'Array', +) // parser: { type: 'Const', value: 'X' } + // } + +const R1 = Runtime.Parse(T, 'X Y Z') // const R1 = [['X'], ' Y Z'] + +const R2 = Runtime.Parse(T, 'X X X Y Z') // const R2 = [['X', 'X', 'X'], ' Y Z'] + +const R3 = Runtime.Parse(T, 'Y Z') // const R3 = [[], 'Y Z'] +``` + +### Optional + +The Optional combinator will parse for zero or one of the interior parser. This combinator always succeeds, returning either a tuple with one element, or zero for no match. + +**EBNF** + +``` + ::= [ "X" ] +``` + +**Type** + +```typescript +type T = ['X'] | [] +``` + +**TypeScript** + +```typescript +const T = Runtime.Optional( // const T = { + Runtime.Const('X') // type: 'Optional', +) // parser: { type: 'Const', value: 'X' } + // } + +const R1 = Runtime.Parse(T, 'X Y Z') // const R1 = ['X', ' Y Z'] + +const R2 = Runtime.Parse(T, 'Y Z') // const R2 = [[], 'Y Z'] +``` + ## Terminals ParseBox provides combinators that can be used to parse common terminals. diff --git a/src/runtime/guard.ts b/src/runtime/guard.ts index fa002e4..80ef3cf 100644 --- a/src/runtime/guard.ts +++ b/src/runtime/guard.ts @@ -26,7 +26,7 @@ THE SOFTWARE. ---------------------------------------------------------------------------*/ -import { IIdent, INumber, IRef, IString, IConst, ITuple, IUnion } from './types' +import { IArray, IConst, IIdent, INumber, IOptional, IRef, IString, ITuple, IUnion } from './types' // ------------------------------------------------------------------ // Value Guard @@ -46,51 +46,43 @@ function IsArrayValue(value: unknown): value is unknown[] { // ------------------------------------------------------------------ // Parser Guard // ------------------------------------------------------------------ -/** Returns true if the value is a Tuple Parser */ -// prettier-ignore -export function IsTuple(value: unknown): value is ITuple { - return IsObjectValue(value) && HasPropertyKey(value, 'type') && value.type === 'Tuple' && HasPropertyKey(value, 'parsers') && IsArrayValue(value.parsers) -} -/** Returns true if the value is a Union Parser */ -// prettier-ignore -export function IsUnion(value: unknown): value is IUnion { - return IsObjectValue(value) && HasPropertyKey(value, 'type') && value.type === 'Union' && HasPropertyKey(value, 'parsers') && IsArrayValue(value.parsers) +/** Returns true if the value is a Array Parser */ +export function IsArray(value: unknown): value is IArray { + return IsObjectValue(value) && HasPropertyKey(value, 'type') && value.type === 'Array' && HasPropertyKey(value, 'parser') && IsObjectValue(value.parser) } /** Returns true if the value is a Const Parser */ -// prettier-ignore export function IsConst(value: unknown): value is IConst { return IsObjectValue(value) && HasPropertyKey(value, 'type') && value.type === 'Const' && HasPropertyKey(value, 'value') && typeof value.value === 'string' } /** Returns true if the value is a Ident Parser */ -// prettier-ignore export function IsIdent(value: unknown): value is IIdent { return IsObjectValue(value) && HasPropertyKey(value, 'type') && value.type === 'Ident' } /** Returns true if the value is a Number Parser */ -// prettier-ignore export function IsNumber(value: unknown): value is INumber { return IsObjectValue(value) && HasPropertyKey(value, 'type') && value.type === 'Number' } +/** Returns true if the value is a Optional Parser */ +export function IsOptional(value: unknown): value is IOptional { + return IsObjectValue(value) && HasPropertyKey(value, 'type') && value.type === 'Optional' && HasPropertyKey(value, 'parser') && IsObjectValue(value.parser) +} /** Returns true if the value is a Ref Parser */ -// prettier-ignore export function IsRef(value: unknown): value is IRef { return IsObjectValue(value) && HasPropertyKey(value, 'type') && value.type === 'Ref' && HasPropertyKey(value, 'ref') && typeof value.ref === 'string' } /** Returns true if the value is a String Parser */ -// prettier-ignore export function IsString(value: unknown): value is IString { return IsObjectValue(value) && HasPropertyKey(value, 'type') && value.type === 'String' && HasPropertyKey(value, 'options') && IsArrayValue(value.options) } +/** Returns true if the value is a Tuple Parser */ +export function IsTuple(value: unknown): value is ITuple { + return IsObjectValue(value) && HasPropertyKey(value, 'type') && value.type === 'Tuple' && HasPropertyKey(value, 'parsers') && IsArrayValue(value.parsers) +} +/** Returns true if the value is a Union Parser */ +export function IsUnion(value: unknown): value is IUnion { + return IsObjectValue(value) && HasPropertyKey(value, 'type') && value.type === 'Union' && HasPropertyKey(value, 'parsers') && IsArrayValue(value.parsers) +} /** Returns true if the value is a Parser */ -// prettier-ignore export function IsParser(value: unknown) { - return ( - IsTuple(value) || - IsUnion(value) || - IsConst(value) || - IsIdent(value) || - IsNumber(value) || - IsRef(value) || - IsString(value) - ) + return IsArray(value) || IsConst(value) || IsIdent(value) || IsNumber(value) || IsOptional(value) || IsRef(value) || IsString(value) || IsTuple(value) || IsUnion(value) } diff --git a/src/runtime/module.ts b/src/runtime/module.ts index 4384ce4..6df334c 100644 --- a/src/runtime/module.ts +++ b/src/runtime/module.ts @@ -32,20 +32,23 @@ import { Parse } from './parse' // ------------------------------------------------------------------ // Module // ------------------------------------------------------------------ -// prettier-ignore export class Module { - constructor(private readonly properties: Properties) { } - + constructor(private readonly properties: Properties) {} + /** Parses using one of the parsers defined on this instance */ - public Parse(key: Key, code: string, context: unknown): [] | [Types.StaticParser, string] + public Parse(key: Key, content: string, context: unknown): [] | [Types.StaticParser, string] /** Parses using one of the parsers defined on this instance */ - public Parse(key: Key, code: string): [] | [Types.StaticParser, string] + public Parse(key: Key, content: string): [] | [Types.StaticParser, string] /** Parses using one of the parsers defined on this instance */ public Parse(...args: any[]): never { - const [key, code, context] = - args.length === 3 ? [args[0], args[1], args[2]] : - args.length === 2 ? [args[0], args[1], undefined] : - (() => { throw Error('Invalid parse arguments') })() - return Parse(this.properties[key], this.properties, code, context) as never + const [key, content, context] = + args.length === 3 + ? [args[0], args[1], args[2]] + : args.length === 2 + ? [args[0], args[1], undefined] + : (() => { + throw Error('Invalid parse arguments') + })() + return Parse(this.properties, this.properties[key], content, context) as never } } diff --git a/src/runtime/parse.ts b/src/runtime/parse.ts index 73ecf2f..8993ead 100644 --- a/src/runtime/parse.ts +++ b/src/runtime/parse.ts @@ -31,48 +31,59 @@ import * as Token from './token' import * as Types from './types' // ------------------------------------------------------------------ -// Tuple +// Array // ------------------------------------------------------------------ -// prettier-ignore -function ParseTuple(parsers: [...Parsers], properties: Properties, code: string, context: unknown): [] | [unknown[], string] { +function ParseArray(moduleProperties: ModuleProperties, parser: Parser, code: string, context: unknown): unknown[] { const buffer = [] as unknown[] let rest = code - for(const parser of parsers) { - const result = ParseParser(parser, properties, rest, context) - if(result.length === 0) return [] + while (rest.length > 0) { + const result = ParseParser(moduleProperties, parser, rest, context) + if (result.length === 0) return [buffer, rest] buffer.push(result[0]) rest = result[1] } return [buffer, rest] } + // ------------------------------------------------------------------ -// Union +// Const // ------------------------------------------------------------------ -// prettier-ignore -function ParseUnion(parsers: [...Parsers], properties: Properties, code: string, context: unknown): [] | [unknown, string] { - for(const parser of parsers) { - const result = ParseParser(parser, properties, code, context) - if(result.length === 0) continue - return result - } - return [] +function ParseConst(value: Value, code: string, context: unknown): [] | [Value, string] { + return Token.Const(value, code) as never } + // ------------------------------------------------------------------ -// Const +// Ident +// ------------------------------------------------------------------ +function ParseIdent(code: string, _context: unknown): [] | [string, string] { + return Token.Ident(code) +} + +// ------------------------------------------------------------------ +// Number // ------------------------------------------------------------------ // prettier-ignore -function ParseConst(value: Value, code: string, context: unknown): [] | [Value, string] { - return Token.Const(value, code) as never +function ParseNumber(code: string, _context: unknown): [] | [string, string] { + return Token.Number(code) +} + +// ------------------------------------------------------------------ +// Optional +// ------------------------------------------------------------------ +function ParseOptional(moduleProperties: ModuleProperties, parser: Parser, code: string, context: unknown): [] | [[unknown] | [], unknown] { + const result = ParseParser(moduleProperties, parser, code, context) + return (result.length === 2 ? [[result[0]], result[1]] : [[], code]) as never } + // ------------------------------------------------------------------ // Ref // ------------------------------------------------------------------ -// prettier-ignore -function ParseRef(ref: Ref, properties: Properties, code: string, context: unknown): [] | [string, string] { - const parser = properties[ref] - if(!Guard.IsParser(parser)) throw Error(`Cannot dereference parser '${ref}'`) - return ParseParser(parser, properties, code, context) as never +function ParseRef(moduleProperties: ModuleProperties, ref: Ref, code: string, context: unknown): [] | [string, string] { + const parser = moduleProperties[ref] + if (!Guard.IsParser(parser)) throw Error(`Cannot dereference Parser '${ref}'`) + return ParseParser(moduleProperties, parser, code, context) as never } + // ------------------------------------------------------------------ // String // ------------------------------------------------------------------ @@ -80,62 +91,80 @@ function ParseRef(moduleProperties: ModuleProperties, parsers: [...Parsers], code: string, context: unknown): [] | [unknown[], string] { + const buffer = [] as unknown[] + let rest = code + for (const parser of parsers) { + const result = ParseParser(moduleProperties, parser, rest, context) + if (result.length === 0) return [] + buffer.push(result[0]) + rest = result[1] + } + return [buffer, rest] } + // ------------------------------------------------------------------ -// Ident +// Union // ------------------------------------------------------------------ // prettier-ignore -function ParseIdent(code: string, _context: unknown): [] | [string, string] { - return Token.Ident(code) +function ParseUnion(moduleProperties: ModuleProperties, parsers: [...Parsers], code: string, context: unknown): [] | [unknown, string] { + for(const parser of parsers) { + const result = ParseParser(moduleProperties, parser, code, context) + if(result.length === 0) continue + return result + } + return [] } + // ------------------------------------------------------------------ // Parser // ------------------------------------------------------------------ // prettier-ignore -function ParseParser(parser: Parser, properties: Types.IModuleProperties, code: string, context: unknown): [] | [Types.StaticParser, string] { - const result = ( - Guard.IsTuple(parser) ? ParseTuple(parser.parsers, properties, code, context) : - Guard.IsUnion(parser) ? ParseUnion(parser.parsers, properties, code, context) : +function ParseParser(moduleProperties: Types.IModuleProperties, parser: Parser, code: string, context: unknown): [] | [Types.StaticParser, string] { + const production = ( + Guard.IsArray(parser) ? ParseArray(moduleProperties, parser.parser, code, context) : Guard.IsConst(parser) ? ParseConst(parser.value, code, context) : - Guard.IsRef(parser) ? ParseRef(parser.ref, properties, code, context) : - Guard.IsString(parser) ? ParseString(parser.options, code, context) : Guard.IsIdent(parser) ? ParseIdent(code, context) : Guard.IsNumber(parser) ? ParseNumber(code, context) : + Guard.IsOptional(parser) ? ParseOptional(moduleProperties, parser.parser, code, context) : + Guard.IsRef(parser) ? ParseRef(moduleProperties, parser.ref, code, context) : + Guard.IsString(parser) ? ParseString(parser.options, code, context) : + Guard.IsTuple(parser) ? ParseTuple(moduleProperties, parser.parsers, code, context) : + Guard.IsUnion(parser) ? ParseUnion(moduleProperties, parser.parsers, code, context) : [] ) return ( - result.length === 2 - ? [parser.mapping(result[0], context), result[1]] - : result + production.length === 2 + ? [parser.mapping(production[0], context), production[1]] + : production ) as never } + // ------------------------------------------------------------------ // Parse // ------------------------------------------------------------------ -/** Parses content using the given parser */ +/** Parses content using the given Parser */ // prettier-ignore -export function Parse(parser: Parser, properties: Types.IModuleProperties, code: string, context: unknown): [] | [Types.StaticParser, string] -/** Parses content using the given parser */ +export function Parse(moduleProperties: Types.IModuleProperties, parser: Parser, code: string, context: unknown): [] | [Types.StaticParser, string] +/** Parses content using the given Parser */ // prettier-ignore -export function Parse(parser: Parser, properties: Types.IModuleProperties, code: string): [] | [Types.StaticParser, string] -/** Parses content using the given parser */ +export function Parse(moduleProperties: Types.IModuleProperties, parser: Parser, code: string): [] | [Types.StaticParser, string] +/** Parses content using the given Parser */ // prettier-ignore -export function Parse(parser: Parser, code: string, context: unknown): [] | [Types.StaticParser, string] -/** Parses content using the given parser */ +export function Parse(parser: Parser, content: string, context: unknown): [] | [Types.StaticParser, string] +/** Parses content using the given Parser */ // prettier-ignore -export function Parse(parser: Parser, code: string): [] | [Types.StaticParser, string] +export function Parse(parser: Parser, content: string): [] | [Types.StaticParser, string] /** Parses content using the given parser */ // prettier-ignore export function Parse(...args: any[]): never { - const withProperties = typeof args[1] === 'string' ? false : true - const [parser, properties, code, context] = withProperties + const withModuleProperties = typeof args[1] === 'string' ? false : true + const [moduleProperties, parser, content, context] = withModuleProperties ? [args[0], args[1], args[2], args[3]] - : [args[0], {}, args[1], args[2]] - return ParseParser(parser, properties, code, context) as never + : [{}, args[0], args[1], args[2]] + return ParseParser(moduleProperties, parser, content, context) as never } diff --git a/src/runtime/types.ts b/src/runtime/types.ts index 53dd047..fb8e0ac 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types.ts @@ -32,6 +32,9 @@ export type IModuleProperties = Record // Static // ------------------------------------------------------------------ +/** Force output static type evaluation for Arrays */ +export type StaticEnsure = T extends infer R ? R : never + /** Infers the Output Parameter for a Parser */ export type StaticParser = Parser extends IParser ? Output : unknown @@ -44,10 +47,8 @@ export type IMapping value /** Maps the output as the given parameter T */ -export const As = - (mapping: T): ((value: unknown) => T) => - (_: unknown) => - mapping +// prettier-ignore +export const As = (mapping: T): ((value: unknown) => T) => (_: unknown) => mapping // ------------------------------------------------------------------ // Parser @@ -56,51 +57,40 @@ export interface IParser { type: string mapping: IMapping } -// ------------------------------------------------------------------ -// Tuple -// ------------------------------------------------------------------ -export type TupleParameter = Parsers extends [infer L extends IParser, ...infer R extends IParser[]] ? TupleParameter]> : Result -export interface ITuple extends IParser { - type: 'Tuple' - parsers: IParser[] -} -/** Creates a Tuple parser */ -export function Tuple>>(parsers: [...Parsers], mapping: Mapping): ITuple> -/** Creates a Tuple parser */ -export function Tuple(parsers: [...Parsers]): ITuple> -export function Tuple(...args: unknown[]): never { - const [parsers, mapping] = args.length === 2 ? [args[0], args[1]] : [args[0], Identity] - return { type: 'Tuple', parsers, mapping } as never -} // ------------------------------------------------------------------ -// Union +// Array // ------------------------------------------------------------------ -export type UnionParameter = Parsers extends [infer L extends IParser, ...infer R extends IParser[]] ? UnionParameter> : Result -export interface IUnion extends IParser { - type: 'Union' - parsers: IParser[] +// prettier-ignore +export type ArrayParameter = StaticEnsure< + StaticParser[] +> +export interface IArray extends IParser { + type: 'Array' + parser: IParser } -/** Creates a Union parser */ -export function Union>>(parsers: [...Parsers], mapping: Mapping): IUnion> -/** Creates a Union parser */ -export function Union(parsers: [...Parsers]): IUnion> -export function Union(...args: unknown[]): never { - const [parsers, mapping] = args.length === 2 ? [args[0], args[1]] : [args[0], Identity] - return { type: 'Union', parsers, mapping } as never +/** `[EBNF]` Creates an Array parser */ +export function Array>>(parser: Parser, mapping: Mapping): IArray> +/** `[EBNF]` Creates an Array parser */ +export function Array(parser: Parser): IArray> +/** `[EBNF]` Creates an Array parser */ +export function Array(...args: unknown[]): never { + const [parser, mapping] = args.length === 2 ? [args[0], args[1]] : [args[0], Identity] + return { type: 'Array', parser, mapping } as never } // ------------------------------------------------------------------ -// Token +// Const // ------------------------------------------------------------------ export interface IConst extends IParser { type: 'Const' value: string } -/** Creates a Const parser */ +/** `[TERM]` Creates a Const parser */ export function Const>(value: Value, mapping: Mapping): IConst> -/** Creates a Const parser */ +/** `[TERM]` Creates a Const parser */ export function Const(value: Value): IConst +/** `[TERM]` Creates a Const parser */ export function Const(...args: unknown[]): never { const [value, mapping] = args.length === 2 ? [args[0], args[1]] : [args[0], Identity] return { type: 'Const', value, mapping } as never @@ -113,10 +103,11 @@ export interface IRef extends IParser type: 'Ref' ref: string } -/** Creates a Ref parser */ +/** `[BNF]` Creates a Ref parser. This Parser can only be used in the context of a Module */ export function Ref>(ref: string, mapping: Mapping): IRef> -/** Creates a Ref parser */ +/** `[BNF]` Creates a Ref parser. This Parser can only be used in the context of a Module */ export function Ref(ref: string): IRef +/** `[BNF]` Creates a Ref parser. This Parser can only be used in the context of a Module */ export function Ref(...args: unknown[]): never { const [ref, mapping] = args.length === 2 ? [args[0], args[1]] : [args[0], Identity] return { type: 'Ref', ref, mapping } as never @@ -129,10 +120,11 @@ export interface IString extends IParser>(options: string[], mapping: Mapping): IString> -/** Creates a String Parser. Options are an array of permissable quote characters */ +/** `[TERM]` Creates a String Parser. Options are an array of permissable quote characters */ export function String(options: string[]): IString +/** `[TERM]` Creates a String Parser. Options are an array of permissable quote characters */ export function String(...params: unknown[]): never { const [options, mapping] = params.length === 2 ? [params[0], params[1]] : [params[0], Identity] return { type: 'String', options, mapping } as never @@ -144,10 +136,11 @@ export function String(...params: unknown[]): never { export interface IIdent extends IParser { type: 'Ident' } -/** Creates an Ident parser */ +/** `[TERM]` Creates an Ident parser where Ident matches any valid JavaScript identifier */ export function Ident>(mapping: Mapping): IIdent> -/** Creates an Ident parser */ +/** `[TERM]` Creates an Ident parser where Ident matches any valid JavaScript identifier */ export function Ident(): IIdent +/** `[TERM]` Creates an Ident parser where Ident matches any valid JavaScript identifier */ export function Ident(...params: unknown[]): never { const mapping = params.length === 1 ? params[0] : Identity return { type: 'Ident', mapping } as never @@ -159,11 +152,79 @@ export function Ident(...params: unknown[]): never { export interface INumber extends IParser { type: 'Number' } -/** Creates a Number parser */ +/** `[TERM]` Creates an Number parser */ export function Number>(mapping: Mapping): INumber> -/** Creates a Number parser */ +/** `[TERM]` Creates an Number parser */ export function Number(): INumber +/** `[TERM]` Creates an Number parser */ export function Number(...params: unknown[]): never { const mapping = params.length === 1 ? params[0] : Identity return { type: 'Number', mapping } as never } + +// ------------------------------------------------------------------ +// Optional +// ------------------------------------------------------------------ +// prettier-ignore +export type OptionalParameter] | []> = ( + Result +) +export interface IOptional extends IParser { + type: 'Optional' + parser: IParser +} +/** `[EBNF]` Creates an Optional parser */ +export function Optional>>(parser: Parser, mapping: Mapping): IOptional> +/** `[EBNF]` Creates an Optional parser */ +export function Optional(parser: Parser): IOptional> +/** `[EBNF]` Creates an Optional parser */ +export function Optional(...args: unknown[]): never { + const [parser, mapping] = args.length === 2 ? [args[0], args[1]] : [args[0], Identity] + return { type: 'Optional', parser, mapping } as never +} + +// ------------------------------------------------------------------ +// Tuple +// ------------------------------------------------------------------ +// prettier-ignore +export type TupleParameter = StaticEnsure< + Parsers extends [infer Left extends IParser, ...infer Right extends IParser[]] + ? TupleParameter>]> + : Result +> +export interface ITuple extends IParser { + type: 'Tuple' + parsers: IParser[] +} +/** `[BNF]` Creates a Tuple parser */ +export function Tuple>>(parsers: [...Parsers], mapping: Mapping): ITuple> +/** `[BNF]` Creates a Tuple parser */ +export function Tuple(parsers: [...Parsers]): ITuple> +/** `[BNF]` Creates a Tuple parser */ +export function Tuple(...args: unknown[]): never { + const [parsers, mapping] = args.length === 2 ? [args[0], args[1]] : [args[0], Identity] + return { type: 'Tuple', parsers, mapping } as never +} + +// ------------------------------------------------------------------ +// Union +// ------------------------------------------------------------------ +// prettier-ignore +export type UnionParameter = StaticEnsure< + Parsers extends [infer Left extends IParser, ...infer Right extends IParser[]] + ? UnionParameter> + : Result +> +export interface IUnion extends IParser { + type: 'Union' + parsers: IParser[] +} +/** `[BNF]` Creates a Union parser */ +export function Union>>(parsers: [...Parsers], mapping: Mapping): IUnion> +/** `[BNF]` Creates a Union parser */ +export function Union(parsers: [...Parsers]): IUnion> +/** `[BNF]` Creates a Union parser */ +export function Union(...args: unknown[]): never { + const [parsers, mapping] = args.length === 2 ? [args[0], args[1]] : [args[0], Identity] + return { type: 'Union', parsers, mapping } as never +} diff --git a/src/static/parse.ts b/src/static/parse.ts index f4fc12e..4ee8430 100644 --- a/src/static/parse.ts +++ b/src/static/parse.ts @@ -30,29 +30,15 @@ import * as Tokens from './token' import * as Types from './types' // ------------------------------------------------------------------ -// Tuple +// Array // ------------------------------------------------------------------ // prettier-ignore -type TupleParser = ( - Parsers extends [infer Left extends Types.IParser, ...infer Right extends Types.IParser[]] - ? Parse extends [infer Value extends unknown, infer Rest extends string] - ? TupleParser - : [] +type ArrayParser = ( + Parse extends [infer Value1 extends unknown, infer Rest extends string] + ? ArrayParser : [Result, Code] ) -// ------------------------------------------------------------------ -// Union -// ------------------------------------------------------------------ -// prettier-ignore -type UnionParser = ( - Parsers extends [infer Left extends Types.IParser, ...infer Right extends Types.IParser[]] - ? Parse extends [infer Value extends unknown, infer Rest extends string] - ? [Value, Rest] - : UnionParser - : [] -) - // ------------------------------------------------------------------ // Const // ------------------------------------------------------------------ @@ -83,6 +69,16 @@ type NumberParser = ( : [] ) +// ------------------------------------------------------------------ +// Optional +// ------------------------------------------------------------------ +// prettier-ignore +type OptionalParser = ( + Parse extends [infer Value extends unknown, infer Rest extends string] + ? [[Value], Rest] + : [[], Code] +) + // ------------------------------------------------------------------ // String // ------------------------------------------------------------------ @@ -93,23 +89,50 @@ type StringParser = ( + Parsers extends [infer Left extends Types.IParser, ...infer Right extends Types.IParser[]] + ? Parse extends [infer Value extends unknown, infer Rest extends string] + ? TupleParser + : [] + : [Result, Code] +) + +// ------------------------------------------------------------------ +// Union +// ------------------------------------------------------------------ +// prettier-ignore +type UnionParser = ( + Parsers extends [infer Left extends Types.IParser, ...infer Right extends Types.IParser[]] + ? Parse extends [infer Value extends unknown, infer Rest extends string] + ? [Value, Rest] + : UnionParser + : [] +) + // ------------------------------------------------------------------ // Parse // ------------------------------------------------------------------ // prettier-ignore type ParseCode = ( - Type extends Types.Union ? UnionParser : - Type extends Types.Tuple ? TupleParser : - Type extends Types.Const ? ConstParser : - Type extends Types.String ? StringParser : + Type extends Types.Array ? ArrayParser : + Type extends Types.Const ? ConstParser : Type extends Types.Ident ? IdentParser : Type extends Types.Number ? NumberParser : + Type extends Types.Optional ? OptionalParser : + Type extends Types.String ? StringParser : + Type extends Types.Tuple ? TupleParser : + Type extends Types.Union ? UnionParser : [] ) // prettier-ignore type ParseMapping = ( (Parser['mapping'] & { input: Result, context: Context })['output'] ) + /** Parses code with the given parser */ // prettier-ignore export type Parse = ( diff --git a/src/static/types.ts b/src/static/types.ts index 551d1ac..fdfc2d9 100644 --- a/src/static/types.ts +++ b/src/static/types.ts @@ -29,16 +29,25 @@ THE SOFTWARE. // ------------------------------------------------------------------ // Mapping // ------------------------------------------------------------------ +/** + * `[ACTION]` Inference mapping base type. Used to specify semantic actions for + * Parser productions. This type is implemented as a higher-kinded type where + * productions are received on the `input` property with mapping assigned + * the `output` property. The parsing context is available on the `context` + * property. + */ export interface IMapping { context: unknown input: unknown output: unknown } -/** Maps input to output. This is the default Mapping */ + +/** `[ACTION]` Default inference mapping. */ export interface Identity extends IMapping { output: this['input'] } -/** Maps the output as the given parameter T */ + +/** `[ACTION]` Maps the given argument `T` as the mapping output */ export interface As extends IMapping { output: T } @@ -50,51 +59,75 @@ export interface IParser { type: string mapping: Mapping } + // ------------------------------------------------------------------ -// Tuple -// ------------------------------------------------------------------ -/** Creates a Tuple Parser */ -export interface Tuple extends IParser { - type: 'Tuple' - parsers: [...Parsers] -} -// ------------------------------------------------------------------ -// Union +// Array // ------------------------------------------------------------------ -/** Creates a Union Parser */ -export interface Union extends IParser { - type: 'Union' - parsers: [...Parsers] +/** `[EBNF]` Creates an Array Parser */ +export interface Array extends IParser { + type: 'Array' + parser: Parser } + // ------------------------------------------------------------------ // Const // ------------------------------------------------------------------ -/** Creates a Const Parser */ +/** `[TERM]` Creates a Const Parser */ export interface Const extends IParser { type: 'Const' value: Value } -// ------------------------------------------------------------------ -// String -// ------------------------------------------------------------------ -/** Creates a String Parser. Options are an array of permissable quote characters */ -export interface String extends IParser { - type: 'String' - quote: Options -} + // ------------------------------------------------------------------ // Ident // ------------------------------------------------------------------ -/** Creates an Ident Parser. */ +/** `[TERM]` Creates an Ident Parser. */ // prettier-ignore export interface Ident extends IParser { type: 'Ident' } + // ------------------------------------------------------------------ // Number // ------------------------------------------------------------------ -/** Creates a Number Parser. */ +/** `[TERM]` Creates a Number Parser. */ // prettier-ignore export interface Number extends IParser { type: 'Number' } + +// ------------------------------------------------------------------ +// Optional +// ------------------------------------------------------------------ +/** `[EBNF]` Creates a Optional Parser */ +export interface Optional extends IParser { + type: 'Optional' + parser: Parser +} + +// ------------------------------------------------------------------ +// String +// ------------------------------------------------------------------ +/** `[TERM]` Creates a String Parser. Options are an array of permissable quote characters */ +export interface String extends IParser { + type: 'String' + quote: Options +} + +// ------------------------------------------------------------------ +// Tuple +// ------------------------------------------------------------------ +/** `[BNF]` Creates a Tuple Parser */ +export interface Tuple extends IParser { + type: 'Tuple' + parsers: [...Parsers] +} + +// ------------------------------------------------------------------ +// Union +// ------------------------------------------------------------------ +/** `[BNF]` Creates a Union Parser */ +export interface Union extends IParser { + type: 'Union' + parsers: [...Parsers] +} diff --git a/test/runtime/parse.ts b/test/runtime/parse.ts index f30ba8e..bee7ab5 100644 --- a/test/runtime/parse.ts +++ b/test/runtime/parse.ts @@ -120,6 +120,23 @@ describe('Parse', () => { Assert(Runtime.Parse(Runtime.Ident(), 'A1 '), ['A1', ' ']) Assert(Runtime.Parse(Runtime.Ident(), ' A1 '), ['A1', ' ']) }) + + it('Array', () => { + Assert(Runtime.Parse(Runtime.Array(Runtime.Const('A')), ''), [[], '']) + Assert(Runtime.Parse(Runtime.Array(Runtime.Const('A')), 'AB'), [['A'], 'B']) + Assert(Runtime.Parse(Runtime.Array(Runtime.Const('A')), 'AAB'), [['A', 'A'], 'B']) + Assert(Runtime.Parse(Runtime.Array(Runtime.Const('AA')), 'AAB'), [['AA'], 'B']) + Assert(Runtime.Parse(Runtime.Array(Runtime.Const('AA')), 'AAAB'), [['AA'], 'AB']) + Assert(Runtime.Parse(Runtime.Array(Runtime.Const('AA')), 'B'), [[], 'B']) + }) + + it('Optional', () => { + Assert(Runtime.Parse(Runtime.Optional(Runtime.Const('A')), ''), [[], '']) + Assert(Runtime.Parse(Runtime.Optional(Runtime.Const('A')), 'A'), [['A'], '']) + Assert(Runtime.Parse(Runtime.Optional(Runtime.Const('A')), 'AA'), [['A'], 'A']) + Assert(Runtime.Parse(Runtime.Optional(Runtime.Const('A')), 'B'), [[], 'B']) + }) + it('Mapping', () => { const Mapping = (_0: 'A', _1: 'B', _2: 'C') => [_2, _1, _0] as const const Mapped = Runtime.Tuple([Runtime.Const('A'), Runtime.Const('B'), Runtime.Const('C')], values => Mapping(...values)) diff --git a/test/static/parse.ts b/test/static/parse.ts index ce2c625..0607be2 100644 --- a/test/static/parse.ts +++ b/test/static/parse.ts @@ -133,6 +133,24 @@ Assert, ['A1', '']>() Assert, ['A1', ' ']>() Assert, ['A1', ' ']>() +// ------------------------------------------------------------------ +// Array +// ------------------------------------------------------------------ +Assert>, ''>, [[], '']>() +Assert>, 'AB'>, [['A'], 'B']>() +Assert>, 'AAB'>, [['A', 'A'], 'B']>() +Assert>, 'AAB'>, [['AA'], 'B']>() +Assert>, 'AAAB'>, [['AA'], 'AB']>() +Assert>, 'B'>, [[], 'B']>() + +// ------------------------------------------------------------------ +// Optional +// ------------------------------------------------------------------ +Assert>, ''>, [[], '']>() +Assert>, 'A'>, [['A'], '']>() +Assert>, 'AA'>, [['A'], 'A']>() +Assert>, 'B'>, [[], 'B']>() + // ------------------------------------------------------------------ // Mapping // ------------------------------------------------------------------