From 720bbd7353f3e4a5ca2dacca2c16066f7ca6a836 Mon Sep 17 00:00:00 2001 From: Siddharth Gelera Date: Tue, 3 Feb 2026 16:21:55 +0530 Subject: [PATCH 1/5] refactor: low hanging perf fixes to avoid v8 deopt --- packages/ohm-js/src/PosInfo.js | 2 +- packages/ohm-js/src/Semantics.js | 14 +-- packages/ohm-js/src/common.js | 153 +++++++++++++++++++++++++---- packages/ohm-js/src/pexprs-eval.js | 2 +- 4 files changed, 144 insertions(+), 27 deletions(-) diff --git a/packages/ohm-js/src/PosInfo.js b/packages/ohm-js/src/PosInfo.js index 8e40d3a4..4104a0cc 100644 --- a/packages/ohm-js/src/PosInfo.js +++ b/packages/ohm-js/src/PosInfo.js @@ -89,7 +89,7 @@ export class PosInfo { Object.keys(memo).forEach(k => { const memoRec = memo[k]; if (pos + memoRec.examinedLength > invalidatedIdx) { - delete memo[k]; + memo[k] = undefined; } else { this.maxExaminedLength = Math.max(this.maxExaminedLength, memoRec.examinedLength); this.maxRightmostFailureOffset = Math.max( diff --git a/packages/ohm-js/src/Semantics.js b/packages/ohm-js/src/Semantics.js index a4033b5e..3121db50 100644 --- a/packages/ohm-js/src/Semantics.js +++ b/packages/ohm-js/src/Semantics.js @@ -39,7 +39,7 @@ class Wrapper { _forgetMemoizedResultFor(attributeName) { // Remove the memoized attribute from the cstNode and all its children. - delete this._node[this._semantics.attributeKeys[attributeName]]; + this._node[this._semantics.attributeKeys[attributeName]] = undefined; this.children.forEach(child => { child._forgetMemoizedResultFor(attributeName); }); @@ -341,7 +341,7 @@ export class Semantics { const thisThing = this._semantics[typePlural][name]; // Check that the caller passed the correct number of arguments. - if (arguments.length !== thisThing.formals.length) { + if (args.length !== thisThing.formals.length) { throw new Error( 'Invalid number of arguments passed to ' + name + @@ -358,9 +358,9 @@ export class Semantics { // Create an "arguments object" from the arguments that were passed to this // operation / attribute. const argsObj = Object.create(null); - for (const [idx, val] of Object.entries(args)) { + for (let idx = 0; idx < args.length; idx += 1) { const formal = thisThing.formals[idx]; - argsObj[formal] = val; + argsObj[formal] = args[idx]; } const oldArgs = this.args; @@ -569,12 +569,12 @@ Semantics.createSemantics = function (grammar, optSuperSemantics) { let semantic; if (operationOrAttributeName in s.operations) { semantic = s.operations[operationOrAttributeName]; - delete s.operations[operationOrAttributeName]; + s.operations[operationOrAttributeName] = undefined; } else if (operationOrAttributeName in s.attributes) { semantic = s.attributes[operationOrAttributeName]; - delete s.attributes[operationOrAttributeName]; + s.attributes[operationOrAttributeName] = undefined; } - delete s.Wrapper.prototype[operationOrAttributeName]; + s.Wrapper.prototype[operationOrAttributeName] = undefined; return semantic; }; proxy.getOperationNames = function () { diff --git a/packages/ohm-js/src/common.js b/packages/ohm-js/src/common.js index 3fac6fd2..5ca1616d 100644 --- a/packages/ohm-js/src/common.js +++ b/packages/ohm-js/src/common.js @@ -4,19 +4,136 @@ // Helpers -const escapeStringFor = {}; -for (let c = 0; c < 128; c++) { - escapeStringFor[c] = String.fromCharCode(c); -} -escapeStringFor["'".charCodeAt(0)] = "\\'"; -escapeStringFor['"'.charCodeAt(0)] = '\\"'; -escapeStringFor['\\'.charCodeAt(0)] = '\\\\'; -escapeStringFor['\b'.charCodeAt(0)] = '\\b'; -escapeStringFor['\f'.charCodeAt(0)] = '\\f'; -escapeStringFor['\n'.charCodeAt(0)] = '\\n'; -escapeStringFor['\r'.charCodeAt(0)] = '\\r'; -escapeStringFor['\t'.charCodeAt(0)] = '\\t'; -escapeStringFor['\u000b'.charCodeAt(0)] = '\\v'; +const escapeStringFor = { + 0: '\x00', + 1: '\x01', + 2: '\x02', + 3: '\x03', + 4: '\x04', + 5: '\x05', + 6: '\x06', + 7: '\x07', + 8: '\\b', + 9: '\\t', + 10: '\\n', + 11: '\\v', + 12: '\\f', + 13: '\\r', + 14: '\x0E', + 15: '\x0F', + 16: '\x10', + 17: '\x11', + 18: '\x12', + 19: '\x13', + 20: '\x14', + 21: '\x15', + 22: '\x16', + 23: '\x17', + 24: '\x18', + 25: '\x19', + 26: '\x1A', + 27: '\x1B', + 28: '\x1C', + 29: '\x1D', + 30: '\x1E', + 31: '\x1F', + 32: ' ', + 33: '!', + 34: '\\"', + 35: '#', + 36: '$', + 37: '%', + 38: '&', + 39: "\\'", + 40: '(', + 41: ')', + 42: '*', + 43: '+', + 44: ',', + 45: '-', + 46: '.', + 47: '/', + 48: '0', + 49: '1', + 50: '2', + 51: '3', + 52: '4', + 53: '5', + 54: '6', + 55: '7', + 56: '8', + 57: '9', + 58: ':', + 59: ';', + 60: '<', + 61: '=', + 62: '>', + 63: '?', + 64: '@', + 65: 'A', + 66: 'B', + 67: 'C', + 68: 'D', + 69: 'E', + 70: 'F', + 71: 'G', + 72: 'H', + 73: 'I', + 74: 'J', + 75: 'K', + 76: 'L', + 77: 'M', + 78: 'N', + 79: 'O', + 80: 'P', + 81: 'Q', + 82: 'R', + 83: 'S', + 84: 'T', + 85: 'U', + 86: 'V', + 87: 'W', + 88: 'X', + 89: 'Y', + 90: 'Z', + 91: '[', + 92: '\\\\', + 93: ']', + 94: '^', + 95: '_', + 96: '`', + 97: 'a', + 98: 'b', + 99: 'c', + 100: 'd', + 101: 'e', + 102: 'f', + 103: 'g', + 104: 'h', + 105: 'i', + 106: 'j', + 107: 'k', + 108: 'l', + 109: 'm', + 110: 'n', + 111: 'o', + 112: 'p', + 113: 'q', + 114: 'r', + 115: 's', + 116: 't', + 117: 'u', + 118: 'v', + 119: 'w', + 120: 'x', + 121: 'y', + 122: 'z', + 123: '{', + 124: '|', + 125: '}', + 126: '~', + 127: '\x7F', +}; // -------------------------------------------------------------------- // Exports @@ -59,7 +176,7 @@ export function defineLazyProperty(obj, propName, getterFn) { export function clone(obj) { if (obj) { - return Object.assign({}, obj); + return {...obj}; } return obj; } @@ -81,14 +198,14 @@ export function repeat(x, n) { } export function getDuplicates(array) { - const duplicates = []; + const duplicates = new Set(); for (let idx = 0; idx < array.length; idx++) { const x = array[idx]; - if (array.lastIndexOf(x) !== idx && duplicates.indexOf(x) < 0) { - duplicates.push(x); + if (array.lastIndexOf(x) !== idx && duplicates.has(x) < 0) { + duplicates.add(x); } } - return duplicates; + return [...duplicates]; } export function copyWithoutDuplicates(array) { diff --git a/packages/ohm-js/src/pexprs-eval.js b/packages/ohm-js/src/pexprs-eval.js index 6158cf23..f50b1cbb 100644 --- a/packages/ohm-js/src/pexprs-eval.js +++ b/packages/ohm-js/src/pexprs-eval.js @@ -222,7 +222,7 @@ pexprs.Apply.prototype.eval = function (state) { if (state.hasNecessaryInfo(memoRec)) { return state.useMemoizedResult(state.inputStream.pos, memoRec); } - delete posInfo.memo[memoKey]; + posInfo.memo[memoKey] = undefined; } return app.reallyEval(state); }; From e95070db2a39b4cb0f170111103260e0dc01c40d Mon Sep 17 00:00:00 2001 From: Siddharth Gelera Date: Tue, 3 Feb 2026 16:36:23 +0530 Subject: [PATCH 2/5] fix: existing behavioral tests --- packages/ohm-js/src/Semantics.js | 2 +- packages/ohm-js/src/common.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ohm-js/src/Semantics.js b/packages/ohm-js/src/Semantics.js index 3121db50..7eef1b65 100644 --- a/packages/ohm-js/src/Semantics.js +++ b/packages/ohm-js/src/Semantics.js @@ -39,7 +39,7 @@ class Wrapper { _forgetMemoizedResultFor(attributeName) { // Remove the memoized attribute from the cstNode and all its children. - this._node[this._semantics.attributeKeys[attributeName]] = undefined; + delete this._node[this._semantics.attributeKeys[attributeName]]; this.children.forEach(child => { child._forgetMemoizedResultFor(attributeName); }); diff --git a/packages/ohm-js/src/common.js b/packages/ohm-js/src/common.js index 5ca1616d..13924b34 100644 --- a/packages/ohm-js/src/common.js +++ b/packages/ohm-js/src/common.js @@ -201,7 +201,7 @@ export function getDuplicates(array) { const duplicates = new Set(); for (let idx = 0; idx < array.length; idx++) { const x = array[idx]; - if (array.lastIndexOf(x) !== idx && duplicates.has(x) < 0) { + if (array.lastIndexOf(x) !== idx && !duplicates.has(x)) { duplicates.add(x); } } From e101861a9f5430ef5249027e4df70c137b8bc747 Mon Sep 17 00:00:00 2001 From: Siddharth Gelera Date: Tue, 3 Feb 2026 18:33:18 +0530 Subject: [PATCH 3/5] chore: reduce mem alloc for UpperCase --- packages/ohm-js/src/common.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ohm-js/src/common.js b/packages/ohm-js/src/common.js index 13924b34..4faa9b29 100644 --- a/packages/ohm-js/src/common.js +++ b/packages/ohm-js/src/common.js @@ -219,8 +219,8 @@ export function copyWithoutDuplicates(array) { } export function isSyntactic(ruleName) { - const firstChar = ruleName[0]; - return firstChar === firstChar.toUpperCase(); + const code = ruleName.charCodeAt(0); + return code >= 65 && code <= 90; } export function isLexical(ruleName) { From 3df0a35275839507d9ac115b82abde50be06de6f Mon Sep 17 00:00:00 2001 From: Siddharth Gelera Date: Tue, 3 Feb 2026 19:01:01 +0530 Subject: [PATCH 4/5] fix: optimize replaceInputRange for better performance - Avoid unnecessary work when replacing input range with empty strings. - Improve memo table management during input updates. --- packages/ohm-js/src/Matcher.js | 39 ++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/ohm-js/src/Matcher.js b/packages/ohm-js/src/Matcher.js index 4cc14aa5..13693e39 100644 --- a/packages/ohm-js/src/Matcher.js +++ b/packages/ohm-js/src/Matcher.js @@ -29,6 +29,14 @@ export class Matcher { replaceInputRange(startIdx, endIdx, str) { const prevInput = this._input; const memoTable = this._memoTable; + + // Grammar.match() creates a fresh Matcher and calls replaceInputRange(0, 0, input). + // In that scenario, there is nothing to preserve or invalidate, so avoid extra work. + if (startIdx === 0 && endIdx === 0 && prevInput.length === 0 && memoTable.length === 0) { + this._input = '' + str; + memoTable.length = str.length; + return this; + } if ( startIdx < 0 || startIdx > prevInput.length || @@ -46,13 +54,32 @@ export class Matcher { } // update memo table (similar to the above) - const restOfMemoTable = memoTable.slice(endIdx); - memoTable.length = startIdx; - for (let idx = 0; idx < str.length; idx++) { - memoTable.push(undefined); + const oldLen = memoTable.length; + const deleteCount = endIdx - startIdx; + const insertCount = str.length; + const delta = insertCount - deleteCount; + const newLen = oldLen + delta; + + // Move the tail segment to its new location. + if (oldLen > endIdx) { + if (delta > 0) { + // Grow first to make room, then shift the tail right. + memoTable.length = newLen; + memoTable.copyWithin(startIdx + insertCount, endIdx, oldLen); + } else if (delta < 0) { + // Shift the tail left first, then shrink (shrinking first would drop tail entries). + memoTable.copyWithin(startIdx + insertCount, endIdx, oldLen); + memoTable.length = newLen; + } else { + memoTable.copyWithin(startIdx + insertCount, endIdx, oldLen); + } + } else if (delta !== 0) { + memoTable.length = newLen; } - for (const posInfo of restOfMemoTable) { - memoTable.push(posInfo); + + // Clear the inserted range. + if (insertCount > 0) { + memoTable.fill(undefined, startIdx, startIdx + insertCount); } // Invalidate memoRecs From 7e7e33e7a1e0827333408f2d6b0ee27fd49421cc Mon Sep 17 00:00:00 2001 From: Siddharth Gelera Date: Wed, 4 Feb 2026 12:00:10 +0530 Subject: [PATCH 5/5] refactor: manuall array allocation for `eval` hotpath --- packages/ohm-js/src/pexprs-eval.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/ohm-js/src/pexprs-eval.js b/packages/ohm-js/src/pexprs-eval.js index f50b1cbb..c832d088 100644 --- a/packages/ohm-js/src/pexprs-eval.js +++ b/packages/ohm-js/src/pexprs-eval.js @@ -133,12 +133,15 @@ pexprs.Iter.prototype.eval = function (state) { } prevPos = inputStream.pos; numMatches++; - const row = state._bindings.splice(state._bindings.length - arity, arity); - const rowOffsets = state._bindingOffsets.splice( - state._bindingOffsets.length - arity, - arity - ); - for (idx = 0; idx < row.length; idx++) { + // Avoid using `splice` which allocates an array; use repeated `pop` which is faster + // in hot loops and preserves insertion order by filling from the end. + const row = new Array(arity); + const rowOffsets = new Array(arity); + for (let j = arity - 1; j >= 0; j--) { + row[j] = state._bindings.pop(); + rowOffsets[j] = state._bindingOffsets.pop(); + } + for (idx = 0; idx < arity; idx++) { cols[idx].push(row[idx]); colOffsets[idx].push(rowOffsets[idx]); } @@ -336,8 +339,13 @@ pexprs.Apply.prototype.evalOnce = function (expr, state) { if (state.eval(expr)) { const arity = expr.getArity(); - const bindings = state._bindings.splice(state._bindings.length - arity, arity); - const offsets = state._bindingOffsets.splice(state._bindingOffsets.length - arity, arity); + // Avoid Array.prototype.splice allocation in hot path; use pop into preallocated arrays + const bindings = new Array(arity); + const offsets = new Array(arity); + for (let j = arity - 1; j >= 0; j--) { + bindings[j] = state._bindings.pop(); + offsets[j] = state._bindingOffsets.pop(); + } const matchLength = inputStream.pos - origPos; return new NonterminalNode(this.ruleName, bindings, offsets, matchLength); } else {