@spakhm/ts-parsec 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +39 -0
- package/dist/index.js +8 -0
- package/package.json +1 -0
- package/src/base.ts +105 -0
- package/src/lib.ts +162 -0
- package/src/stream.ts +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
|
|
2
|
+
This is a parser combinator library written in typescript. Design
|
|
3
|
+
goals:
|
|
4
|
+
|
|
5
|
+
- Produces recursive descent parsers capable of parsing PEG grammars.
|
|
6
|
+
- For throwaway projects only. Will never grow big, have complex
|
|
7
|
+
optimizations, or other fancy featues.
|
|
8
|
+
- Small, so I can understand every detail. The library is under 500
|
|
9
|
+
lines of code and took maybe a couple of days to write.
|
|
10
|
+
- Type safe. The syntax tree types are inferred from the combinators.
|
|
11
|
+
It's beautiful and really fun to use.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
```sh
|
|
15
|
+
npm install @spakhm/ts-parsec
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Example
|
|
19
|
+
|
|
20
|
+
Here is a simple example:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
const digit = range('0', '9');
|
|
24
|
+
const lower = range('a', 'z');
|
|
25
|
+
const upper = range('A', 'Z');
|
|
26
|
+
const alpha = either(lower, upper);
|
|
27
|
+
const alnum = either(alpha, digit);
|
|
28
|
+
|
|
29
|
+
const ident = lex(seq(alpha, many(alnum))).map(([first, rest]) =>
|
|
30
|
+
[first, ...rest].join(""));
|
|
31
|
+
|
|
32
|
+
const input = "Hello";
|
|
33
|
+
const stream = fromString(input);
|
|
34
|
+
ident(stream);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Limitations
|
|
38
|
+
|
|
39
|
+
- No error reporting at all.
|
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"0.1.2","license":"MIT","main":"dist/index.js","typings":"dist/index.d.ts","files":["dist","src"],"engines":{"node":">=10"},"scripts":{"start":"tsdx watch","build":"tsdx build","test":"tsdx test","lint":"tsdx lint","prepare":"tsdx build","size":"size-limit","analyze":"size-limit --why"},"husky":{"hooks":{"pre-commit":"tsdx lint"}},"prettier":{"printWidth":80,"semi":true,"singleQuote":true,"trailingComma":"es5"},"name":"@spakhm/ts-parsec","author":"Slava Akhmechet","module":"dist/ts-parsec.esm.js","size-limit":[{"path":"dist/ts-parsec.cjs.production.min.js","limit":"10 KB"},{"path":"dist/ts-parsec.esm.js","limit":"10 KB"}],"devDependencies":{"@size-limit/preset-small-lib":"^11.1.6","husky":"^9.1.6","size-limit":"^11.1.6","tsdx":"^0.14.1","tslib":"^2.7.0","typescript":"^3.9.10"}}
|
package/src/base.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { stream } from './stream';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Result handling
|
|
5
|
+
*/
|
|
6
|
+
export type result<T, E> = { type: 'ok', res: T, } | { type: 'err', err: E, };
|
|
7
|
+
export type parser_error = { row: number, col: number, msg: string, };
|
|
8
|
+
|
|
9
|
+
export const ok = <T>(res: T): result<T, never> => ({ type: 'ok', res, });
|
|
10
|
+
export const err = (row: number, col: number, msg: string): result<never, parser_error> =>
|
|
11
|
+
({ type: 'err', err: { row, col, msg, }});
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
Parser types
|
|
15
|
+
*/
|
|
16
|
+
export type parserFn<T> = (source: stream) => result<T, parser_error>;
|
|
17
|
+
export type parser<T> = parserFn<T> & {
|
|
18
|
+
map: <U>(fn: ((value: T) => U)) => parser<U>,
|
|
19
|
+
};
|
|
20
|
+
export type parserlike<T> = parserFn<T> | parser<T> | string;
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
Allowing functions and strings to act like parsers
|
|
24
|
+
*/
|
|
25
|
+
export function toParser<T extends string>(p: T): parser<T>;
|
|
26
|
+
export function toParser<T>(p: parserlike<T>): parser<T>;
|
|
27
|
+
export function toParser <T>(pl: parserlike<T>) {
|
|
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<T> = pl as parser<T>;
|
|
37
|
+
|
|
38
|
+
fn_.map = <U>(fnTransform: (value: T) => U): parser<U> => {
|
|
39
|
+
return toParser((source: stream): result<U, parser_error> => {
|
|
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 = <T extends string>(match: T): parser<T> =>
|
|
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 = <T>(p: parserlike<T>) => keepWs((source: stream) => {
|
|
66
|
+
ws(source);
|
|
67
|
+
return toParser(p)(source);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export const keepWs = <T>(p: parserlike<T>) =>
|
|
71
|
+
toParser((source: stream) => {
|
|
72
|
+
const prev_drop_ws = source.drop_ws;
|
|
73
|
+
source.drop_ws = false;
|
|
74
|
+
const res = toParser(p)(source);
|
|
75
|
+
source.drop_ws = prev_drop_ws;
|
|
76
|
+
return res;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export const ws = toParser((source: stream) => {
|
|
80
|
+
while (true) {
|
|
81
|
+
source.push();
|
|
82
|
+
const ch = source.next();
|
|
83
|
+
if (ch?.trim() === "") {
|
|
84
|
+
source.pop_continue();
|
|
85
|
+
} else {
|
|
86
|
+
source.pop_rollback();
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return ok({});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
/*
|
|
94
|
+
Laziness helper
|
|
95
|
+
*/
|
|
96
|
+
export const fwd = <T>(thunk: (() => parserlike<T>)): parser<T> =>
|
|
97
|
+
toParser((source: stream) => toParser(thunk())(source));
|
|
98
|
+
|
|
99
|
+
/*
|
|
100
|
+
TODO:
|
|
101
|
+
- In `either('foo').map(...)` the string 'foo' gets mapped to unknown.
|
|
102
|
+
Should fix that.
|
|
103
|
+
- If I could push infinite regress through map, it would be trivial to
|
|
104
|
+
just specify the AST type in map, and avoid the trick in `form`.
|
|
105
|
+
*/
|
package/src/lib.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
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 = <T>(parser: parserlike<T>): parser<T> =>
|
|
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<string> =>
|
|
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 = <Ts extends any[]>(...parsers: { [K in keyof Ts]: parserlike<Ts[K]> }): parser<Ts[number]> =>
|
|
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<T extends any[]> = parser<T> & {
|
|
37
|
+
map2: <U>(fn: ((...values: T) => U)) => parser<U>,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const seq = <Ts extends any[]>(...parsers: { [K in keyof Ts]: parserlike<Ts[K]> }): seq_parser<Ts> => {
|
|
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<Ts>;
|
|
53
|
+
p.map2 = <U>(fn: ((...values: Ts) => U)) =>
|
|
54
|
+
p.map(x => fn(...x));
|
|
55
|
+
return p;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const many = <T>(parser: parserlike<T>): parser<T[]> =>
|
|
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 = <T>(parser: parserlike<T>): parser<T[]> =>
|
|
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 = <T>(p: parserlike<T>) =>
|
|
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 = <T, U>(item: parserlike<T>, sep: parserlike<U>, allowTrailingSep: boolean = true): parser<T[]> =>
|
|
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 ok(res);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const res_ = attempt(item)(source);
|
|
118
|
+
if (res_.type == 'err') {
|
|
119
|
+
return allowTrailingSep ? ok(res) : err(0, 0, '');
|
|
120
|
+
} else {
|
|
121
|
+
res.push(res_.res);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
export function binop<O, D, N>(
|
|
127
|
+
operator: parserlike<O>,
|
|
128
|
+
operand: parserlike<D>,
|
|
129
|
+
makeNode: (op: O, left: D | N, right: D) => N
|
|
130
|
+
): parser<N | D> {
|
|
131
|
+
return toParser((source: stream) => {
|
|
132
|
+
const p = seq(operand, many(seq(operator, operand))).map2<N | D>((left, rights) => {
|
|
133
|
+
const acc = rights.reduce<N | D>(
|
|
134
|
+
(acc, [op, right]) => makeNode(op, acc, right), left);
|
|
135
|
+
return acc;
|
|
136
|
+
});
|
|
137
|
+
return p(source);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function binopr<O, D, N>(
|
|
142
|
+
operator: parserlike<O>,
|
|
143
|
+
operand: parserlike<D>,
|
|
144
|
+
makeNode: (op: O, left: D, right: D | N) => N
|
|
145
|
+
): parser<N | D> {
|
|
146
|
+
return toParser((source: stream) => {
|
|
147
|
+
const p = seq(operand, many(seq(operator, operand))).map2<N | D>((left, rights) => {
|
|
148
|
+
if (rights.length === 0) return left;
|
|
149
|
+
|
|
150
|
+
// Start from the last operand and reduce from right to left
|
|
151
|
+
let acc: D | N = rights[rights.length - 1][1];
|
|
152
|
+
for (let i = rights.length - 2; i >= 0; i--) {
|
|
153
|
+
const [op, right] = rights[i];
|
|
154
|
+
acc = makeNode(op, right, acc);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return makeNode(rights[0][0], left, acc);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return p(source);
|
|
161
|
+
});
|
|
162
|
+
}
|
package/src/stream.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
}
|