├── bun.lockb ├── .gitignore ├── src ├── index.ts ├── stream.ts ├── base.ts └── lib.ts ├── test ├── eof.test.ts ├── calc.test.ts ├── sepby.test.ts └── parser.test.ts ├── package.json ├── tsconfig.json ├── LICENSE └── README.md /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coffeemug/ts-parsec/HEAD/bun.lockb -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | .vscode 6 | .claude 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './base'; 3 | export * from './lib'; 4 | export * from './stream'; -------------------------------------------------------------------------------- /test/eof.test.ts: -------------------------------------------------------------------------------- 1 | import { ok, err } from '../src/base'; 2 | import { eof, nat, seq } from '../src/lib'; 3 | import { fromString } from '../src/stream'; 4 | 5 | it('eof succeeds at end of input', () => { 6 | expect(eof(fromString(""))).toEqual(ok(null)); 7 | }); 8 | 9 | it('eof fails when input remains', () => { 10 | expect(eof(fromString("abc"))).toEqual(err(0, 0, '')); 11 | }); 12 | 13 | it('eof succeeds after consuming all input', () => { 14 | expect(seq(nat, eof)(fromString("123"))).toEqual(ok([123, null])); 15 | }); 16 | 17 | it('eof fails when input remains after parsing', () => { 18 | expect(seq(nat, eof)(fromString("123abc"))).toEqual(err(0, 0, '')); 19 | }); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@spakhm/ts-parsec", 3 | "version": "0.2.0", 4 | "license": "MIT", 5 | "author": "Slava Akhmechet", 6 | "type": "module", 7 | "main": "dist/index.js", 8 | "module": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "files": ["dist", "src"], 11 | "scripts": { 12 | "build": "bun build ./src/index.ts --outdir ./dist --target node && bun run build:types", 13 | "build:types": "tsc --emitDeclarationOnly", 14 | "test": "bun test", 15 | "prepublishOnly": "bun run build" 16 | }, 17 | "devDependencies": { 18 | "@types/bun": "^1.1.14", 19 | "typescript": "^5.7.2" 20 | }, 21 | "prettier": { 22 | "printWidth": 80, 23 | "semi": true, 24 | "singleQuote": true, 25 | "trailingComma": "es5" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "target": "ES2022", 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "lib": ["ES2022"], 8 | 9 | // Output 10 | "outDir": "./dist", 11 | "rootDir": "./src", 12 | "declaration": true, 13 | "declarationMap": true, 14 | 15 | // Strict 16 | "strict": true, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | 22 | // Interop 23 | "esModuleInterop": true, 24 | "skipLibCheck": true, 25 | "forceConsistentCasingInFileNames": true, 26 | 27 | // Emit only declarations (bun handles JS) 28 | "noEmit": false, 29 | "emitDeclarationOnly": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/calc.test.ts: -------------------------------------------------------------------------------- 1 | import { fwd, ok, str } from "../src/base"; 2 | import { either, int, binop, binopr } from "../src/lib"; 3 | import { fromString } from "../src/stream"; 4 | 5 | type node = ['+' | '-' | '*' | '/', node, node] | number; 6 | const term = fwd(() => binop(either(str('+'), str('-')), factor, 7 | (a, b: node, c): node => [a, b, c])); 8 | const factor = binop(either(str('*'), str('/')), int, 9 | (a, b: node, c): node => [a, b, c]); 10 | 11 | it('', () => { 12 | expect(term(fromString("1+2"))).toEqual(ok(['+', 1, 2])); 13 | expect(term(fromString("1+2+3"))).toEqual(ok(['+', ['+', 1, 2], 3])); 14 | expect(term(fromString("1+2*3"))).toEqual(ok(['+', 1, ['*', 2, 3]])); 15 | }); 16 | 17 | const assign = binopr(str('='), int, (a, b, c) => [a, b, c]); 18 | 19 | it('', () => { 20 | expect(assign(fromString("1=2=3"))).toEqual(ok(['=', 1, ['=', 2, 3]])); 21 | }); 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Slava Akhmechet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | This is a parser combinator library written in typescript. You can 3 | find a writeup about it [here](https://www.spakhm.com/ts-parsec). Design 4 | goals: 5 | 6 | - Produces recursive descent parsers capable of parsing PEG grammars. 7 | - For throwaway projects only. Will never grow big, have complex 8 | optimizations, or other fancy featues. 9 | - Small, so I can understand every detail. The library is under 500 10 | lines of code and took maybe a couple of days to write. 11 | - Type safe. The syntax tree types are inferred from the combinators. 12 | It's beautiful and really fun to use. 13 | 14 | ## Installation 15 | ```sh 16 | npm install @spakhm/ts-parsec 17 | ``` 18 | 19 | ## Example 20 | 21 | Here is a simple example: 22 | 23 | ```ts 24 | const digit = range('0', '9'); 25 | const lower = range('a', 'z'); 26 | const upper = range('A', 'Z'); 27 | const alpha = either(lower, upper); 28 | const alnum = either(alpha, digit); 29 | 30 | const ident = lex(seq(alpha, many(alnum))).map(([first, rest]) => 31 | [first, ...rest].join("")); 32 | 33 | const input = "Hello"; 34 | const stream = fromString(input); 35 | ident(stream); 36 | ``` 37 | 38 | ## Limitations 39 | 40 | - No error reporting at all. 41 | -------------------------------------------------------------------------------- /src/stream.ts: -------------------------------------------------------------------------------- 1 | 2 | export type stream = { 3 | row: number, 4 | col: number, 5 | drop_ws: boolean, 6 | next: () => string | null, 7 | push: () => void, 8 | pop_continue: () => void, 9 | pop_rollback: () => void, 10 | }; 11 | 12 | class string_stream { 13 | row: number = 1; 14 | col: number = 1; 15 | idx: number = 0; 16 | 17 | stack: { 18 | row: number, 19 | col: number, 20 | idx: number, 21 | }[] = []; 22 | 23 | constructor(public source: string, public drop_ws: boolean = true) {} 24 | 25 | next(): string | null { 26 | if (this.idx == this.source.length) { 27 | return null; 28 | } 29 | const ch = this.source[this.idx++]; 30 | this.col++; 31 | if (ch == '\n') { 32 | this.row++; 33 | this.col = 1; 34 | } 35 | 36 | if (this.drop_ws && ch.trim() === "") { 37 | return this.next(); 38 | } else { 39 | return ch; 40 | } 41 | } 42 | 43 | push() { 44 | this.stack.push({ 45 | row: this.row, col: this.col, idx: this.idx, 46 | }) 47 | } 48 | 49 | pop_continue() { 50 | this.stack.pop(); 51 | } 52 | 53 | pop_rollback() { 54 | const x = this.stack.pop()!; 55 | this.row = x.row; 56 | this.col = x.col; 57 | this.idx = x.idx; 58 | } 59 | } 60 | 61 | export const fromString = (source: string): stream => { 62 | return new string_stream(source); 63 | } 64 | -------------------------------------------------------------------------------- /test/sepby.test.ts: -------------------------------------------------------------------------------- 1 | import { ok, err } from '../src/base'; 2 | import { nat, sepBy, sepBy1 } from '../src/lib'; 3 | import { fromString } from '../src/stream'; 4 | 5 | it('sepBy with trailingSep allow (default)', () => { 6 | expect(sepBy(nat, ',')(fromString(""))).toEqual(ok([])); 7 | expect(sepBy(nat, ',')(fromString("12"))).toEqual(ok([12])); 8 | expect(sepBy(nat, ',')(fromString("12,23,34"))).toEqual(ok([12,23,34])); 9 | expect(sepBy(nat, ',')(fromString("12,23,34,"))).toEqual(ok([12,23,34])); 10 | }); 11 | 12 | it('sepBy with trailingSep forbid', () => { 13 | expect(sepBy(nat, ',', 'forbid')(fromString(""))).toEqual(ok([])); 14 | expect(sepBy(nat, ',', 'forbid')(fromString("12"))).toEqual(ok([12])); 15 | expect(sepBy(nat, ',', 'forbid')(fromString("12,23,34"))).toEqual(ok([12,23,34])); 16 | expect(sepBy(nat, ',', 'forbid')(fromString("12,23,34,"))).toEqual(err(0, 0, '')); 17 | }); 18 | 19 | it('sepBy with trailingSep require', () => { 20 | expect(sepBy(nat, ',', 'require')(fromString(""))).toEqual(ok([])); 21 | expect(sepBy(nat, ',', 'require')(fromString("12"))).toEqual(err(0, 0, '')); 22 | expect(sepBy(nat, ',', 'require')(fromString("12,23,34"))).toEqual(err(0, 0, '')); 23 | expect(sepBy(nat, ',', 'require')(fromString("12,23,34,"))).toEqual(ok([12,23,34])); 24 | }); 25 | 26 | it('sepBy1', () => { 27 | expect(sepBy1(nat, ',')(fromString(""))).toEqual(err(0, 0, '')); 28 | expect(sepBy1(nat, ',')(fromString("12"))).toEqual(ok([12])); 29 | expect(sepBy1(nat, ',')(fromString("12,23,34"))).toEqual(ok([12,23,34])); 30 | expect(sepBy1(nat, ',')(fromString("12,23,34,"))).toEqual(ok([12,23,34])); 31 | }); 32 | 33 | it('sepBy1 with trailingSep options', () => { 34 | expect(sepBy1(nat, ',', 'forbid')(fromString("12,23,"))).toEqual(err(0, 0, '')); 35 | expect(sepBy1(nat, ',', 'require')(fromString("12,23"))).toEqual(err(0, 0, '')); 36 | expect(sepBy1(nat, ',', 'require')(fromString("12,23,"))).toEqual(ok([12,23])); 37 | }); 38 | -------------------------------------------------------------------------------- /test/parser.test.ts: -------------------------------------------------------------------------------- 1 | import { ok, err, str } from '../src/base'; 2 | import { nat, either, many, seq, some, alnum, sepBy, maybe, int, anych } from '../src/lib'; 3 | import { fromString } from '../src/stream'; 4 | 5 | it('', () => { 6 | expect(either('1', 'a')(fromString('12'))).toEqual(ok('1')); 7 | expect(either('a', '1')(fromString('12'))).toEqual(ok('1')); 8 | expect(seq('1', '2')(fromString('12'))).toEqual(ok(['1', '2'])); 9 | }); 10 | 11 | it('', () => { 12 | expect(nat(fromString('171hello'))).toEqual(ok(171)); 13 | expect(str('171')(fromString('171hello'))).toEqual(ok('171')); 14 | 15 | expect(either(nat, '171')(fromString('171hello'))).toEqual(ok(171)); 16 | expect(either('171', nat)(fromString('171hello'))).toEqual(ok("171")); 17 | 18 | expect(either(nat, str('hi'))(fromString('hi'))).toEqual(ok('hi')); 19 | }); 20 | 21 | it('', () => { 22 | expect(many(str('hi'))(fromString(''))).toEqual(ok([])); 23 | expect(many(str('hi'))(fromString('foo'))).toEqual(ok([])); 24 | expect(many(str('hi'))(fromString('hi'))).toEqual(ok(['hi'])); 25 | expect(many(str('hi'))(fromString('hihi'))).toEqual(ok(['hi', 'hi'])); 26 | expect(many(str('hi'))(fromString('hifoo'))).toEqual(ok(['hi'])); 27 | }); 28 | 29 | it('', () => { 30 | expect(some(str('hi'))(fromString('hi'))).toEqual(ok(['hi'])); 31 | expect(some(str('hi'))(fromString('hihi'))).toEqual(ok(['hi', 'hi'])); 32 | expect(some(str('hi'))(fromString('hifoo'))).toEqual(ok(['hi'])); 33 | }); 34 | 35 | it('', () => { 36 | expect(alnum(fromString("0"))).toEqual(ok('0')); 37 | expect(alnum(fromString("1"))).toEqual(ok('1')); 38 | expect(alnum(fromString("5"))).toEqual(ok('5')); 39 | expect(alnum(fromString("9"))).toEqual(ok('9')); 40 | expect(alnum(fromString("a"))).toEqual(ok('a')); 41 | expect(alnum(fromString("m"))).toEqual(ok('m')); 42 | expect(alnum(fromString("z"))).toEqual(ok('z')); 43 | expect(alnum(fromString("A"))).toEqual(ok('A')); 44 | expect(alnum(fromString("M"))).toEqual(ok('M')); 45 | expect(alnum(fromString("Z"))).toEqual(ok('Z')); 46 | }); 47 | 48 | it('', () => { 49 | expect(sepBy(nat, ',')(fromString(""))).toEqual(ok([])); 50 | expect(sepBy(nat, ',')(fromString("12"))).toEqual(ok([12])); 51 | expect(sepBy(nat, ',')(fromString("12,23,34"))).toEqual(ok([12,23,34])); 52 | expect(sepBy(nat, ',')(fromString("12,23,34,"))).toEqual(ok([12,23,34])); 53 | }); 54 | 55 | it('', () => { 56 | expect(maybe('foo')(fromString('foo'))).toEqual(ok('foo')); 57 | expect(maybe('foo')(fromString('bar'))).toEqual(ok(null)); 58 | }); 59 | 60 | it('', () => { 61 | expect(int(fromString('12'))).toEqual(ok(12)); 62 | expect(int(fromString('-12'))).toEqual(ok(-12)); 63 | expect(int(fromString('+12'))).toEqual(ok(12)); 64 | expect(int(fromString('- 12'))).toEqual(ok(-12)); 65 | expect(int(fromString('+ 12'))).toEqual(ok(12)); 66 | }); 67 | 68 | it('', () => { 69 | expect(anych()(fromString('12'))).toEqual(ok('1')); 70 | expect(anych()(fromString('!!!'))).toEqual(ok('!')); 71 | expect(anych({ but: '!' })(fromString('!!!'))).toEqual(err(0, 0, '')); 72 | }); -------------------------------------------------------------------------------- /src/base.ts: -------------------------------------------------------------------------------- 1 | import type { stream } from './stream'; 2 | 3 | /* 4 | Result handling 5 | */ 6 | export type result = { type: 'ok', res: T, } | { type: 'err', err: E, }; 7 | export type parser_error = { row: number, col: number, msg: string, }; 8 | 9 | export const ok = (res: T): result => ({ type: 'ok', res, }); 10 | export const err = (row: number, col: number, msg: string): result => 11 | ({ type: 'err', err: { row, col, msg, }}); 12 | 13 | /* 14 | Parser types 15 | */ 16 | export type parserFn = (source: stream) => result; 17 | export type parser = parserFn & { 18 | map: (fn: ((value: T) => U)) => parser, 19 | }; 20 | export type parserlike = parserFn | parser | string; 21 | 22 | /* 23 | Allowing functions and strings to act like parsers 24 | */ 25 | export function toParser(p: T): parser; 26 | export function toParser(p: parserlike): parser; 27 | export function toParser (pl: parserlike) { 28 | if (typeof pl == 'string') { 29 | return str(pl); 30 | } 31 | 32 | if ('map' in pl) { 33 | return pl; 34 | } 35 | 36 | const fn_: parser = pl as parser; 37 | 38 | fn_.map = (fnTransform: (value: T) => U): parser => { 39 | return toParser((source: stream): result => { 40 | const res = fn_(source); 41 | if (res.type == 'ok') { 42 | return ok(fnTransform(res.res)); 43 | } else { 44 | return res; 45 | } 46 | }); 47 | }; 48 | 49 | return fn_; 50 | } 51 | 52 | /* 53 | The most basic of parsers 54 | */ 55 | export const str = (match: T): parser => 56 | lex(toParser((source: stream) => { 57 | for (let i = 0; i < match.length; i++) { 58 | if(source.next() != match[i]) { 59 | return err(0, 0, ''); 60 | } 61 | } 62 | return ok(match); 63 | })); 64 | 65 | export const lex = (p: parserlike) => toParser((source: stream) => { 66 | if (!source.drop_ws) { 67 | // this call to lex is nested (i.e. we're in lex mode already) 68 | // don't drop more whitespace 69 | return toParser(p)(source); 70 | } else { 71 | return keepWs((source: stream) => { 72 | ws(source); 73 | return toParser(p)(source); 74 | })(source); 75 | } 76 | }); 77 | 78 | export const keepWs = (p: parserlike) => 79 | toParser((source: stream) => { 80 | const prev_drop_ws = source.drop_ws; 81 | source.drop_ws = false; 82 | const res = toParser(p)(source); 83 | source.drop_ws = prev_drop_ws; 84 | return res; 85 | }); 86 | 87 | export const ws = toParser((source: stream) => { 88 | while (true) { 89 | source.push(); 90 | const ch = source.next(); 91 | if (ch?.trim() === "") { 92 | source.pop_continue(); 93 | } else { 94 | source.pop_rollback(); 95 | break; 96 | } 97 | } 98 | return ok({}); 99 | }); 100 | 101 | /* 102 | Laziness helper 103 | */ 104 | export const fwd = (thunk: (() => parserlike)): parser => 105 | toParser((source: stream) => toParser(thunk())(source)); 106 | 107 | /* 108 | TODO: 109 | - In `either('foo').map(...)` the string 'foo' gets mapped to unknown. 110 | Should fix that. 111 | - If I could push infinite regress through map, it would be trivial to 112 | just specify the AST type in map, and avoid the trick in `form`. 113 | */ 114 | -------------------------------------------------------------------------------- /src/lib.ts: -------------------------------------------------------------------------------- 1 | import type { stream } from './stream'; 2 | import type { parser, parserlike } from './base'; 3 | import { err, ok, toParser, lex } from './base'; 4 | 5 | export const attempt = (parser: parserlike): parser => 6 | toParser((source: stream) => { 7 | source.push(); 8 | const res = toParser(parser)(source); 9 | if (res.type == 'ok') { 10 | source.pop_continue(); 11 | } else { 12 | source.pop_rollback(); 13 | } 14 | return res; 15 | }); 16 | 17 | export const range = (start: string, end: string): parser => 18 | toParser((source: stream) => { 19 | const next = source.next(); 20 | if (!next) return err(0, 0, ''); 21 | if (next >= start[0] && next <= end[0]) return ok(next); 22 | return err(0, 0, ''); 23 | }); 24 | 25 | export const either = (...parsers: { [K in keyof Ts]: parserlike }): parser => 26 | toParser((source: stream) => { 27 | for (const parser of parsers) { 28 | const res = attempt(parser)(source); 29 | if (res.type == 'ok') { 30 | return res; 31 | } 32 | } 33 | return err(0, 0, ''); 34 | }); 35 | 36 | export type seq_parser = parser & { 37 | map2: (fn: ((...values: T) => U)) => parser, 38 | }; 39 | 40 | export const seq = (...parsers: { [K in keyof Ts]: parserlike }): seq_parser => { 41 | const p = toParser((source: stream) => { 42 | const res: unknown[] = []; 43 | for (const parser of parsers) { 44 | const res_ = toParser(parser)(source); 45 | if (res_.type == 'ok') { 46 | res.push(res_.res); 47 | } else { 48 | return err(0, 0, ''); 49 | } 50 | } 51 | return ok(res as any); 52 | }) as seq_parser; 53 | p.map2 = (fn: ((...values: Ts) => U)) => 54 | p.map(x => fn(...x)); 55 | return p; 56 | } 57 | 58 | export const many = (parser: parserlike): parser => 59 | toParser((source: stream) => { 60 | const res: T[] = []; 61 | while (true) { 62 | const _res = attempt(parser)(source); 63 | if (_res.type == 'ok') { 64 | res.push(_res.res); 65 | } else { 66 | break; 67 | } 68 | } 69 | 70 | return ok(res); 71 | }); 72 | 73 | export const some = (parser: parserlike): parser => 74 | seq(parser, many(parser)).map2((ft, rt) => [ft, ...rt]); 75 | 76 | export const digit = range('0', '9'); 77 | 78 | export const nat = lex(some(digit)).map((val) => 79 | parseInt(val.join(""))); 80 | 81 | export const maybe = (p: parserlike) => 82 | toParser((source: stream) => { 83 | const res = attempt(p)(source); 84 | return res.type == 'ok' ? res : ok(null); 85 | }); 86 | 87 | export const int = seq(maybe(either('-', '+')), nat).map2((sign, val) => { 88 | if (sign === '-') { 89 | return -val; 90 | } else { 91 | return val; 92 | } 93 | }); 94 | 95 | export const lower = range('a', 'z'); 96 | export const upper = range('A', 'Z'); 97 | export const alpha = either(lower, upper); 98 | export const alnum = either(alpha, digit); 99 | 100 | export const sepBy = (item: parserlike, sep: parserlike, trailingSep: 'allow' | 'forbid' | 'require' = 'allow'): parser => 101 | toParser((source: stream) => { 102 | const res: T[] = []; 103 | 104 | const res_ = attempt(item)(source); 105 | if (res_.type == 'err') { 106 | return ok(res); 107 | } else { 108 | res.push(res_.res); 109 | } 110 | 111 | while (true) { 112 | const sepres_ = attempt(sep)(source); 113 | if (sepres_.type == 'err') { 114 | return trailingSep === 'require' ? err(0, 0, '') : ok(res); 115 | } 116 | 117 | const res_ = attempt(item)(source); 118 | if (res_.type == 'err') { 119 | return trailingSep === 'forbid' ? err(0, 0, '') : ok(res); 120 | } else { 121 | res.push(res_.res); 122 | } 123 | } 124 | }); 125 | 126 | export const sepBy1 = (item: parserlike, sep: parserlike, trailingSep: 'allow' | 'forbid' | 'require' = 'allow'): parser => 127 | toParser((source: stream) => { 128 | const res = sepBy(item, sep, trailingSep)(source); 129 | if (res.type == 'err') return res; 130 | return res.res.length >= 1 ? res : err(0, 0, ''); 131 | }); 132 | 133 | export function binop( 134 | operator: parserlike, 135 | operand: parserlike, 136 | makeNode: (op: O, left: D | N, right: D) => N 137 | ): parser { 138 | return toParser((source: stream) => { 139 | const p = seq(operand, many(seq(operator, operand))).map2((left, rights) => { 140 | const acc = rights.reduce( 141 | (acc, [op, right]) => makeNode(op, acc, right), left); 142 | return acc; 143 | }); 144 | return p(source); 145 | }); 146 | } 147 | 148 | export function binopr( 149 | operator: parserlike, 150 | operand: parserlike, 151 | makeNode: (op: O, left: D, right: D | N) => N 152 | ): parser { 153 | return toParser((source: stream) => { 154 | const p = seq(operand, many(seq(operator, operand))).map2((left, rights) => { 155 | if (rights.length === 0) return left; 156 | 157 | // Start from the last operand and reduce from right to left 158 | let acc: D | N = rights[rights.length - 1][1]; 159 | for (let i = rights.length - 2; i >= 0; i--) { 160 | const [op, right] = rights[i]; 161 | acc = makeNode(op, right, acc); 162 | } 163 | 164 | return makeNode(rights[0][0], left, acc); 165 | }); 166 | 167 | return p(source); 168 | }); 169 | } 170 | 171 | export const noop = toParser((_: stream) => ok(true)); 172 | 173 | export const not = (p: parserlike) => toParser((source: stream) => { 174 | const res = toParser(p)(source); 175 | if (res.type == 'ok') { 176 | return err(0, 0, ""); 177 | } else { 178 | return ok(null); 179 | } 180 | }); 181 | 182 | export const peek = (p: parserlike) => toParser((source: stream) => { 183 | source.push(); 184 | const res = toParser(p)(source); 185 | source.pop_rollback(); 186 | return res; 187 | }); 188 | 189 | export const anych = (opts?: { but: parserlike }) => toParser((source: stream) => { 190 | if (opts?.but) { 191 | const res = peek(opts.but)(source); 192 | if (res.type == 'ok') { 193 | return err(0, 0, ""); 194 | } 195 | } 196 | const res = source.next() 197 | return res ? ok(res) : err(0, 0, ""); 198 | }); 199 | 200 | export const eof = not(anych()); 201 | --------------------------------------------------------------------------------