|
| 1 | +// @ts-check |
| 2 | +/* globals globalThis */ |
1 | 3 | /// <reference path="../src/peg.d.ts"/> |
2 | 4 | import * as util from 'util'; |
3 | | -import tagToStringOnlyTag from '../src/tag-string.js'; |
4 | 5 |
|
5 | | -let curSrc = ''; |
| 6 | +/** |
| 7 | + * @typedef {(val: any, message?: string) => asserts val} Assert |
| 8 | + */ |
| 9 | + |
| 10 | +/** |
| 11 | + * @typedef {{ |
| 12 | + * args: any[], |
| 13 | + * result: PromiseSettledResult<any> |
| 14 | + * }} TestCall |
| 15 | + */ |
| 16 | + |
| 17 | +/** |
| 18 | + * @typedef {ReturnType<typeof makeAstBuilders>} TestTools |
| 19 | + */ |
| 20 | + |
| 21 | +/** |
| 22 | + * @typedef {IParserTag<any, TestCall & { tools: TestTools }>} TestTag |
| 23 | + */ |
| 24 | + |
| 25 | +/** |
| 26 | + * @type {Assert} |
| 27 | + */ |
| 28 | +const globalAssert = globalThis.assert; |
| 29 | + |
| 30 | +/** |
| 31 | + * Map an array of nodes, also calling thunks for nodes. |
| 32 | + * |
| 33 | + * @param {any[]} args |
| 34 | + */ |
| 35 | +const arr = args => |
| 36 | + args.map(a => { |
| 37 | + if (typeof a === 'function') { |
| 38 | + return a(); |
| 39 | + } |
| 40 | + return a; |
| 41 | + }); |
| 42 | + |
| 43 | +const setPosition = (node, position) => { |
| 44 | + const mappedArgs = arr(node); |
| 45 | + return Object.assign(mappedArgs, { |
| 46 | + _pegPosition: position, |
| 47 | + }); |
| 48 | +}; |
| 49 | + |
| 50 | +/** @param {string[]} tmpl */ |
| 51 | +const makeAstBuilders = tmpl => { |
| 52 | + let nextHole = 0; |
| 53 | + |
| 54 | + /** |
| 55 | + * Create an internal AST node. |
| 56 | + * |
| 57 | + * @param {number} pos |
| 58 | + * @param {...unknown} node |
| 59 | + */ |
| 60 | + const nest = (pos, ...node) => { |
| 61 | + const nh = nextHole; |
| 62 | + assert( |
| 63 | + nh < tmpl.length, |
| 64 | + `next hole ${nh} is more than template ${tmpl.length}`, |
| 65 | + ); |
| 66 | + const ts = tmpl[nh]; |
| 67 | + assert( |
| 68 | + pos < ts.length, |
| 69 | + `position ${pos} for templateString ${nh} is past ${ts.length}`, |
| 70 | + ); |
| 71 | + return () => { |
| 72 | + const posNode = setPosition( |
| 73 | + node, |
| 74 | + `${JSON.stringify(tmpl[nextHole][pos])} #${nextHole}:${pos}`, |
| 75 | + ); |
| 76 | + return posNode; |
| 77 | + }; |
| 78 | + }; |
| 79 | + |
| 80 | + /** |
| 81 | + * Make an AST node that represents a hole to be filled in by the parser's |
| 82 | + * template arguments. |
| 83 | + * |
| 84 | + * @param {number} index |
| 85 | + * @param {...any} node |
| 86 | + */ |
| 87 | + const hole = (index, ...node) => { |
| 88 | + assert( |
| 89 | + index === nextHole, |
| 90 | + `index ${index} is not expected next hole ${nextHole}`, |
| 91 | + ); |
| 92 | + const thisHole = nextHole; |
| 93 | + const nh = thisHole + 1; |
| 94 | + assert( |
| 95 | + nh < tmpl.length, |
| 96 | + `next hole ${nh} is more than template ${tmpl.length}`, |
| 97 | + ); |
| 98 | + return () => { |
| 99 | + const posNode = setPosition(node, `hole #${index}`); |
| 100 | + nextHole = nh; |
| 101 | + return posNode; |
| 102 | + }; |
| 103 | + }; |
| 104 | + |
| 105 | + /** |
| 106 | + * The root of an AST. |
| 107 | + * |
| 108 | + * @param {number | (() => any)} posOrThunk |
| 109 | + * @param {any[]} node |
| 110 | + */ |
| 111 | + const ast = (posOrThunk, ...node) => { |
| 112 | + nextHole = 0; |
| 113 | + if (typeof posOrThunk !== 'function') { |
| 114 | + posOrThunk = nest(posOrThunk, ...node); |
| 115 | + } |
| 116 | + return posOrThunk(); |
| 117 | + }; |
| 118 | + |
| 119 | + return { ast, arr, nest, hole }; |
| 120 | +}; |
6 | 121 |
|
7 | 122 | /** |
8 | | - * @param {IParserTag<any>} tag |
| 123 | + * @param {TestTag} testTag |
| 124 | + * @param {Assert} assert |
9 | 125 | */ |
10 | | -export function makeParser(tag) { |
11 | | - /** @type {IParserTag<any>} */ |
12 | | - const stringTag = tagToStringOnlyTag(tag); |
| 126 | +const makeSerialParseWithTools = (testTag, assert) => { |
13 | 127 | /** |
| 128 | + * @type {TestTools} |
| 129 | + */ |
| 130 | + let currentTestTools; |
| 131 | + |
| 132 | + /** |
| 133 | + * @type {TestTools} |
| 134 | + */ |
| 135 | + const parseTestTools = { |
| 136 | + arr, |
| 137 | + ast: (...args) => currentTestTools.ast(...args), |
| 138 | + nest: (...args) => currentTestTools.nest(...args), |
| 139 | + hole: (...args) => currentTestTools.hole(...args), |
| 140 | + }; |
| 141 | + |
| 142 | + /** |
| 143 | + * This convenience `parse` updates the behaviour of `parseTestTools` with |
| 144 | + * every call That means you must finish using the test tools to process a |
| 145 | + * single `parse` at a time before running the next `parse`. |
| 146 | + * |
14 | 147 | * @param {string} src |
15 | 148 | * @param {boolean} [doDump] |
16 | 149 | * @param {boolean} [doDebug] |
17 | 150 | */ |
18 | | - return (src, doDump = false, doDebug = false) => { |
19 | | - curSrc = src; |
20 | | - const dtag = doDebug ? stringTag.options({ debug: true }) : stringTag; |
21 | | - const parsed = dtag`${src}`; |
| 151 | + const parse = (src, doDump = false, doDebug = false) => { |
| 152 | + const tmpl = Object.assign([src], { raw: [src] }); |
| 153 | + const dtag = doDebug ? testTag.options({ debug: true }) : testTag; |
| 154 | + const { result, tools } = dtag(tmpl); |
| 155 | + assert(result.status === 'fulfilled', 'parse failed'); |
| 156 | + const parsed = result.value; |
| 157 | + currentTestTools = tools; |
22 | 158 | if (doDump) { |
23 | 159 | // tslint:disable-next-line:no-console |
24 | 160 | console.log('Dump:', util.inspect(parsed, { depth: Infinity })); |
25 | 161 | doDump = false; |
26 | 162 | } |
27 | 163 | return parsed; |
28 | 164 | }; |
29 | | -} |
| 165 | + |
| 166 | + return { parse, ...parseTestTools }; |
| 167 | +}; |
30 | 168 |
|
31 | 169 | /** |
32 | | - * |
33 | | - * @param {number} pos |
34 | | - * @param {...unknown} args |
| 170 | + * @param {IParserTag<any>} rawTag |
| 171 | + * @param {Assert} [testAssert] |
35 | 172 | */ |
36 | | -export function ast(pos, ...args) { |
37 | | - return Object.assign(args, { |
38 | | - _pegPosition: `${JSON.stringify(curSrc[pos])} #0:${pos}`, |
39 | | - }); |
| 173 | +export function makeParserUtils(rawTag, testAssert) { |
| 174 | + /** @type {Assert} */ |
| 175 | + const assert = testAssert || globalAssert; |
| 176 | + |
| 177 | + /** |
| 178 | + * Return a parser tag which returns testing tools. |
| 179 | + * |
| 180 | + * @param {IParserTag<any>} baseTag parser tag to wrap |
| 181 | + * @returns {TestTag} |
| 182 | + */ |
| 183 | + const wrapTag = baseTag => { |
| 184 | + const wrappedTag = (tmpl, ...holes) => { |
| 185 | + /** @type {PromiseSettledResult<any>} */ |
| 186 | + let result; |
| 187 | + try { |
| 188 | + const value = baseTag(tmpl, ...holes); |
| 189 | + result = { status: 'fulfilled', value }; |
| 190 | + } catch (e) { |
| 191 | + result = { status: 'rejected', reason: e }; |
| 192 | + } |
| 193 | + return { |
| 194 | + args: [tmpl, ...holes], |
| 195 | + result, |
| 196 | + tools: makeAstBuilders(tmpl), |
| 197 | + }; |
| 198 | + }; |
| 199 | + return Object.assign(wrappedTag, { |
| 200 | + options: opts => wrapTag(baseTag.options(opts)), |
| 201 | + parserCreator: baseTag.parserCreator, |
| 202 | + // eslint-disable-next-line no-underscore-dangle |
| 203 | + _asExtending: baseTag._asExtending, |
| 204 | + }); |
| 205 | + }; |
| 206 | + |
| 207 | + const testTag = wrapTag(rawTag); |
| 208 | + |
| 209 | + return { testTag, ...makeSerialParseWithTools(testTag, assert) }; |
40 | 210 | } |
0 commit comments