UNPKG

llparse

Version:

Compile incremental parsers to C code

315 lines 11.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TableLookup = void 0; const assert = require("assert"); const base_1 = require("./base"); const MAX_CHAR = 0xff; const TABLE_GROUP = 16; // _mm_cmpestri takes 8 ranges const SSE_RANGES_LEN = 16; // _mm_cmpestri takes 128bit input const SSE_RANGES_PAD = 16; const MAX_SSE_CALLS = 2; const MAX_NEON_RANGES = 6; const MAX_WASM_RANGES = 6; const SSE_ALIGNMENT = 16; class TableLookup extends base_1.Node { doBuild(out) { const ctx = this.compilation; const table = this.buildTable(); for (const line of table.declaration) { out.push(line); } this.prologue(out); const transform = ctx.unwrapTransform(this.ref.transform); // Try to vectorize nodes matching characters and looping to themselves // NOTE: `switch` below triggers when there is not enough characters in the // stream for vectorized processing. if (this.canVectorize()) { this.buildSSE(out); this.buildNeon(out); this.buildWASM(out); } const current = transform.build(ctx, `*${ctx.posArg()}`); out.push(`switch (${table.name}[(uint8_t) ${current}]) {`); for (const [index, edge] of this.ref.edges.entries()) { out.push(` case ${index + 1}: {`); const tmp = []; this.tailTo(tmp, { noAdvance: edge.noAdvance, node: edge.node, value: undefined, }); ctx.indent(out, tmp, ' '); out.push(' }'); } out.push(` default: {`); const tmp = []; this.tailTo(tmp, this.ref.otherwise); ctx.indent(out, tmp, ' '); out.push(' }'); out.push('}'); } canVectorize() { // Transformation is not supported atm if (this.ref.transform && this.ref.transform.ref.name !== 'id') { return false; } if (this.ref.edges.length !== 1) { return false; } const edge = this.ref.edges[0]; if (!edge || edge.node.ref !== this.ref) { return false; } assert.strictEqual(edge.noAdvance, false); return true; } buildRanges(edge) { // NOTE: keys are sorted const ranges = []; let first; let last; for (const key of edge.keys) { if (first === undefined) { first = key; } if (last === undefined) { last = key; } if (key - last > 1) { ranges.push(first, last); first = key; } last = key; } if (first !== undefined && last !== undefined) { ranges.push(first, last); } return ranges; } buildSSE(out) { const ctx = this.compilation; const edge = this.ref.edges[0]; assert(edge !== undefined); const ranges = this.buildRanges(edge); if (ranges.length === 0) { return false; } // Way too many calls would be required if (ranges.length > MAX_SSE_CALLS * SSE_RANGES_LEN) { return false; } out.push('#ifdef __SSE4_2__'); out.push(`if (${ctx.endPosArg()} - ${ctx.posArg()} >= 16) {`); out.push(' __m128i ranges;'); out.push(' __m128i input;'); out.push(' int match_len;'); out.push(''); out.push(' /* Load input */'); out.push(` input = _mm_loadu_si128((__m128i const*) ${ctx.posArg()});`); for (let off = 0; off < ranges.length; off += SSE_RANGES_LEN) { const subRanges = ranges.slice(off, off + SSE_RANGES_LEN); let paddedRanges = subRanges.slice(); while (paddedRanges.length < SSE_RANGES_PAD) { paddedRanges.push(0); } const blob = ctx.blob(Buffer.from(paddedRanges), SSE_ALIGNMENT); out.push(` ranges = _mm_loadu_si128((__m128i const*) ${blob});`); out.push(''); out.push(' /* Find first character that does not match `ranges` */'); out.push(` match_len = _mm_cmpestri(ranges, ${subRanges.length},`); out.push(' input, 16,'); out.push(' _SIDD_UBYTE_OPS | _SIDD_CMP_RANGES |'); out.push(' _SIDD_NEGATIVE_POLARITY);'); out.push(''); out.push(' if (match_len != 0) {'); out.push(` ${ctx.posArg()} += match_len;`); const tmp = []; this.tailTo(tmp, { noAdvance: true, node: edge.node, }); ctx.indent(out, tmp, ' '); out.push(' }'); } { const tmp = []; this.tailTo(tmp, this.ref.otherwise); ctx.indent(out, tmp, ' '); } out.push('}'); out.push('#endif /* __SSE4_2__ */'); return true; } buildNeon(out) { const ctx = this.compilation; const edge = this.ref.edges[0]; assert(edge !== undefined); const ranges = this.buildRanges(edge); if (ranges.length === 0) { return false; } // Way too many calls would be required if (ranges.length > MAX_NEON_RANGES) { return false; } out.push('#ifdef __ARM_NEON__'); out.push(`while (${ctx.endPosArg()} - ${ctx.posArg()} >= 16) {`); out.push(' uint8x16_t input;'); out.push(' uint8x16_t single;'); out.push(' uint8x16_t mask;'); out.push(' uint8x8_t narrow;'); out.push(' uint64_t match_mask;'); out.push(' int match_len;'); out.push(''); out.push(' /* Load input */'); out.push(` input = vld1q_u8(${ctx.posArg()});`); out.push(' /* Find first character that does not match `ranges` */'); function v128(value) { return `vdupq_n_u8(${ctx.toChar(value)})`; } for (let off = 0; off < ranges.length; off += 2) { const start = ranges[off]; const end = ranges[off + 1]; assert(start !== undefined); assert(end !== undefined); // Same character, equality is sufficient (and faster) if (start === end) { out.push(` single = vceqq_u8(input, ${v128(start)});`); } else { out.push(` single = vandq_u16(`); out.push(` vcgeq_u8(input, ${v128(start)}),`); out.push(` vcleq_u8(input, ${v128(end)})`); out.push(' );'); } if (off === 0) { out.push(' mask = single;'); } else { out.push(' mask = vorrq_u16(mask, single);'); } } // https://community.arm.com/arm-community-blogs/b/servers-and-cloud-computing-blog/posts/porting-x86-vector-bitmask-optimizations-to-arm-neon out.push(' narrow = vshrn_n_u16(mask, 4);'); out.push(' match_mask = ~vget_lane_u64(vreinterpret_u64_u8(narrow), 0);'); out.push(' match_len = __builtin_ctzll(match_mask) >> 2;'); out.push(' if (match_len != 16) {'); out.push(` ${ctx.posArg()} += match_len;`); { const tmp = []; this.tailTo(tmp, this.ref.otherwise); ctx.indent(out, tmp, ' '); } out.push(' }'); out.push(` ${ctx.posArg()} += 16;`); out.push('}'); out.push(`if (${ctx.posArg()} == ${ctx.endPosArg()}) {`); { const tmp = []; this.pause(tmp); this.compilation.indent(out, tmp, ' '); } out.push('}'); out.push('#endif /* __ARM_NEON__ */'); return true; } buildWASM(out) { const ctx = this.compilation; const edge = this.ref.edges[0]; assert(edge !== undefined); const ranges = this.buildRanges(edge); if (ranges.length === 0) { return false; } // Way too many calls would be required if (ranges.length > MAX_WASM_RANGES) { return false; } out.push('#ifdef __wasm_simd128__'); out.push(`while (${ctx.endPosArg()} - ${ctx.posArg()} >= 16) {`); out.push(' v128_t input;'); out.push(' v128_t mask;'); out.push(' v128_t single;'); out.push(' int match_len;'); out.push(''); out.push(' /* Load input */'); out.push(` input = wasm_v128_load(${ctx.posArg()});`); out.push(' /* Find first character that does not match `ranges` */'); function v128(value) { return `wasm_u8x16_const_splat(${ctx.toChar(value)})`; } for (let off = 0; off < ranges.length; off += 2) { const start = ranges[off]; const end = ranges[off + 1]; assert(start !== undefined); assert(end !== undefined); // Same character, equality is sufficient (and faster) if (start === end) { out.push(` single = wasm_i8x16_eq(input, ${v128(start)});`); } else { out.push(` single = wasm_v128_and(`); out.push(` wasm_i8x16_ge(input, ${v128(start)}),`); out.push(` wasm_i8x16_le(input, ${v128(end)})`); out.push(' );'); } if (off === 0) { out.push(' mask = single;'); } else { out.push(' mask = wasm_v128_or(mask, single);'); } } out.push(' match_len = __builtin_ctz('); out.push(' ~wasm_i8x16_bitmask(mask)'); out.push(' );'); out.push(' if (match_len != 16) {'); out.push(` ${ctx.posArg()} += match_len;`); { const tmp = []; this.tailTo(tmp, this.ref.otherwise); ctx.indent(out, tmp, ' '); } out.push(' }'); out.push(` ${ctx.posArg()} += 16;`); out.push('}'); out.push(`if (${ctx.posArg()} == ${ctx.endPosArg()}) {`); { const tmp = []; this.pause(tmp); this.compilation.indent(out, tmp, ' '); } out.push('}'); out.push('#endif /* __wasm_simd128__ */'); return true; } buildTable() { const table = 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; }); } const lines = [ 'static uint8_t lookup_table[] = {', ]; for (let i = 0; i < table.length; i += TABLE_GROUP) { let line = ` ${table.slice(i, i + TABLE_GROUP).join(', ')}`; if (i + TABLE_GROUP < table.length) { line += ','; } lines.push(line); } lines.push('};'); return { name: 'lookup_table', declaration: lines, }; } } exports.TableLookup = TableLookup; //# sourceMappingURL=table-lookup.js.map