diff --git a/package-lock.json b/package-lock.json index 68a5290..d5759a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -302,9 +302,9 @@ } }, "llparse-builder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/llparse-builder/-/llparse-builder-1.3.0.tgz", - "integrity": "sha512-hi0KVAhwfpMs+Tbu12Kaj1kLBVykpxzhRS0UlqoKaAZU7tdVHOw4mVVj0I/6Wd62bTuTzcRU5PBgyqzxvwBT1Q==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/llparse-builder/-/llparse-builder-1.3.2.tgz", + "integrity": "sha512-0l2slf0O7TpzR+uWsENSCbMv+kdAgLC9eBuaLZYpXKYGg5bD4o4xX9MRKw8a51Prce+HvkpEWpR+8d/VYGpQpg==", "requires": { "@types/debug": "0.0.30", "binary-search": "^1.3.3", @@ -312,12 +312,12 @@ } }, "llparse-frontend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/llparse-frontend/-/llparse-frontend-1.2.0.tgz", - "integrity": "sha512-TqBpdNXE0nSYms0JLPJVq0MmbYUap3a//YOKLqWb9RPiT5zItr4CAvH+x7ZsiD3C3mi3vo8V7J/cHBNIuh9cTg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/llparse-frontend/-/llparse-frontend-1.2.1.tgz", + "integrity": "sha512-7n7NHAU2ZVNP/7S3llhvScY5FoDzMbPo5Le1lYr3JMAH91EPOxEbCxristRtMVcGKgij8qb2Ar7SBeu4mnb++g==", "requires": { - "debug": "^3.1.0", - "llparse-builder": "^1.3.0" + "debug": "^3.2.6", + "llparse-builder": "^1.3.2" } }, "llparse-test-fixture": { diff --git a/package.json b/package.json index 2cc4585..3464ce4 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,6 @@ "dependencies": { "bitcode": "^1.2.0", "debug": "^3.2.6", - "llparse-frontend": "^1.2.0" + "llparse-frontend": "^1.2.1" } } diff --git a/src/compiler/index.ts b/src/compiler/index.ts index a62b0b1..7206ce4 100644 --- a/src/compiler/index.ts +++ b/src/compiler/index.ts @@ -5,6 +5,7 @@ import source = frontend.source; import * as bitcodeImpl from '../implementation/bitcode'; import * as cImpl from '../implementation/c'; +import * as jsImpl from '../implementation/js'; import { HeaderBuilder } from './header-builder'; const debug = debugAPI('llparse:compiler'); @@ -37,11 +38,17 @@ export interface ICompilerOptions { /** Generate C (`true` by default) */ readonly generateC?: boolean; + /** Generate JS (`true` by default) */ + readonly generateJS?: boolean; + /** Optional frontend configuration */ readonly frontend?: frontend.IFrontendLazyOptions; /** Optional C-backend configuration */ readonly c?: cImpl.ICPublicOptions; + + /** Optional JS-backend configuration */ + readonly js?: jsImpl.IJSPublicOptions; } export interface ICompilerResult { @@ -55,6 +62,11 @@ export interface ICompilerResult { */ readonly c?: string; + /** + * Textual JS code, if `generateJS` option was `true` + */ + readonly js?: string; + /** * Textual C header file */ @@ -64,6 +76,7 @@ export interface ICompilerResult { interface IWritableCompilerResult { bitcode?: Buffer; c?: string; + js?: string; header: string; } @@ -91,6 +104,13 @@ export class Compiler { }, this.options.c)); } + let js: jsImpl.JSCompiler | undefined; + if (this.options.generateJS !== false) { + js = new jsImpl.JSCompiler(container, Object.assign({ + debug: this.options.debug, + }, this.options.js!)); + } + debug('Running frontend pass'); const f = new frontend.Frontend(this.prefix, container.build(), @@ -122,6 +142,11 @@ export class Compiler { result.c = c.compile(info); } + debug('Building JS'); + if (js) { + result.js = js.compile(info); + } + return result; } } diff --git a/src/implementation/js/code/base.ts b/src/implementation/js/code/base.ts new file mode 100644 index 0000000..d0ea2c9 --- /dev/null +++ b/src/implementation/js/code/base.ts @@ -0,0 +1,16 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; + +export abstract class Code { + protected cachedDecl: string | undefined; + + constructor(public readonly ref: T) { + } + + public buildGlobal(ctx: Compilation, out: string[]): void { + // no-op by default + } + + public abstract build(ctx: Compilation, out: string[]): void; +} diff --git a/src/implementation/js/code/external.ts b/src/implementation/js/code/external.ts new file mode 100644 index 0000000..811bff3 --- /dev/null +++ b/src/implementation/js/code/external.ts @@ -0,0 +1,29 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { Code } from './base'; + +export abstract class External + extends Code { + + public buildGlobal(ctx: Compilation, out: string[]): void { + ctx.importCode(this.ref.name, out); + } + + // NOTE: Overridden in Span + protected getArgs(ctx: Compilation): ReadonlyArray { + const args = [ ctx.bufArg(), ctx.offArg() ]; + if (this.ref.signature === 'value') { + args.push(ctx.matchVar()); + } + return args; + } + + public build(ctx: Compilation, out: string[]): void { + const args = this.getArgs(ctx); + + out.push(`_${this.ref.name}(${args.join(', ')}) {`); + out.push(` return ${this.ref.name}(this, ${args.join(', ')});`); + out.push(`}`); + } +} diff --git a/src/implementation/js/code/field-value.ts b/src/implementation/js/code/field-value.ts new file mode 100644 index 0000000..1c8fe0a --- /dev/null +++ b/src/implementation/js/code/field-value.ts @@ -0,0 +1,14 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { Field } from './field'; + +export abstract class FieldValue extends Field { + protected value(ctx: Compilation): string { + let res = this.ref.value.toString(); + if (ctx.getFieldType(this.ref.field) === 'i64') { + res += 'n'; + } + return res; + } +} diff --git a/src/implementation/js/code/field.ts b/src/implementation/js/code/field.ts new file mode 100644 index 0000000..612850a --- /dev/null +++ b/src/implementation/js/code/field.ts @@ -0,0 +1,25 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { Code } from './base'; + +export abstract class Field extends Code { + public build(ctx: Compilation, out: string[]): void { + const args = [ ctx.bufArg(), ctx.offArg() ]; + + if (this.ref.signature === 'value') { + args.push(ctx.matchVar()); + } + out.push(`_${this.ref.name}(${args.join(', ')}) {`); + const tmp: string[] = []; + this.doBuild(ctx, tmp); + ctx.indent(out, tmp, ' '); + out.push('}'); + } + + protected abstract doBuild(ctx: Compilation, out: string[]): void; + + protected field(ctx: Compilation): string { + return ctx.stateField(this.ref.field); + } +} diff --git a/src/implementation/js/code/index.ts b/src/implementation/js/code/index.ts new file mode 100644 index 0000000..0ecd543 --- /dev/null +++ b/src/implementation/js/code/index.ts @@ -0,0 +1,26 @@ +import * as frontend from 'llparse-frontend'; + +import { External } from './external'; +import { Span } from './span'; +import { IsEqual } from './is-equal'; +import { Load } from './load'; +import { MulAdd } from './mul-add'; +import { Or } from './or'; +import { Store } from './store'; +import { Test } from './test'; +import { Update } from './update'; + +export * from './base'; + +export default { + IsEqual, + Load, + Match: class Match extends External {}, + MulAdd, + Or, + Span, + Store, + Test, + Update, + Value: class Value extends External {}, +}; diff --git a/src/implementation/js/code/is-equal.ts b/src/implementation/js/code/is-equal.ts new file mode 100644 index 0000000..c2e9cb5 --- /dev/null +++ b/src/implementation/js/code/is-equal.ts @@ -0,0 +1,10 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { FieldValue } from './field-value'; + +export class IsEqual extends FieldValue { + protected doBuild(ctx: Compilation, out: string[]): void { + out.push(`return ${this.field(ctx)} === ${this.value(ctx)};`); + } +} diff --git a/src/implementation/js/code/load.ts b/src/implementation/js/code/load.ts new file mode 100644 index 0000000..4295496 --- /dev/null +++ b/src/implementation/js/code/load.ts @@ -0,0 +1,17 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { Field } from './field'; + +export class Load extends Field { + protected doBuild(ctx: Compilation, out: string[]): void { + let value = this.field(ctx); + + // Convert BigNum to number + if (ctx.getFieldType(this.ref.field) === 'i64') { + value = `Number(${value})`; + } + + out.push(`return ${value};`); + } +} diff --git a/src/implementation/js/code/mul-add.ts b/src/implementation/js/code/mul-add.ts new file mode 100644 index 0000000..b29fb11 --- /dev/null +++ b/src/implementation/js/code/mul-add.ts @@ -0,0 +1,87 @@ +import * as assert from 'assert'; +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { SIGNED_LIMITS, UNSIGNED_LIMITS } from '../constants'; +import { Field } from './field'; + +export class MulAdd extends Field { + protected doBuild(ctx: Compilation, out: string[]): void { + const options = this.ref.options; + const ty = ctx.getFieldType(this.ref.field); + + const field = this.field(ctx); + + const match = ctx.matchVar(); + let base = options.base.toString(); + + if (ty === 'i64') { + // Convert `match` to BigInt + out.push(`${ctx.matchVar()} = BigInt(${ctx.matchVar()});`); + out.push(''); + + base += 'n'; + } + + const limits = options.signed ? SIGNED_LIMITS : UNSIGNED_LIMITS; + assert(limits.has(ty), `Unexpected mulAdd type "${ty}"`); + const [ min, max ] = limits.get(ty)!; + + let mulMax = `${max} / ${base}`; + let mulMin = `${min} / ${base}`; + + // Round division results to integers + if (ty !== 'i64') { + if (options.signed) { + mulMax = `((${mulMax}) | 0)`; + mulMin = `((${mulMin}) | 0)`; + } else { + mulMax = `((${mulMax}) >>> 0)`; + mulMin = `((${mulMin}) >>> 0)`; + } + } + + out.push('// Multiplication overflow'); + out.push(`if (${field} > ${mulMax}) {`); + out.push(' return 1;'); + out.push('}'); + if (options.signed) { + out.push(`if (${field} < ${mulMin}) {`); + out.push(' return 1;'); + out.push('}'); + } + out.push(''); + + out.push(`${field} *= ${base};`); + out.push(''); + + out.push('// Addition overflow'); + out.push(`if (${match} >= 0) {`); + out.push(` if (${field} > ${max} - ${match}) {`); + out.push(' return 1;'); + out.push(' }'); + out.push('} else {'); + out.push(` if (${field} < ${min} - ${match}) {`); + out.push(' return 1;'); + out.push(' }'); + out.push('}'); + + out.push(`${field} += ${match};`); + + if (options.max !== undefined) { + let max = options.max.toString(); + + if (ty === 'i64') { + max += 'n'; + } + + out.push(''); + out.push('// Enforce maximum'); + out.push(`if (${field} > ${max}) {`); + out.push(' return 1;'); + out.push('}'); + } + + out.push('return 0;'); + } +} diff --git a/src/implementation/js/code/or.ts b/src/implementation/js/code/or.ts new file mode 100644 index 0000000..e1a06c8 --- /dev/null +++ b/src/implementation/js/code/or.ts @@ -0,0 +1,11 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { FieldValue } from './field-value'; + +export class Or extends FieldValue { + protected doBuild(ctx: Compilation, out: string[]): void { + out.push(`${this.field(ctx)} |= ${this.value(ctx)};`); + out.push('return 0;'); + } +} diff --git a/src/implementation/js/code/span.ts b/src/implementation/js/code/span.ts new file mode 100644 index 0000000..23a181d --- /dev/null +++ b/src/implementation/js/code/span.ts @@ -0,0 +1,10 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { External } from './external'; + +export class Span extends External { + protected getArgs(ctx: Compilation): ReadonlyArray { + return [ ctx.bufArg(), ctx.offArg(), 'offEnd' ]; + } +} diff --git a/src/implementation/js/code/store.ts b/src/implementation/js/code/store.ts new file mode 100644 index 0000000..a37d963 --- /dev/null +++ b/src/implementation/js/code/store.ts @@ -0,0 +1,11 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { Field } from './field'; + +export class Store extends Field { + protected doBuild(ctx: Compilation, out: string[]): void { + out.push(`${this.field(ctx)} = ${ctx.matchVar()};`); + out.push('return 0;'); + } +} diff --git a/src/implementation/js/code/test.ts b/src/implementation/js/code/test.ts new file mode 100644 index 0000000..ff6fa86 --- /dev/null +++ b/src/implementation/js/code/test.ts @@ -0,0 +1,11 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { FieldValue } from './field-value'; + +export class Test extends FieldValue { + protected doBuild(ctx: Compilation, out: string[]): void { + const value = this.value(ctx); + out.push(`return (${this.field(ctx)} & ${value}) === ${value};`); + } +} diff --git a/src/implementation/js/code/update.ts b/src/implementation/js/code/update.ts new file mode 100644 index 0000000..c181078 --- /dev/null +++ b/src/implementation/js/code/update.ts @@ -0,0 +1,11 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { FieldValue } from './field-value'; + +export class Update extends FieldValue { + protected doBuild(ctx: Compilation, out: string[]): void { + out.push(`${this.field(ctx)} = ${this.value(ctx)};`); + out.push('return 0;'); + } +} diff --git a/src/implementation/js/compilation.ts b/src/implementation/js/compilation.ts new file mode 100644 index 0000000..9cbb385 --- /dev/null +++ b/src/implementation/js/compilation.ts @@ -0,0 +1,364 @@ +import * as assert from 'assert'; +import { Buffer } from 'buffer'; +import * as frontend from 'llparse-frontend'; + +import { + CONTAINER_KEY, STATE_ERROR, + ARG_CURRENT, ARG_BUF, ARG_OFF, + VAR_MATCH, + STATE_PREFIX, BLOB_PREFIX, TABLE_PREFIX, + SEQUENCE_COMPLETE, SEQUENCE_MISMATCH, SEQUENCE_PAUSE, +} from './constants'; +import { Code } from './code'; +import { Node } from './node'; +import { Transform } from './transform'; +import { MatchSequence } from './helpers/match-sequence'; + +// Number of hex words per line of blob declaration +const BLOB_GROUP_SIZE = 4; +const TABLE_GROUP_SIZE = 16; + +type WrappedNode = frontend.IWrap; + +// TODO(indutny): deduplicate +export interface ICompilationOptions { + readonly debug?: string; + readonly module: 'esm' | 'commonjs'; +} + +// TODO(indutny): deduplicate +export interface ICompilationProperty { + readonly name: string; + readonly ty: string; +} + +interface ITable { + readonly name: string; + readonly value: ReadonlyArray; +} + +export class Compilation { + private readonly stateMap: Map> = new Map(); + private readonly blobs: Map = new Map(); + private readonly tables: ITable[] = []; + private readonly codeMap: Map> = new Map(); + private readonly matchSequence: + Map = new Map(); + + constructor(public readonly prefix: string, + private readonly properties: ReadonlyArray, + private readonly options: ICompilationOptions) { + } + + private buildStateEnum(out: string[]): void { + let index = 0; + + out.push(`const ${STATE_ERROR} = ${index++};`); + for (const stateName of this.stateMap.keys()) { + out.push(`const ${stateName} = ${index++};`); + } + } + + private buildBlobs(out: string[]): void { + if (this.blobs.size === 0) { + return; + } + + for (const [ blob, name ] of this.blobs) { + out.push(`const ${name} = [`); + + for (let i = 0; i < blob.length; i += BLOB_GROUP_SIZE) { + const limit = Math.min(blob.length, i + BLOB_GROUP_SIZE); + const hex: string[] = []; + for (let j = i; j < limit; j++) { + const value = blob[j] as number; + + const ch = String.fromCharCode(value); + let enc = `0x${value.toString(16)}`; + + // `'`, `\` + if (value === 0x27 || value === 0x5c) { + enc = `/* '\\${ch}' */ ` + enc; + } else if (value >= 0x20 && value <= 0x7e) { + enc = `/* '${ch}' */ ` + enc; + } + hex.push(enc); + } + let line = ' ' + hex.join(', '); + if (limit !== blob.length) { + line += ','; + } + out.push(line); + } + + out.push(`];`); + } + out.push(''); + } + + private buildTables(out: string[]): void { + if (this.tables.length === 0) { + return; + } + + for (const { name, value } of this.tables) { + out.push(`const ${name} = [`); + for (let i = 0; i < value.length; i += TABLE_GROUP_SIZE) { + let line = ` ${value.slice(i, i + TABLE_GROUP_SIZE).join(', ')}`; + if (i + TABLE_GROUP_SIZE < value.length) { + line += ','; + } + out.push(line); + } + out.push('];'); + } + out.push(''); + } + + private buildMatchSequence(out: string[]): void { + if (this.matchSequence.size === 0) { + return; + } + + for (const match of this.matchSequence.values()) { + match.build(this, out); + out.push(''); + } + } + + public reserveSpans(spans: ReadonlyArray): void { + for (const span of spans) { + for (const callback of span.callbacks) { + this.buildCode(this.unwrapCode(callback)); + } + } + } + + public debug(out: string[], message: string): void { + if (this.options.debug === undefined) { + return; + } + + const args = [ + this.stateVar(), + this.bufArg(), + this.offArg(), + JSON.stringify(message), + ]; + + out.push(`${this.options.debug}(${args.join(', ')});`); + } + + public buildGlobals(out: string[]): void { + out.push('function unreachable() {'); + out.push(' throw new Error(\'Unreachable\');'); + out.push('}'); + out.push(''); + + this.buildBlobs(out); + this.buildTables(out); + + if (this.matchSequence.size !== 0) { + MatchSequence.buildGlobals(out); + out.push(''); + } + + this.buildStateEnum(out); + + for (const code of this.codeMap.values()) { + const tmp: string[] = []; + code.buildGlobal(this, tmp); + if (tmp.length !== 0) { + out.push(''); + out.push(...tmp); + } + } + } + + public buildMethods(out: string[]): void { + this.buildMatchSequence(out); + + for (const code of this.codeMap.values()) { + code.build(this, out); + out.push(''); + } + } + + public buildStates(out: string[]): void { + this.stateMap.forEach((lines, name) => { + out.push(`case ${name}: {`); + lines.forEach((line) => out.push(` ${line}`)); + out.push(' unreachable();'); + out.push('}'); + }); + } + + public addState(state: string, lines: ReadonlyArray): void { + assert(!this.stateMap.has(state)); + this.stateMap.set(state, lines); + } + + public buildCode(code: Code): string { + if (this.codeMap.has(code.ref.name)) { + assert.strictEqual(this.codeMap.get(code.ref.name)!, code, + `Code name conflict for "${code.ref.name}"`); + } else { + this.codeMap.set(code.ref.name, code); + } + + return `this._${code.ref.name}`; + } + + public getFieldType(field: string): string { + for (const property of this.properties) { + if (property.name === field) { + return property.ty; + } + } + throw new Error(`Field "${field}" not found`); + } + + public exportDefault(lines: string[]): string { + const out: string[] = []; + + out.push('\'use strict\''); + out.push(''); + + if (this.options.module === 'esm') { + out.push(`export default (binding) => {`); + } else { + out.push(`module.exports = (binding) => {`); + } + this.indent(out, lines, ' '); + out.push('};'); + + return out.join('\n'); + } + + public importCode(name: string, out: string[]) { + out.push(`const ${name} = binding.${name};`); + } + + // Helpers + + public unwrapCode(code: frontend.IWrap) + : Code { + const container = code as frontend.ContainerWrap; + return container.get(CONTAINER_KEY); + } + + public unwrapNode(node: WrappedNode): Node { + const container = node as frontend.ContainerWrap; + return container.get(CONTAINER_KEY); + } + + public unwrapTransform(node: frontend.IWrap) + : Transform { + const container = + node as frontend.ContainerWrap; + return container.get(CONTAINER_KEY); + } + + public indent(out: string[], lines: ReadonlyArray, pad: string) { + for (const line of lines) { + out.push(`${pad}${line}`); + } + } + + // MatchSequence cache + + // TODO(indutny): this is practically a copy from `bitcode/compilation.ts` + // Unify it somehow? + public getMatchSequence( + transform: frontend.IWrap, select: Buffer) + : string { + const wrap = this.unwrapTransform(transform); + + let res: MatchSequence; + if (this.matchSequence.has(wrap.ref.name)) { + res = this.matchSequence.get(wrap.ref.name)!; + } else { + res = new MatchSequence(wrap); + this.matchSequence.set(wrap.ref.name, res); + } + + return 'this.' + res.getName(); + } + + // Arguments + + public bufArg(): string { + return ARG_BUF; + } + + public offArg(): string { + return ARG_OFF; + } + + public currentArg(): string { + return ARG_CURRENT; + } + + public stateVar(): string { + return 'this'; + } + + public matchVar(): string { + return VAR_MATCH; + } + + // State fields + + public indexField(): string { + return this.stateField('_index'); + } + + public currentField(): string { + return this.stateField('_current'); + } + + public statusField(): string { + return this.stateField('_status'); + } + + public errorField(): string { + return this.stateField('error'); + } + + public reasonField(): string { + return this.stateField('reason'); + } + + public errorOffField(): string { + return this.stateField('errorOff'); + } + + public spanOffField(index: number): string { + return this.stateField(`_spanOff${index}`); + } + + public spanCbField(index: number): string { + return this.stateField(`_spanCb${index}`); + } + + public stateField(name: string): string { + return `this.${name}`; + } + + // Globals + + public blob(value: Buffer): string { + if (this.blobs.has(value)) { + return this.blobs.get(value)!; + } + const res = BLOB_PREFIX + this.blobs.size; + this.blobs.set(value, res); + return res; + } + + public table(value: ReadonlyArray): string { + const name = TABLE_PREFIX + this.tables.length; + this.tables.push({ name, value }); + return name; + } +} diff --git a/src/implementation/js/constants.ts b/src/implementation/js/constants.ts new file mode 100644 index 0000000..a25aad6 --- /dev/null +++ b/src/implementation/js/constants.ts @@ -0,0 +1,31 @@ +export const CONTAINER_KEY = 'js'; + +export const STATE_PREFIX = 'S_N_'; +export const STATE_ERROR = 'S_ERROR'; + +export const BLOB_PREFIX = 'BLOB_'; +export const TABLE_PREFIX = 'LOOKUP_TABLE_'; + +export const ARG_CURRENT = 'current'; +export const ARG_BUF = 'buf'; +export const ARG_OFF = 'off'; + +export const VAR_MATCH = 'match'; + +// MatchSequence + +export const SEQUENCE_COMPLETE = 'SEQUENCE_COMPLETE'; +export const SEQUENCE_MISMATCH = 'SEQUENCE_MISMATCH'; +export const SEQUENCE_PAUSE = 'SEQUENCE_PAUSE'; + +export const SIGNED_LIMITS: Map = new Map(); +SIGNED_LIMITS.set('i8', [ '-0x80', '0x7f' ]); +SIGNED_LIMITS.set('i16', [ '-0x8000', '0x7fff' ]); +SIGNED_LIMITS.set('i32', [ '-0x80000000', '0x7fffffff' ]); +SIGNED_LIMITS.set('i64', [ '-0x8000000000000000n', '0x7fffffffffffffffn' ]); + +export const UNSIGNED_LIMITS: Map = new Map(); +UNSIGNED_LIMITS.set('i8', [ '0', '0xff' ]); +UNSIGNED_LIMITS.set('i16', [ '0', '0xffff' ]); +UNSIGNED_LIMITS.set('i32', [ '0', '0xffffffff' ]); +UNSIGNED_LIMITS.set('i64', [ '0n', '0xffffffffffffffffn' ]); diff --git a/src/implementation/js/helpers/match-sequence.ts b/src/implementation/js/helpers/match-sequence.ts new file mode 100644 index 0000000..ecacd7d --- /dev/null +++ b/src/implementation/js/helpers/match-sequence.ts @@ -0,0 +1,56 @@ +import * as assert from 'assert'; +import { Buffer } from 'buffer'; +import * as frontend from 'llparse-frontend'; + +import { + SEQUENCE_COMPLETE, SEQUENCE_MISMATCH, SEQUENCE_PAUSE, +} from '../constants'; +import { Transform } from '../transform'; +import { Compilation } from '../compilation'; + +type TransformWrap = Transform; + +export class MatchSequence { + constructor(private readonly transform: TransformWrap) { + } + + public static buildGlobals(out: string[]): void { + out.push(`const ${SEQUENCE_COMPLETE} = 0;`); + out.push(`const ${SEQUENCE_PAUSE} = 1;`); + out.push(`const ${SEQUENCE_MISMATCH} = 2;`); + } + + public getName(): string { + return `_match_sequence_${this.transform.ref.name}`; + } + + public build(ctx: Compilation, out: string[]): void { + const buf = ctx.bufArg(); + const off = ctx.offArg(); + + out.push(`${this.getName()}(${buf}, ${off}, seq) {`); + + // Body + out.push(` let index = ${ctx.indexField()};`); + out.push(` for (; ${off} !== ${buf}.length; ${off}++) {`); + const single = this.transform.build(ctx, `${buf}[${off}]`); + out.push(` const current = ${single};`); + out.push(' if (current === seq[index]) {'); + out.push(' if (++index === seq.length) {'); + out.push(` ${ctx.indexField()} = 0;`); + out.push(` ${ctx.statusField()} = ${SEQUENCE_COMPLETE};`); + out.push(' return off;'); + out.push(' }'); + out.push(' } else {'); + out.push(` ${ctx.indexField()} = 0;`); + out.push(` ${ctx.statusField()} = ${SEQUENCE_MISMATCH};`); + out.push(' return off;'); + out.push(' }'); + out.push(' }'); + + out.push(` ${ctx.indexField()} = index;`); + out.push(` ${ctx.statusField()} = ${SEQUENCE_PAUSE};`); + out.push(' return off;'); + out.push('}'); + } +} diff --git a/src/implementation/js/index.ts b/src/implementation/js/index.ts new file mode 100644 index 0000000..5995a0e --- /dev/null +++ b/src/implementation/js/index.ts @@ -0,0 +1,176 @@ +import * as frontend from 'llparse-frontend'; + +import { + STATE_ERROR, + CONTAINER_KEY, +} from './constants'; +import { Compilation } from './compilation'; +import code from './code'; +import node from './node'; +import { Node } from './node'; +import transform from './transform'; + +export interface IJSCompilerOptions { + readonly debug?: string; + readonly module?: 'esm' | 'commonjs'; +} + +export interface IJSPublicOptions { + readonly module?: 'esm' | 'commonjs'; +} + +export class JSCompiler { + constructor(container: frontend.Container, + public readonly options: IJSCompilerOptions) { + container.add(CONTAINER_KEY, { code, node, transform }); + } + + public compile(info: frontend.IFrontendResult): string { + const ctx = new Compilation(info.prefix, info.properties, Object.assign({ + module: 'commonjs', + }, this.options)); + const out: string[] = []; + + // Queue span callbacks to be built before `executeSpans()` code gets called + // below. + ctx.reserveSpans(info.spans); + + const root = info.root as frontend.ContainerWrap; + const rootState = root.get>(CONTAINER_KEY) + .build(ctx); + + ctx.buildGlobals(out); + out.push(''); + + out.push('class Parser {'); + out.push(' constructor() {'); + out.push(` ${ctx.indexField()} = 0;`); + out.push(` ${ctx.currentField()} = ${rootState};`); + out.push(` ${ctx.statusField()} = 0;`); + out.push(` ${ctx.errorField()} = 0;`); + out.push(` ${ctx.reasonField()} = null;`); + out.push(` ${ctx.errorOffField()} = 0;`); + + for (const { ty, name } of info.properties) { + let value; + if (ty === 'i64') { + value = '0n'; + } else if (ty === 'ptr') { + value = 'null'; + } else { + value = '0'; + } + out.push(` ${ctx.stateField(name)} = ${value};`); + } + + out.push(' }'); + out.push(''); + + let tmp: string[] = []; + ctx.buildMethods(tmp); + ctx.indent(out, tmp, ' '); + + // Run + + out.push(` _run(${ctx.currentArg()}, ${ctx.bufArg()}, ${ctx.offArg()}) {`); + out.push(` let ${ctx.matchVar()};`); + out.push(' for (;;) {'); + out.push(` switch (${ctx.currentArg()} | 0) {`); + + tmp = []; + ctx.buildStates(tmp); + ctx.indent(out, tmp, ' '); + + out.push(' }'); + out.push(' }'); + out.push(' unreachable();'); + out.push(' }'); + out.push(''); + + // Execute + + out.push(` execute(${ctx.bufArg()}) {`); + out.push(' // check lingering errors'); + out.push(` if (${ctx.errorField()} !== 0) {`); + out.push(` return ${ctx.errorField()};`); + out.push(' }'); + out.push(''); + + tmp = []; + this.restartSpans(ctx, info, tmp); + ctx.indent(out, tmp, ' '); + + out.push(` const next = this._run(` + + `${ctx.currentField()}, ${ctx.bufArg()}, 0);`); + out.push(` if (next === ${STATE_ERROR}) {`); + out.push(` return ${ctx.errorField()};`); + out.push(' }'); + out.push(` ${ctx.currentField()} = next;`); + out.push(''); + + tmp = []; + this.executeSpans(ctx, info, tmp); + ctx.indent(out, tmp, ' '); + + out.push(' return 0;'); + + out.push(' }'); + out.push('}'); + out.push(''); + + out.push('return Parser;'); + + return ctx.exportDefault(out); + } + + private restartSpans(ctx: Compilation, info: frontend.IFrontendResult, + out: string[]): void { + if (info.spans.length === 0) { + return; + } + + out.push('// restart spans'); + for (const span of info.spans) { + const offField = ctx.spanOffField(span.index); + + out.push(`if (${offField} !== -1) {`); + out.push(` ${offField} = 0;`); + out.push('}'); + } + out.push(''); + } + + private executeSpans(ctx: Compilation, info: frontend.IFrontendResult, + out: string[]): void { + if (info.spans.length === 0) { + return; + } + + out.push('// execute spans'); + for (const span of info.spans) { + const offField = ctx.spanOffField(span.index); + let callback: string; + if (span.callbacks.length === 1) { + callback = ctx.buildCode(ctx.unwrapCode(span.callbacks[0])); + } else { + callback = ctx.spanCbField(span.index); + } + + const args = [ + ctx.bufArg(), offField, `${ctx.bufArg()}.length`, + ]; + + out.push(`if (${offField} !== -1) {`); + out.push(` const error = ${callback}(${args.join(', ')});`); + + // TODO(indutny): de-duplicate this here and in SpanEnd + out.push(' if (error !== 0) {'); + out.push(` ${ctx.errorField()} = error;`); + out.push(` ${ctx.errorOffField()} = ${ctx.bufArg()}.length;`); + out.push(' return error;'); + out.push(' }'); + out.push('}'); + } + out.push(''); + } +} diff --git a/src/implementation/js/node/base.ts b/src/implementation/js/node/base.ts new file mode 100644 index 0000000..32c6923 --- /dev/null +++ b/src/implementation/js/node/base.ts @@ -0,0 +1,78 @@ +import * as assert from 'assert'; +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { + STATE_PREFIX, +} from '../constants'; + +export interface INodeEdge { + readonly node: frontend.IWrap; + readonly noAdvance: boolean; + readonly value?: number; +} + +export abstract class Node { + protected cachedDecl: string | undefined; + protected privCompilation: Compilation | undefined; + + constructor(public readonly ref: T) { + } + + public build(compilation: Compilation): string { + if (this.cachedDecl !== undefined) { + return this.cachedDecl; + } + + const res = STATE_PREFIX + this.ref.id.name.toUpperCase(); + this.cachedDecl = res; + + this.privCompilation = compilation; + + const out: string[] = []; + compilation.debug(out, + `Entering node "${this.ref.id.originalName}" ("${this.ref.id.name}")`); + this.doBuild(out); + + compilation.addState(res, out); + + return res; + } + + protected get compilation(): Compilation { + assert(this.privCompilation !== undefined); + return this.privCompilation!; + } + + protected prologue(out: string[]): void { + const ctx = this.compilation; + + out.push(`if (${ctx.offArg()} === ${ctx.bufArg()}.length) {`); + + const tmp: string[] = []; + this.pause(tmp); + this.compilation.indent(out, tmp, ' '); + + out.push('}'); + } + + protected pause(out: string[]): void { + out.push(`return ${this.cachedDecl};`); + } + + protected tailTo(out: string[], edge: INodeEdge): void { + const ctx = this.compilation; + const target = ctx.unwrapNode(edge.node).build(ctx); + + if (!edge.noAdvance) { + out.push(`${ctx.offArg()}++;`); + } + if (edge.value !== undefined) { + out.push(`${ctx.matchVar()} = ${edge.value};`); + } + out.push(`${ctx.currentArg()} = ${target};`); + out.push('continue;'); + } + + protected abstract doBuild(out: string[]): void; +} diff --git a/src/implementation/js/node/consume.ts b/src/implementation/js/node/consume.ts new file mode 100644 index 0000000..dd676b5 --- /dev/null +++ b/src/implementation/js/node/consume.ts @@ -0,0 +1,32 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { Node } from './base'; + +export class Consume extends Node { + public doBuild(out: string[]): void { + const ctx = this.compilation; + + const index = ctx.stateField(this.ref.field); + const ty = ctx.getFieldType(this.ref.field); + + out.push(`let avail = ${ctx.bufArg()}.length - ${ctx.offArg()};`); + out.push(`const need = ${index};`); + if (ty === 'i64') { + out.push('avail = BigInt(avail);'); + } + out.push('if (avail >= need) {'); + out.push(` off += ${ ty === 'i64' ? 'Number(need)' : 'need' };`); + out.push(` ${index} = ${ ty === 'i64' ? '0n' : '' };`); + + const tmp: string[] = []; + this.tailTo(tmp, this.ref.otherwise!); + ctx.indent(out, tmp, ' '); + + out.push('}'); + out.push(''); + + out.push(`${index} -= avail;`); + this.pause(out); + } +} diff --git a/src/implementation/js/node/empty.ts b/src/implementation/js/node/empty.ts new file mode 100644 index 0000000..e28ecb5 --- /dev/null +++ b/src/implementation/js/node/empty.ts @@ -0,0 +1,16 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { Node } from './base'; + +export class Empty extends Node { + public doBuild(out: string[]): void { + const otherwise = this.ref.otherwise!; + + if (!otherwise.noAdvance) { + this.prologue(out); + } + + this.tailTo(out, otherwise); + } +} diff --git a/src/implementation/js/node/error.ts b/src/implementation/js/node/error.ts new file mode 100644 index 0000000..50bdc6a --- /dev/null +++ b/src/implementation/js/node/error.ts @@ -0,0 +1,32 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { STATE_ERROR } from '../constants'; +import { Node } from './base'; + +class ErrorNode extends Node { + protected storeError(out: string[]): void { + const ctx = this.compilation; + + let hexCode: string; + if (this.ref.code < 0) { + hexCode = `-0x` + this.ref.code.toString(16); + } else { + hexCode = '0x' + this.ref.code.toString(16); + } + + out.push(`${ctx.errorField()} = ${hexCode};`); + out.push(`${ctx.reasonField()} = ${JSON.stringify(this.ref.reason)};`); + out.push(`${ctx.errorOffField()} = ${ctx.offArg()};`); + } + + public doBuild(out: string[]): void { + this.storeError(out); + + // Non-recoverable state + out.push(`${this.compilation.currentField()} = ${STATE_ERROR};`); + out.push(`return ${STATE_ERROR};`); + } +} + +export { ErrorNode as Error }; diff --git a/src/implementation/js/node/index.ts b/src/implementation/js/node/index.ts new file mode 100644 index 0000000..ba751d9 --- /dev/null +++ b/src/implementation/js/node/index.ts @@ -0,0 +1,27 @@ +import * as frontend from 'llparse-frontend'; + +import { Consume } from './consume'; +import { Empty } from './empty'; +import { Error as ErrorNode } from './error'; +import { Invoke } from './invoke'; +import { Pause } from './pause'; +import { Sequence } from './sequence'; +import { Single } from './single'; +import { SpanEnd } from './span-end'; +import { SpanStart } from './span-start'; +import { TableLookup } from './table-lookup'; + +export { Node } from './base'; + +export default { + Consume, + Empty, + Error: class Error extends ErrorNode {}, + Invoke, + Pause, + Sequence, + Single, + SpanEnd, + SpanStart, + TableLookup, +}; diff --git a/src/implementation/js/node/invoke.ts b/src/implementation/js/node/invoke.ts new file mode 100644 index 0000000..3169e19 --- /dev/null +++ b/src/implementation/js/node/invoke.ts @@ -0,0 +1,43 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { Node } from './base'; + +export class Invoke extends Node { + public doBuild(out: string[]): void { + const ctx = this.compilation; + + const code = ctx.unwrapCode(this.ref.code); + const codeDecl = ctx.buildCode(code); + + const args: string[] = [ + ctx.bufArg(), + ctx.offArg(), + ]; + + const signature = code.ref.signature; + if (signature === 'value') { + args.push(ctx.matchVar()); + } + + out.push(`switch (${codeDecl}(${args.join(', ')}) | 0) {`); + let tmp: string[]; + + for (const edge of this.ref.edges) { + out.push(` case ${edge.code}:`); + tmp = []; + this.tailTo(tmp, { + noAdvance: true, + node: edge.node, + value: undefined, + }); + ctx.indent(out, tmp, ' '); + } + + out.push(' default:'); + tmp = []; + this.tailTo(tmp, this.ref.otherwise!); + ctx.indent(out, tmp, ' '); + out.push('}'); + } +} diff --git a/src/implementation/js/node/pause.ts b/src/implementation/js/node/pause.ts new file mode 100644 index 0000000..bf696cf --- /dev/null +++ b/src/implementation/js/node/pause.ts @@ -0,0 +1,18 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { STATE_ERROR } from '../constants'; +import { Error as ErrorNode } from './error'; + +export class Pause extends ErrorNode { + public doBuild(out: string[]): void { + const ctx = this.compilation; + + this.storeError(out); + + // Recoverable state + const otherwise = ctx.unwrapNode(this.ref.otherwise!.node).build(ctx); + out.push(`${ctx.currentField()} = ${otherwise}`); + out.push(`return ${STATE_ERROR};`); + } +} diff --git a/src/implementation/js/node/sequence.ts b/src/implementation/js/node/sequence.ts new file mode 100644 index 0000000..01acdd3 --- /dev/null +++ b/src/implementation/js/node/sequence.ts @@ -0,0 +1,49 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { + SEQUENCE_COMPLETE, SEQUENCE_MISMATCH, SEQUENCE_PAUSE, +} from '../constants'; +import { Node } from './base'; + +export class Sequence extends Node { + public doBuild(out: string[]): void { + const ctx = this.compilation; + + this.prologue(out); + + const matchSequence = ctx.getMatchSequence(this.ref.transform!, + this.ref.select); + + out.push(`${ctx.offArg()} = ${matchSequence}(${ctx.bufArg()}, ` + + `${ctx.offArg()}, ${ctx.blob(this.ref.select)});`); + + let tmp: string[]; + + out.push(`const status = ${ctx.statusField()};`); + + out.push(`if (status === ${SEQUENCE_COMPLETE}) {`); + + tmp = []; + this.tailTo(tmp, { + noAdvance: false, + node: this.ref.edge!.node, + value: this.ref.edge!.value, + }); + ctx.indent(out, tmp, ' '); + + out.push(`} else if (status === ${SEQUENCE_PAUSE}) {`); + + tmp = []; + this.pause(tmp); + ctx.indent(out, tmp, ' '); + + out.push('} else {'); + + tmp = []; + this.tailTo(tmp, this.ref.otherwise!); + ctx.indent(out, tmp, ' '); + + out.push('}'); + } +} diff --git a/src/implementation/js/node/single.ts b/src/implementation/js/node/single.ts new file mode 100644 index 0000000..f0947b3 --- /dev/null +++ b/src/implementation/js/node/single.ts @@ -0,0 +1,44 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { Node } from './base'; + +export class Single extends Node { + public doBuild(out: string[]): void { + const ctx = this.compilation; + + const otherwise = this.ref.otherwise!; + + this.prologue(out); + + const transform = ctx.unwrapTransform(this.ref.transform!); + const current = transform.build(ctx, `${ctx.bufArg()}[${ctx.offArg()}]`); + + out.push(`switch (${current} | 0) {`) + this.ref.edges.forEach((edge) => { + let ch: string = edge.key.toString(); + + // Non-printable ASCII, or single-quote + if (!(edge.key < 0x20 || edge.key > 0x7e || edge.key === 0x27)) { + ch = `/* '${String.fromCharCode(edge.key)}' */ ` + ch; + } + out.push(` case ${ch}: {`); + + const tmp: string[] = []; + this.tailTo(tmp, edge); + ctx.indent(out, tmp, ' '); + + out.push(' }'); + }); + + out.push(` default: {`); + + const tmp: string[] = []; + this.tailTo(tmp, otherwise); + ctx.indent(out, tmp, ' '); + + out.push(' }'); + + out.push(`}`); + } +} diff --git a/src/implementation/js/node/span-end.ts b/src/implementation/js/node/span-end.ts new file mode 100644 index 0000000..2f9acd8 --- /dev/null +++ b/src/implementation/js/node/span-end.ts @@ -0,0 +1,51 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { STATE_ERROR } from '../constants'; +import { Node } from './base'; + +export class SpanEnd extends Node { + public doBuild(out: string[]): void { + const ctx = this.compilation; + const field = this.ref.field; + const offField = ctx.spanOffField(field.index); + + // Load start position + out.push(`const start = ${offField};`); + + // ...and reset + out.push(`${offField} = -1;`); + + // Invoke callback + const callback = ctx.buildCode(ctx.unwrapCode(this.ref.callback)); + out.push(`const err = ${callback}(${ctx.bufArg()}, ` + + `start, ${ctx.offArg()});`); + + out.push('if (err !== 0) {'); + const tmp: string[] = []; + this.buildError(tmp, 'err'); + ctx.indent(out, tmp, ' '); + out.push('}'); + + const otherwise = this.ref.otherwise!; + this.tailTo(out, otherwise); + } + + private buildError(out: string[], code: string) { + const ctx = this.compilation; + + out.push(`${ctx.errorField()} = ${code};`); + + const otherwise = this.ref.otherwise!; + let resumeOff = ctx.offArg(); + if (!otherwise.noAdvance) { + resumeOff = `(${resumeOff} + 1)`; + } + + out.push(`${ctx.errorOffField()} = ${resumeOff};`); + + const resumptionTarget = ctx.unwrapNode(otherwise.node).build(ctx); + out.push(`${ctx.currentField()} = ${resumptionTarget};`); + out.push(`return ${STATE_ERROR};`); + } +} diff --git a/src/implementation/js/node/span-start.ts b/src/implementation/js/node/span-start.ts new file mode 100644 index 0000000..b095482 --- /dev/null +++ b/src/implementation/js/node/span-start.ts @@ -0,0 +1,26 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { Node } from './base'; + +export class SpanStart extends Node { + public doBuild(out: string[]): void { + // Prevent spurious empty spans + this.prologue(out); + + const ctx = this.compilation; + const field = this.ref.field; + + const offField = ctx.spanOffField(field.index); + out.push(`${offField} = ${ctx.offArg()};`); + + if (field.callbacks.length > 1) { + const cbField = ctx.spanCbField(field.index); + const callback = ctx.unwrapCode(this.ref.callback); + out.push(`${cbField} = ${ctx.buildCode(callback)};`); + } + + const otherwise = this.ref.otherwise!; + this.tailTo(out, otherwise); + } +} diff --git a/src/implementation/js/node/table-lookup.ts b/src/implementation/js/node/table-lookup.ts new file mode 100644 index 0000000..cb90aea --- /dev/null +++ b/src/implementation/js/node/table-lookup.ts @@ -0,0 +1,60 @@ +import * as assert from 'assert'; +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { Node } from './base'; + +const MAX_CHAR = 0xff; + +export class TableLookup extends Node { + public doBuild(out: string[]): void { + const ctx = this.compilation; + + const table = this.buildTable(); + + this.prologue(out); + + const transform = ctx.unwrapTransform(this.ref.transform!); + const current = transform.build(ctx, `${ctx.bufArg()}[${ctx.offArg()}]`); + + out.push(`switch (${table}[${current}]) {`); + + for (const [ index, edge ] of this.ref.edges.entries()) { + out.push(` case ${index + 1}: {`); + + const tmp: string[] = []; + const edge = this.ref.edges[index]; + this.tailTo(tmp, { + noAdvance: edge.noAdvance, + node: edge.node, + value: undefined, + }); + ctx.indent(out, tmp, ' '); + + out.push(' }'); + } + + out.push(` default: {`); + + const tmp: string[] = []; + this.tailTo(tmp, this.ref.otherwise!); + ctx.indent(out, tmp, ' '); + + out.push(' }'); + out.push('}'); + } + + // TODO(indutny): reduce copy-paste between `C` and `bitcode` implementations + private buildTable(): string { + const table: number[] = new Array(MAX_CHAR + 1).fill(0); + + for (const [ index, edge ] of this.ref.edges.entries()) { + edge.keys.forEach((key) => { + assert.strictEqual(table[key], 0); + table[key] = index + 1; + }); + } + + return this.compilation.table(table); + } +} diff --git a/src/implementation/js/transform/base.ts b/src/implementation/js/transform/base.ts new file mode 100644 index 0000000..82028d5 --- /dev/null +++ b/src/implementation/js/transform/base.ts @@ -0,0 +1,10 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; + +export abstract class Transform { + constructor(public readonly ref: T) { + } + + public abstract build(ctx: Compilation, value: string): string; +} diff --git a/src/implementation/js/transform/id.ts b/src/implementation/js/transform/id.ts new file mode 100644 index 0000000..6c6105f --- /dev/null +++ b/src/implementation/js/transform/id.ts @@ -0,0 +1,11 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { Transform } from './base'; + +export class ID extends Transform { + public build(ctx: Compilation, value: string): string { + // Identity transformation + return value; + } +} diff --git a/src/implementation/js/transform/index.ts b/src/implementation/js/transform/index.ts new file mode 100644 index 0000000..a8c280f --- /dev/null +++ b/src/implementation/js/transform/index.ts @@ -0,0 +1,9 @@ +import { ID } from './id'; +import { ToLowerUnsafe } from './to-lower-unsafe'; + +export { Transform } from './base'; + +export default { + ID, + ToLowerUnsafe, +}; diff --git a/src/implementation/js/transform/to-lower-unsafe.ts b/src/implementation/js/transform/to-lower-unsafe.ts new file mode 100644 index 0000000..27f608c --- /dev/null +++ b/src/implementation/js/transform/to-lower-unsafe.ts @@ -0,0 +1,10 @@ +import * as frontend from 'llparse-frontend'; + +import { Compilation } from '../compilation'; +import { Transform } from './base'; + +export class ToLowerUnsafe extends Transform { + public build(ctx: Compilation, value: string): string { + return `((${value}) | 0x20)`; + } +} diff --git a/test/fixtures/extra.ts b/test/fixtures/extra.ts new file mode 100644 index 0000000..6f6425b --- /dev/null +++ b/test/fixtures/extra.ts @@ -0,0 +1,57 @@ +export default (binding, inBench) => { + const nop = () => 0; + + binding.llparse__print_zero = inBench ? nop : (p, buf, off) => { + binding.llparse__print(buf, off, '0'); + return 0; + }; + + binding.llparse__print_one = inBench ? nop : (p, buf, off) => { + binding.llparse__print(buf, off, '1'); + return 0; + }; + + binding.llparse__print_off = inBench ? nop : (p, buf, off) => { + binding.llparse__print(buf, off, ''); + return 0; + }; + + binding.llparse__print_match = inBench ? nop : (p, buf, off, value) => { + binding.llparse__print(buf, off, 'match=%d', value); + return 0; + }; + + binding.llparse__on_dot = inBench ? nop : (p, buf, off, offLen) => { + return binding.llparse__print_span('dot', buf, off, offLen); + }; + + binding.llparse__on_dash = inBench ? nop : (p, buf, off, offLen) => { + return binding.llparse__print_span('dash', buf, off, offLen); + }; + + binding.llparse__on_underscore = inBench ? nop : (p, buf, off, offLen) => { + return binding.llparse__print_span('underscore', buf, off, offLen); + }; + + /* A span callback, really */ + binding.llparse__please_fail = (p) => { + p.reason = 'please fail'; + return 1; + }; + + /* A span callback, really */ + let onceCounter = 0; + + binding.llparse__pause_once = (p, buf, off, offLen) => { + if (!inBench) { + binding.llparse__print_span('pause', buf, off, offLen); + } + + if (onceCounter !== 0) { + return 0; + } + + onceCounter++; + return binding.LLPARSE__ERROR_PAUSE; + }; +}; diff --git a/test/fixtures/index.ts b/test/fixtures/index.ts index f53c909..84e0de9 100644 --- a/test/fixtures/index.ts +++ b/test/fixtures/index.ts @@ -20,6 +20,9 @@ export function build(llparse: LLParse, node: source.node.Node, outFile: string) c: { header: outFile, }, + js: { + binding: path.join(__dirname, 'js-binding.js'), + }, }), outFile); }