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.
Files changed (2) hide show
  1. package/README.md +27 -33
  2. 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 with combinators, then let the bundler plugin compile them to optimized inline functions at build time — `charCodeAt` dispatch, `while` loops, zero allocation on failure paths. Your parsers are just TypeScript — the plugin optimizes them at build time, invisibly. Easy to write, easy to debug.
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
- The same code runs without the plugin: the interpreter produces identical results. Use the macro build for production; skip it in tests and anywhere a bundler isn't in the picture.
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
  ![CSV parsing benchmarks](https://raw.githubusercontent.com/matthew-dean/parsecraft/main/assets/bench-csv.svg)
18
18
 
19
- Parséman compiled edges out Peggy on small and medium JSON. At 12 kB Peggy pulls ahead by ~10% — it's been doing this a while. On CSV, where the grammar is non-recursive and fully inlines, Parséman compiled is the clear winner.
19
+ ![GraphQL parsing benchmarks](https://raw.githubusercontent.com/matthew-dean/parsecraft/main/assets/bench-graphql.svg)
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
- | `parser(factory)` | Mutually recursive grammar rules — no forward declarations needed. |
130
- | `ref<T>()` | Low-level forward declaration slot (use `parser()` in most cases). |
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
- Pass `trivia` to `parse()` and `sequence()` will automatically skip it between terms:
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, parse } from 'parseman'
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(list, 'foo , bar , baz', { trivia: ws })
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 trivia = many(choice(regex(/\s+/), lineComment, blockComment))
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
- ## Recursive grammars
195
+ ## Named and recursive rules
202
196
 
203
- Use `parser()` for mutually recursive rules. Pass a factory that receives all rule names as ready-to-use references and returns the definitions:
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 } = parser<{ value: Combinator<JSON> }>(g => {
214
- const comma = sequence(ws, literal(','), ws)
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: grammar(
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 parser 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`.
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 note:** Recursive parsers can't be inlined as a single expression, so the plugin leaves them as-is — but you can still call `compile()` at runtime for the same speedups. The codegen emits mutually recursive named functions and handles cycles just fine. Non-recursive leaf parsers in the same file still get inlined.
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
- `parser()` is the right tool for most recursive grammars. `ref<T>()` is the lower-level primitive it uses internally, exposed for cases where you need a single forward slot outside of a `parser()` call:
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(grammar({ trivia: ws }, choice(object, array, str, num, bool, nil)))
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.1.6",
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",