parseman 0.1.6 → 0.2.0
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/README.md +27 -33
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
# Parséman (PAR-zə-mahn)
|
|
6
6
|
|
|
7
|
-
Write parsers
|
|
7
|
+
Write parsers in TypeScript — fast enough to run as-is, and blazing fast when the bundler macro kicks in. Same code either way; no grammar files, no generated output to check in. Drop the plugin in tests or anywhere a bundler isn't around and everything still works.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
_Note: Not necessarily production-ready! I still have test cases and sample parsers I want to build to more rigorously test the API and performance._
|
|
10
10
|
|
|
11
11
|
## Benchmarks
|
|
12
12
|
|
|
@@ -16,7 +16,9 @@ Measured on Apple M2 Pro. Bars show µs per parse — shorter is faster.
|
|
|
16
16
|
|
|
17
17
|

|
|
18
18
|
|
|
19
|
-
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
On JSON, Parséman compiled matches Peggy at small and medium sizes; Peggy pulls ahead by ~10% at 12 kB — it's been doing this a while. On CSV and GraphQL, where the grammar is non-recursive or fully inlineable, Parséman compiled is the clear winner. The `w/ .compile()` bar shows total time including the one-time compile step; the dark overlay shows parse-only time (same compiled code as the macro build).
|
|
20
22
|
|
|
21
23
|
---
|
|
22
24
|
|
|
@@ -126,8 +128,8 @@ Pure combinator trees — `literal`, `regex`, `sequence`, `choice`, `many`, `one
|
|
|
126
128
|
| `sepBy(parser, sep)` | Zero or more `parser` separated by `sep`. |
|
|
127
129
|
| `transform(parser, fn)` | Map the result: `fn(value, span) → newValue`. |
|
|
128
130
|
| `skip(main, skipped)` | Match `main` then `skipped`; return `main`'s value. |
|
|
129
|
-
| `
|
|
130
|
-
| `ref<T>()` | Low-level forward declaration slot (use `
|
|
131
|
+
| `rules(factory)` | Named grammar rules — no forward declarations needed, handles mutual recursion. |
|
|
132
|
+
| `ref<T>()` | Low-level forward declaration slot (use `rules()` in most cases). |
|
|
131
133
|
| `not(parser)` | Negative lookahead — succeeds (consuming nothing) when `parser` fails. |
|
|
132
134
|
| `guard(predicate)` | Succeeds only when `predicate(ctx)` returns true; used for context-sensitive rules. |
|
|
133
135
|
| `withCtx(extra, parser)` | Merge `extra` into the user context for the duration of `parser`. |
|
|
@@ -139,16 +141,16 @@ Pure combinator trees — `literal`, `regex`, `sequence`, `choice`, `many`, `one
|
|
|
139
141
|
|
|
140
142
|
## Whitespace and comment skipping
|
|
141
143
|
|
|
142
|
-
|
|
144
|
+
Wrap your root combinator with `parser()` to declare trivia — whitespace and comments to skip between tokens. This bakes the setting into the combinator tree so all modes (interpreter, `compile()`, macro build) behave identically:
|
|
143
145
|
|
|
144
146
|
```ts
|
|
145
|
-
import { regex, sepBy, literal
|
|
147
|
+
import { parser, regex, trivia, sepBy, literal } from 'parseman'
|
|
146
148
|
|
|
147
|
-
const ws = regex(/\s*/)
|
|
149
|
+
const ws = trivia(regex(/\s*/))
|
|
148
150
|
const word = regex(/[a-z]+/)
|
|
149
|
-
const list = sepBy(word, literal(','))
|
|
151
|
+
const list = parser({ trivia: ws }, sepBy(word, literal(',')))
|
|
150
152
|
|
|
151
|
-
parse(
|
|
153
|
+
list.parse('foo , bar , baz')
|
|
152
154
|
// { ok: true, value: ['foo', 'bar', 'baz'], ... }
|
|
153
155
|
```
|
|
154
156
|
|
|
@@ -157,15 +159,7 @@ Multiple trivia types — whitespace and comments — combine with `choice()` an
|
|
|
157
159
|
```ts
|
|
158
160
|
const lineComment = sequence(literal('//'), regex(/[^\n]*/))
|
|
159
161
|
const blockComment = sequence(literal('/*'), scanTo(literal('*/'), []))
|
|
160
|
-
const
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
Use `grammar(opts, root)` instead of the `parse()` trivia option when you want trivia only for a subtree within a larger parse:
|
|
164
|
-
|
|
165
|
-
```ts
|
|
166
|
-
import { grammar } from 'parseman'
|
|
167
|
-
|
|
168
|
-
const jsonValue = grammar({ trivia: ws }, choice(object, array, str, num, bool, nil))
|
|
162
|
+
const ws = trivia(many(choice(regex(/\s+/), lineComment, blockComment)))
|
|
169
163
|
```
|
|
170
164
|
|
|
171
165
|
---
|
|
@@ -198,20 +192,20 @@ const op = choice(literal('instanceof'), literal('in'), literal('if'))
|
|
|
198
192
|
|
|
199
193
|
---
|
|
200
194
|
|
|
201
|
-
##
|
|
195
|
+
## Named and recursive rules
|
|
202
196
|
|
|
203
|
-
Use `
|
|
197
|
+
Use `rules()` when your combinators need to reference each other by name. Pass a factory that receives all rule names as ready-to-use references and returns the definitions:
|
|
204
198
|
|
|
205
199
|
```ts
|
|
206
|
-
import { parser, choice, sequence, literal, sepBy, transform, regex } from 'parseman'
|
|
200
|
+
import { rules, parser, choice, sequence, literal, sepBy, transform, trivia, regex } from 'parseman'
|
|
207
201
|
import type { Combinator } from 'parseman'
|
|
208
202
|
|
|
209
203
|
type JSON = null | boolean | number | string | JSON[] | Record<string, JSON>
|
|
210
204
|
|
|
211
|
-
const ws = regex(/[ \t\n\r]*/)
|
|
205
|
+
const ws = trivia(regex(/[ \t\n\r]*/))
|
|
212
206
|
|
|
213
|
-
const { value } =
|
|
214
|
-
const comma =
|
|
207
|
+
const { value } = rules<{ value: Combinator<JSON> }>(g => {
|
|
208
|
+
const comma = literal(',')
|
|
215
209
|
|
|
216
210
|
const array = transform(
|
|
217
211
|
sequence(literal('['), sepBy(g.value, comma), literal(']')),
|
|
@@ -227,26 +221,26 @@ const { value } = parser<{ value: Combinator<JSON> }>(g => {
|
|
|
227
221
|
)
|
|
228
222
|
|
|
229
223
|
return {
|
|
230
|
-
value:
|
|
231
|
-
{ trivia: ws },
|
|
232
|
-
choice(object, array, jsonString, jsonNumber, jsonBool, jsonNull)
|
|
233
|
-
) as Combinator<JSON>,
|
|
224
|
+
value: choice(object, array, jsonString, jsonNumber, jsonBool, jsonNull) as Combinator<JSON>,
|
|
234
225
|
}
|
|
235
226
|
})
|
|
227
|
+
|
|
228
|
+
export const jsonParser = parser({ trivia: ws }, value)
|
|
229
|
+
jsonParser.parse('{ "a": 1 }')
|
|
236
230
|
```
|
|
237
231
|
|
|
238
|
-
`g.value` is a
|
|
232
|
+
`g.value` is a reference that works anywhere inside the factory regardless of order. Local helpers (`comma`, `pair`, `object`) that don't need to be cross-referenced can be plain `const`. Only put a rule in the returned object if other rules need to reach it as `g.xxx`.
|
|
239
233
|
|
|
240
|
-
> **Macro
|
|
234
|
+
> **Macro and `rules()`:** The plugin fully compiles `rules()` factories, including recursive ones. It emits mutually recursive named functions (`_pf0` etc.) so the cycle is broken. Add `with { type: 'macro' }` to your import and the entire grammar — recursive rules included — is inlined at build time.
|
|
241
235
|
|
|
242
236
|
### `ref<T>()` — low-level forward declaration
|
|
243
237
|
|
|
244
|
-
`
|
|
238
|
+
`rules()` handles forward references automatically. `ref<T>()` is the lower-level primitive it uses internally, exposed for cases where you need a single forward slot outside of a `rules()` call:
|
|
245
239
|
|
|
246
240
|
```ts
|
|
247
241
|
const value = ref<JSON>()
|
|
248
242
|
// ... build parsers that use value ...
|
|
249
|
-
value.define(
|
|
243
|
+
value.define(choice(object, array, str, num, bool, nil))
|
|
250
244
|
```
|
|
251
245
|
|
|
252
246
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "parseman",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Parser combinators that compile to optimized JavaScript — use as a library or as a build-time macro",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Matthew Dean <matthew-dean@users.noreply.github.com>",
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"test": "vitest run",
|
|
35
35
|
"test:watch": "vitest",
|
|
36
36
|
"typecheck": "tsc",
|
|
37
|
-
"bench": "node --import tsx/esm bench/run.ts"
|
|
37
|
+
"bench": "node --import tsx/esm bench/run.ts",
|
|
38
|
+
"bench:compile-grammars": "peggy --format es -o bench/json-parser.js bench/json.pegjs && peggy --format es -o bench/csv-parser.js bench/csv.pegjs && peggy --format es -o bench/graphqlite-parser.js bench/graphqlite.pegjs"
|
|
38
39
|
},
|
|
39
40
|
"keywords": [
|
|
40
41
|
"parser",
|
|
@@ -56,7 +57,8 @@
|
|
|
56
57
|
"tsx": "~4.22.4",
|
|
57
58
|
"typescript": "7.0.1-rc",
|
|
58
59
|
"vite": "~8.0.16",
|
|
59
|
-
"vitest": "~4.1.9"
|
|
60
|
+
"vitest": "~4.1.9",
|
|
61
|
+
"xregexp": "~5.1.2"
|
|
60
62
|
},
|
|
61
63
|
"dependencies": {
|
|
62
64
|
"magic-string": "~0.30.21",
|