septima-lang 0.1.0 → 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/src/result.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { AstNode, UnitId } from './ast-node'
2
+ import { failMe } from './fail-me'
3
+ import { Span } from './location'
4
+ import { SourceUnit } from './septima'
5
+
6
+ export type ResultSink = {
7
+ tag: 'sink'
8
+ where: Span | undefined
9
+ trace: string | undefined
10
+ symbols: Record<string, unknown> | undefined
11
+ message: string
12
+ }
13
+
14
+ export type Result =
15
+ | {
16
+ tag: 'ok'
17
+ value: unknown
18
+ }
19
+ | ResultSink
20
+
21
+ const sourceCode = (unitId: string, unitByUnitId: Map<UnitId, SourceUnit>) =>
22
+ (unitByUnitId.get(unitId) ?? failMe(`source code not found for ${unitId}`)).sourceCode
23
+
24
+ export function formatTrace(trace: AstNode[] | undefined, unitByUnitId: Map<UnitId, SourceUnit>) {
25
+ if (!trace) {
26
+ return undefined
27
+ }
28
+ const format = (ast: AstNode) => sourceCode(ast.unitId, unitByUnitId).formatAst(ast)
29
+
30
+ const spacer = ' '
31
+ const joined = trace
32
+ .map(at => format(at))
33
+ .reverse()
34
+ .join(`\n${spacer}`)
35
+ return joined ? `${spacer}${joined}` : undefined
36
+ }
37
+
38
+ // TODO(imaman): generate a stack trace that is similar to node's a-la:
39
+ // $ node a.js
40
+ // /home/imaman/code/imaman/bigband/d.js:3
41
+ // return arg.n.foo()
42
+ // ^
43
+ //
44
+ // TypeError: Cannot read properties of undefined (reading 'foo')
45
+ // at d (/home/imaman/code/imaman/bigband/d.js:3:18)
46
+ // at c (/home/imaman/code/imaman/bigband/c.js:4:15)
47
+ // at b (/home/imaman/code/imaman/bigband/b.js:4:15)
48
+ // at Object.<anonymous> (/home/imaman/code/imaman/bigband/a.js:3:1)
49
+ // at Module._compile (node:internal/modules/cjs/loader:1155:14)
50
+ // at Object.Module._extensions..js (node:internal/modules/cjs/loader:1209:10)
51
+ // at Module.load (node:internal/modules/cjs/loader:1033:32)
52
+ // at Function.Module._load (node:internal/modules/cjs/loader:868:12)
53
+ // at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
54
+ // at node:internal/main/run_main_module:22:47
package/src/runtime.ts ADDED
@@ -0,0 +1,462 @@
1
+ import crypto from 'crypto'
2
+
3
+ import { AstNode, show, Unit, UnitId } from './ast-node'
4
+ import { extractMessage } from './extract-message'
5
+ import { failMe } from './fail-me'
6
+ import { shouldNeverHappen } from './should-never-happen'
7
+ import * as Stack from './stack'
8
+ import { switchOn } from './switch-on'
9
+ import { SymbolTable, Visibility } from './symbol-table'
10
+ import { Value } from './value'
11
+
12
+ interface Placeholder {
13
+ destination: undefined | Value
14
+ }
15
+
16
+ class SymbolFrame implements SymbolTable {
17
+ constructor(
18
+ readonly symbol: string,
19
+ readonly placeholder: Placeholder,
20
+ private readonly earlier: SymbolTable,
21
+ private readonly visibility: Visibility,
22
+ ) {}
23
+
24
+ lookup(sym: string): Value {
25
+ if (this.symbol === sym) {
26
+ const ret = this.placeholder.destination
27
+ if (ret === undefined) {
28
+ throw new Error(`Unresolved definition: ${this.symbol}`)
29
+ }
30
+ return ret
31
+ }
32
+
33
+ return this.earlier.lookup(sym)
34
+ }
35
+
36
+ export() {
37
+ const ret = this.earlier.export()
38
+ ret[this.symbol] = this.placeholder.destination?.export() ?? failMe(`Unbounded symbol: ${this.symbol}`)
39
+ return ret
40
+ }
41
+
42
+ exportValue(): Record<string, Value> {
43
+ const ret = this.earlier.exportValue()
44
+ if (this.visibility === 'INTERNAL') {
45
+ return ret
46
+ }
47
+
48
+ if (this.visibility === 'EXPORTED') {
49
+ ret[this.symbol] = this.placeholder.destination ?? failMe(`Unbounded symbol: ${this.symbol}`)
50
+ return ret
51
+ }
52
+
53
+ shouldNeverHappen(this.visibility)
54
+ }
55
+ }
56
+
57
+ class EmptySymbolTable implements SymbolTable {
58
+ lookup(sym: string): Value {
59
+ throw new Error(`Symbol ${sym} was not found`)
60
+ }
61
+
62
+ export() {
63
+ return {}
64
+ }
65
+
66
+ exportValue(): Record<string, Value> {
67
+ return {}
68
+ }
69
+ }
70
+
71
+ export type Verbosity = 'quiet' | 'trace'
72
+ export type Outputter = (u: unknown) => void
73
+
74
+ export class Runtime {
75
+ private stack: Stack.T = undefined
76
+ constructor(
77
+ private readonly root: AstNode,
78
+ private readonly verbosity: Verbosity = 'quiet',
79
+ private readonly getAstOf: (importerAsPathFromSourceRoot: string, relativePathFromImporter: string) => Unit,
80
+ private readonly args: Record<string, unknown>,
81
+ private readonly consoleLog?: Outputter,
82
+ ) {}
83
+
84
+ private output(v: Value) {
85
+ const logger = this.consoleLog ?? console.log // eslint-disable-line no-console
86
+ logger(JSON.stringify(v))
87
+ }
88
+
89
+ private buildInitialSymbolTable(generateTheArgsObject: boolean) {
90
+ const empty = new EmptySymbolTable()
91
+
92
+ const keys = Value.foreign(o => o.keys())
93
+ const entries = Value.foreign(o => o.entries())
94
+ const fromEntries = Value.foreign(o => o.fromEntries())
95
+ const isArray = Value.foreign(o => o.isArray())
96
+ const log = Value.foreign(o => {
97
+ this.output(o)
98
+ return o
99
+ })
100
+
101
+ const parse = Value.foreign(o => JSON.parse(o.toString()))
102
+ const hash224 = Value.foreign(o => crypto.createHash('sha224').update(JSON.stringify(o.unwrap())).digest('hex'))
103
+
104
+ let lib = new SymbolFrame('Object', { destination: Value.obj({ keys, entries, fromEntries }) }, empty, 'INTERNAL')
105
+ lib = new SymbolFrame('String', { destination: Value.foreign(o => Value.str(o.toString())) }, lib, 'INTERNAL')
106
+ lib = new SymbolFrame('Boolean', { destination: Value.foreign(o => Value.bool(o.toBoolean())) }, lib, 'INTERNAL')
107
+ lib = new SymbolFrame('Number', { destination: Value.foreign(o => Value.num(o.toNumber())) }, lib, 'INTERNAL')
108
+ lib = new SymbolFrame('Array', { destination: Value.obj({ isArray }) }, lib, 'INTERNAL')
109
+ lib = new SymbolFrame('console', { destination: Value.obj({ log }) }, lib, 'INTERNAL')
110
+ lib = new SymbolFrame('JSON', { destination: Value.obj({ parse }) }, lib, 'INTERNAL')
111
+ lib = new SymbolFrame('crypto', { destination: Value.obj({ hash224 }) }, lib, 'INTERNAL')
112
+
113
+ if (generateTheArgsObject) {
114
+ lib = new SymbolFrame('args', { destination: Value.from(this.args) }, lib, 'INTERNAL')
115
+ }
116
+
117
+ return lib
118
+ }
119
+
120
+ compute() {
121
+ try {
122
+ const value = this.evalNode(this.root, this.buildInitialSymbolTable(true))
123
+ return { value }
124
+ } catch (e) {
125
+ const trace: AstNode[] = []
126
+ for (let curr = this.stack; curr; curr = curr?.next) {
127
+ trace.push(curr.ast)
128
+ }
129
+ return {
130
+ expressionTrace: trace,
131
+ errorMessage: extractMessage(e),
132
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
133
+ stack: (e as { stack?: string[] }).stack,
134
+ }
135
+ }
136
+ }
137
+
138
+ private evalNode(ast: AstNode, table: SymbolTable): Value {
139
+ this.stack = Stack.push(ast, this.stack)
140
+ const ret = this.evalNodeImpl(ast, table)
141
+ switchOn(this.verbosity, {
142
+ quiet: () => {},
143
+ trace: () => {
144
+ // eslint-disable-next-line no-console
145
+ console.log(`output of <|${show(ast)}|> is ${JSON.stringify(ret)} // ${ast.tag}`)
146
+ },
147
+ })
148
+ this.stack = Stack.pop(this.stack)
149
+ return ret
150
+ }
151
+
152
+ private importDefinitions(importerAsPathFromSourceRoot: UnitId, relativePathFromImporter: string): Value {
153
+ const importee = this.getAstOf(importerAsPathFromSourceRoot, relativePathFromImporter)
154
+ const exp = importee.expression
155
+ if (
156
+ exp.tag === 'arrayLiteral' ||
157
+ exp.tag === 'binaryOperator' ||
158
+ exp.tag === 'dot' ||
159
+ exp.tag === 'export*' ||
160
+ exp.tag === 'functionCall' ||
161
+ exp.tag === 'ident' ||
162
+ exp.tag === 'formalArg' ||
163
+ exp.tag === 'if' ||
164
+ exp.tag === 'ternary' ||
165
+ exp.tag === 'indexAccess' ||
166
+ exp.tag === 'lambda' ||
167
+ exp.tag === 'literal' ||
168
+ exp.tag === 'objectLiteral' ||
169
+ exp.tag === 'templateLiteral' ||
170
+ exp.tag === 'unaryOperator' ||
171
+ exp.tag === 'unit'
172
+ ) {
173
+ // TODO(imaman): throw an error on non-exporting unit?
174
+ return Value.obj({})
175
+ }
176
+
177
+ if (exp.tag === 'topLevelExpression') {
178
+ // Construct a syntehtic unit which is similar to importedUnit but override its expression with an expression that
179
+ // just returns the importee's definitions bundled in a single object (an export* expression), and evaluate it.
180
+ // This is the trick that allows the importer to gain access to the importee's own stuff.
181
+ const exporStarUnit: AstNode = {
182
+ tag: 'unit',
183
+ imports: importee.imports,
184
+ unitId: importee.unitId,
185
+ expression: {
186
+ tag: 'topLevelExpression',
187
+ definitions: exp.definitions,
188
+ unitId: importee.unitId,
189
+ computation: { tag: 'export*', unitId: importee.unitId },
190
+ },
191
+ }
192
+ return this.evalNode(exporStarUnit, this.buildInitialSymbolTable(false))
193
+ }
194
+
195
+ shouldNeverHappen(exp)
196
+ }
197
+
198
+ private evalNodeImpl(ast: AstNode, table: SymbolTable): Value {
199
+ if (ast.tag === 'unit') {
200
+ let newTable = table
201
+ for (const imp of ast.imports) {
202
+ const o = this.importDefinitions(ast.unitId, imp.pathToImportFrom.text)
203
+ newTable = new SymbolFrame(imp.ident.t.text, { destination: o }, newTable, 'INTERNAL')
204
+ }
205
+ return this.evalNode(ast.expression, newTable)
206
+ }
207
+ if (ast.tag === 'topLevelExpression') {
208
+ let newTable = table
209
+ for (const def of ast.definitions) {
210
+ const name = def.ident.t.text
211
+ const placeholder: Placeholder = { destination: undefined }
212
+ newTable = new SymbolFrame(name, placeholder, newTable, def.isExported ? 'EXPORTED' : 'INTERNAL')
213
+ const v = this.evalNode(def.value, newTable)
214
+ placeholder.destination = v
215
+ }
216
+
217
+ if (!ast.computation) {
218
+ return Value.str('')
219
+ }
220
+ const c = this.evalNode(ast.computation, newTable)
221
+ if (ast.throwToken) {
222
+ throw new Error(JSON.stringify(c))
223
+ }
224
+
225
+ return c
226
+ }
227
+
228
+ if (ast.tag === 'export*') {
229
+ return Value.obj(table.exportValue())
230
+ }
231
+
232
+ if (ast.tag === 'binaryOperator') {
233
+ const lhs = this.evalNode(ast.lhs, table)
234
+ if (ast.operator === '||') {
235
+ return lhs.or(() => this.evalNode(ast.rhs, table))
236
+ }
237
+ if (ast.operator === '&&') {
238
+ return lhs.and(() => this.evalNode(ast.rhs, table))
239
+ }
240
+
241
+ const rhs = this.evalNode(ast.rhs, table)
242
+ if (ast.operator === '!=') {
243
+ return lhs.equalsTo(rhs).not()
244
+ }
245
+ if (ast.operator === '==') {
246
+ return lhs.equalsTo(rhs)
247
+ }
248
+
249
+ if (ast.operator === '<=') {
250
+ const comp = lhs.order(rhs)
251
+ return comp.isToZero('<=')
252
+ }
253
+ if (ast.operator === '<') {
254
+ const comp = lhs.order(rhs)
255
+ return comp.isToZero('<')
256
+ }
257
+ if (ast.operator === '>=') {
258
+ const comp = lhs.order(rhs)
259
+ return comp.isToZero('>=')
260
+ }
261
+ if (ast.operator === '>') {
262
+ const comp = lhs.order(rhs)
263
+ return comp.isToZero('>')
264
+ }
265
+ if (ast.operator === '%') {
266
+ return lhs.modulo(rhs)
267
+ }
268
+ if (ast.operator === '*') {
269
+ return lhs.times(rhs)
270
+ }
271
+ if (ast.operator === '**') {
272
+ return lhs.power(rhs)
273
+ }
274
+ if (ast.operator === '+') {
275
+ return lhs.plus(rhs)
276
+ }
277
+ if (ast.operator === '-') {
278
+ return lhs.minus(rhs)
279
+ }
280
+ if (ast.operator === '/') {
281
+ return lhs.over(rhs)
282
+ }
283
+
284
+ if (ast.operator === '??') {
285
+ return lhs.coalesce(() => this.evalNode(ast.rhs, table))
286
+ }
287
+
288
+ shouldNeverHappen(ast.operator)
289
+ }
290
+
291
+ if (ast.tag === 'unaryOperator') {
292
+ const operand = this.evalNode(ast.operand, table)
293
+ if (ast.operator === '!') {
294
+ return operand.not()
295
+ }
296
+ if (ast.operator === '+') {
297
+ // We intentionally do <0 + operand> instead of just <operand>. This is due to type-checking: the latter will
298
+ // evaluate to the operand as-is, making expression such as `+true` dynamically valid (which is not the desired
299
+ // behavior)
300
+ return Value.num(0).plus(operand)
301
+ }
302
+ if (ast.operator === '-') {
303
+ return operand.negate()
304
+ }
305
+
306
+ shouldNeverHappen(ast.operator)
307
+ }
308
+ if (ast.tag === 'ident') {
309
+ return table.lookup(ast.t.text)
310
+ }
311
+
312
+ if (ast.tag === 'formalArg') {
313
+ if (ast.defaultValue) {
314
+ return this.evalNode(ast.defaultValue, table)
315
+ }
316
+
317
+ // This error should not be reached. The call flow should evaluate a formalArg node only when if it has
318
+ // a default value sud-node.
319
+ throw new Error(`no default value for ${ast}`)
320
+ }
321
+
322
+ if (ast.tag === 'literal') {
323
+ if (ast.type === 'bool') {
324
+ // TODO(imaman): stricter checking of 'false'
325
+ return Value.bool(ast.t.text === 'true' ? true : false)
326
+ }
327
+ if (ast.type === 'num') {
328
+ return Value.num(Number(ast.t.text))
329
+ }
330
+ if (ast.type === 'str') {
331
+ return Value.str(ast.t.text)
332
+ }
333
+
334
+ if (ast.type === 'undef') {
335
+ return Value.undef()
336
+ }
337
+ shouldNeverHappen(ast.type)
338
+ }
339
+
340
+ if (ast.tag === 'templateLiteral') {
341
+ const result = ast.parts
342
+ .map(part => {
343
+ if (part.tag === 'string') {
344
+ return part.value
345
+ }
346
+ if (part.tag === 'expression') {
347
+ return this.evalNode(part.expr, table).toString()
348
+ }
349
+ shouldNeverHappen(part)
350
+ })
351
+ .join('')
352
+ return Value.str(result)
353
+ }
354
+
355
+ if (ast.tag === 'arrayLiteral') {
356
+ const arr: Value[] = []
357
+ for (const curr of ast.parts) {
358
+ if (curr.tag === 'element') {
359
+ arr.push(this.evalNode(curr.v, table))
360
+ } else if (curr.tag === 'spread') {
361
+ const v = this.evalNode(curr.v, table)
362
+ if (v.isUndefined()) {
363
+ continue
364
+ }
365
+ arr.push(...v.assertArr())
366
+ } else {
367
+ shouldNeverHappen(curr)
368
+ }
369
+ }
370
+
371
+ return Value.arr(arr)
372
+ }
373
+
374
+ if (ast.tag === 'objectLiteral') {
375
+ const entries: [string, Value][] = ast.parts.flatMap(at => {
376
+ if (at.tag === 'hardName') {
377
+ return [[at.k.t.text, this.evalNode(at.v, table)]]
378
+ }
379
+ if (at.tag === 'quotedString') {
380
+ return [[at.k.t.text, this.evalNode(at.v, table)]]
381
+ }
382
+ if (at.tag === 'computedName') {
383
+ return [[this.evalNode(at.k, table).assertStr(), this.evalNode(at.v, table)]]
384
+ }
385
+ if (at.tag === 'spread') {
386
+ const o = this.evalNode(at.o, table)
387
+ if (o.isUndefined()) {
388
+ return []
389
+ }
390
+ return Object.entries(o.assertObj())
391
+ }
392
+
393
+ shouldNeverHappen(at)
394
+ })
395
+
396
+ // TODO(imaman): verify type of all keys (strings, maybe also numbers)
397
+ return Value.obj(Object.fromEntries(entries.filter(([_, v]) => !v.isUndefined())))
398
+ }
399
+
400
+ if (ast.tag === 'lambda') {
401
+ return Value.lambda(ast, table)
402
+ }
403
+
404
+ if (ast.tag === 'functionCall') {
405
+ const argValues = ast.actualArgs.map(a => this.evalNode(a, table))
406
+ const callee = this.evalNode(ast.callee, table)
407
+
408
+ return this.call(callee, argValues)
409
+ }
410
+
411
+ if (ast.tag === 'if' || ast.tag === 'ternary') {
412
+ const c = this.evalNode(ast.condition, table)
413
+ return c.ifElse(
414
+ () => this.evalNode(ast.positive, table),
415
+ () => this.evalNode(ast.negative, table),
416
+ )
417
+ }
418
+
419
+ if (ast.tag === 'dot') {
420
+ const rec = this.evalNode(ast.receiver, table)
421
+ if (rec === undefined || rec === null) {
422
+ throw new Error(`Cannot access attribute .${ast.ident.t.text} of ${rec}`)
423
+ }
424
+ return rec.access(ast.ident.t.text, (callee, args) => this.call(callee, args))
425
+ }
426
+
427
+ if (ast.tag === 'indexAccess') {
428
+ const rec = this.evalNode(ast.receiver, table)
429
+ const index = this.evalNode(ast.index, table)
430
+ return rec.access(index, (callee, args) => this.call(callee, args))
431
+ }
432
+
433
+ shouldNeverHappen(ast)
434
+ }
435
+
436
+ call(callee: Value, actualValues: Value[]) {
437
+ return callee.call(actualValues, (formals, body, lambdaTable: SymbolTable) => {
438
+ const requiredCount = formals.filter(f => !f.defaultValue).length
439
+ if (actualValues.length < requiredCount) {
440
+ throw new Error(`Expected at least ${requiredCount} argument(s) but got ${actualValues.length}`)
441
+ }
442
+
443
+ let newTable = lambdaTable
444
+ for (let i = 0; i < formals.length; ++i) {
445
+ const formal = formals[i]
446
+ let actual = actualValues.at(i)
447
+ const useDefault = actual === undefined || (actual.isUndefined() && formal.defaultValue)
448
+
449
+ if (useDefault && formal.defaultValue) {
450
+ actual = this.evalNode(formal.defaultValue, lambdaTable)
451
+ }
452
+
453
+ if (actual === undefined) {
454
+ throw new Error(`A value must be passed to formal argument: ${show(formal.ident)}`)
455
+ }
456
+
457
+ newTable = new SymbolFrame(formal.ident.t.text, { destination: actual }, newTable, 'INTERNAL')
458
+ }
459
+ return this.evalNode(body, newTable)
460
+ })
461
+ }
462
+ }
package/src/scanner.ts ADDED
@@ -0,0 +1,136 @@
1
+ import { Location } from './location'
2
+ import { SourceCode } from './source-code'
3
+
4
+ export interface Token {
5
+ readonly text: string
6
+ readonly location: Location
7
+ }
8
+
9
+ type ConsumeIfBasicPattern = RegExp | string
10
+ type ConsumeIfPattern = { either: ConsumeIfBasicPattern[]; noneOf: ConsumeIfBasicPattern[] }
11
+
12
+ export class Scanner {
13
+ constructor(readonly sourceCode: SourceCode, private offset = 0) {
14
+ if (this.offset === 0) {
15
+ this.eatWhitespace()
16
+ }
17
+ }
18
+
19
+ get sourceRef() {
20
+ return this.sourceCode.sourceRef(this.sourceCode.expandToEndOfLine({ offset: this.offset }))
21
+ }
22
+
23
+ private curr() {
24
+ return this.sourceCode.input.substring(this.offset)
25
+ }
26
+
27
+ private eatBlockComment() {
28
+ const startedAt = this.sourceRef
29
+ while (true) {
30
+ if (this.eof()) {
31
+ throw new Error(`Block comment that started at ${startedAt} is missing its closing (*/)`)
32
+ }
33
+ if (this.consumeIf('*/')) {
34
+ return
35
+ }
36
+
37
+ // By default, the . symbol in regexp does _not_ match newline. we use /./s to override the default.
38
+ this.consume(/./s, false)
39
+ }
40
+ }
41
+
42
+ private eatWhitespace() {
43
+ while (true) {
44
+ if (this.consumeIf(/\s*/, false)) {
45
+ continue
46
+ }
47
+ if (this.consumeIf('//', false)) {
48
+ this.consume(/[^\n]*/, false)
49
+ continue
50
+ }
51
+ if (this.consumeIf('/*', false)) {
52
+ this.eatBlockComment()
53
+ continue
54
+ }
55
+
56
+ return
57
+ }
58
+ }
59
+
60
+ eof(): boolean {
61
+ return this.offset >= this.sourceCode.input.length
62
+ }
63
+
64
+ synopsis() {
65
+ const c = this.curr()
66
+ let lookingAt = c.substring(0, 20)
67
+ if (lookingAt.length !== c.length) {
68
+ lookingAt = `${lookingAt}...`
69
+ }
70
+
71
+ return {
72
+ position: this.offset,
73
+ lookingAt,
74
+ }
75
+ }
76
+
77
+ headMatches(...patterns: (ConsumeIfPattern | ConsumeIfBasicPattern)[]): boolean {
78
+ const alt = new Scanner(this.sourceCode, this.offset)
79
+ for (const p of patterns) {
80
+ const t = alt.consumeIf(p, true)
81
+ if (t === undefined) {
82
+ return false
83
+ }
84
+ }
85
+
86
+ return true
87
+ }
88
+
89
+ consume(r: RegExp | string, eatWhitespace = true): Token {
90
+ const text = this.match(r)
91
+ if (text === undefined) {
92
+ throw new Error(`Expected ${r} ${this.sourceRef}`)
93
+ }
94
+
95
+ const offset = this.offset
96
+ this.offset += text.length
97
+
98
+ if (eatWhitespace) {
99
+ this.eatWhitespace()
100
+ }
101
+ return { location: { offset }, text }
102
+ }
103
+
104
+ consumeIf(p: ConsumeIfBasicPattern | ConsumeIfPattern, eatWhitespace = true): Token | undefined {
105
+ const pattern = typeof p === 'string' ? { either: [p], noneOf: [] } : isRegExp(p) ? { either: [p], noneOf: [] } : p
106
+
107
+ const found = pattern.either.find(r => this.match(r))
108
+ const hasNegative = pattern.noneOf.some(r => this.match(r))
109
+ const ret = found && !hasNegative
110
+ if (!ret) {
111
+ return undefined
112
+ }
113
+
114
+ return this.consume(found, eatWhitespace)
115
+ }
116
+
117
+ private match(r: RegExp | string): string | undefined {
118
+ if (typeof r === 'string') {
119
+ if (this.curr().startsWith(r)) {
120
+ return r
121
+ }
122
+ return undefined
123
+ }
124
+
125
+ const m = this.curr().match(r)
126
+ if (m && m.index === 0) {
127
+ return m[0]
128
+ }
129
+
130
+ return undefined
131
+ }
132
+ }
133
+
134
+ function isRegExp(u: object): u is RegExp {
135
+ return u.constructor.name === 'RegExp'
136
+ }