[acorn-opt] Add inlineConstants pass to inline primitive JS constants

Inlines immutable primitive constants (numbers, booleans, and bigints)
defined via `const` into their expression uses in
`tools/acorn-optimizer.mjs`.

When combined with `JSDCE`, this allows constants defined in JS library
code (such as pthread/worker message command IDs and socket states) to
be inlined and their declarations eliminated in builds that do not run
Closure Compiler.

Also add unit tests in `test/test_other.py`.
diff --git a/test/js_optimizer/inlineConstants-dce-output.js b/test/js_optimizer/inlineConstants-dce-output.js
new file mode 100644
index 0000000..7003858
--- /dev/null
+++ b/test/js_optimizer/inlineConstants-dce-output.js
@@ -0,0 +1,11 @@
+export const EXPORTED = 3;
+
+function handle(cmd) {
+  if (cmd === 1) return 10;
+  if (cmd === 2) return 20;
+  return 0;
+}
+
+export function run(cmd) {
+  return handle(cmd) + 3;
+}
diff --git a/test/js_optimizer/inlineConstants-dce.js b/test/js_optimizer/inlineConstants-dce.js
new file mode 100644
index 0000000..fea9bca
--- /dev/null
+++ b/test/js_optimizer/inlineConstants-dce.js
@@ -0,0 +1,14 @@
+const CMD_LOAD = 1;
+const CMD_RUN = 2;
+const UNUSED = 99;
+export const EXPORTED = 3;
+
+function handle(cmd) {
+  if (cmd === CMD_LOAD) return 10;
+  if (cmd === CMD_RUN) return 20;
+  return 0;
+}
+
+export function run(cmd) {
+  return handle(cmd) + EXPORTED;
+}
diff --git a/test/js_optimizer/inlineConstants-output.js b/test/js_optimizer/inlineConstants-output.js
new file mode 100644
index 0000000..abdaefd
--- /dev/null
+++ b/test/js_optimizer/inlineConstants-output.js
@@ -0,0 +1,76 @@
+const NUM = 42;
+
+const FLOAT = 3.14;
+
+const BOOL_T = true;
+
+const BOOL_F = false;
+
+const NEG = -1;
+
+const BIG = 100n;
+
+const NEG_BIG = -1n;
+
+const ALIAS = 42;
+
+const MULTI_A = 1, MULTI_B = 2;
+
+const UNUSED = 999;
+
+export const EXPORTED_CONST = 77;
+
+function testExpressions(cmd) {
+  if (cmd === 42) return true;
+  if (cmd < 3.14) return false;
+  if (cmd === -1) return -1n;
+  switch (cmd) {
+   case 1:
+    return 10;
+
+   case 2:
+    return 20;
+  }
+  return 42 + 100n;
+}
+
+const obj = {
+  NUM: 42,
+  prop: 3.14,
+  [-1]: "computed",
+  NUM: "uncomputed key should not be changed"
+};
+
+function testMember(x) {
+  var a = x[42];
+  var b = x.NUM;
+  return a + b;
+}
+
+function testParamShadow(NUM) {
+  return NUM;
+}
+
+function testLocalConstShadow() {
+  const NUM = 100;
+  return 100;
+}
+
+function testLocalVarShadow() {
+  if (true) {
+    var NUM = 200;
+  }
+  return NUM;
+}
+
+function testCatchShadow() {
+  try {
+    throw 1;
+  } catch (NUM) {
+    return NUM;
+  }
+}
+
+const arrow = x => x + 42;
+
+export { NUM };
diff --git a/test/js_optimizer/inlineConstants.js b/test/js_optimizer/inlineConstants.js
new file mode 100644
index 0000000..88df070
--- /dev/null
+++ b/test/js_optimizer/inlineConstants.js
@@ -0,0 +1,73 @@
+// Constants of various primitive types
+const NUM = 42;
+const FLOAT = 3.14;
+const BOOL_T = true;
+const BOOL_F = false;
+const NEG = -1;
+const BIG = 100n;
+const NEG_BIG = -1n;
+const ALIAS = NUM;
+const MULTI_A = 1, MULTI_B = 2;
+const UNUSED = 999;
+export const EXPORTED_CONST = 77;
+
+// Uses in expressions
+function testExpressions(cmd) {
+  if (cmd === NUM) return BOOL_T;
+  if (cmd < FLOAT) return BOOL_F;
+  if (cmd === NEG) return NEG_BIG;
+  switch (cmd) {
+    case MULTI_A: return 10;
+    case MULTI_B: return 20;
+  }
+  return ALIAS + BIG;
+}
+
+// Uses in objects
+const obj = {
+  NUM,
+  prop: FLOAT,
+  [NEG]: "computed",
+  NUM: "uncomputed key should not be changed"
+};
+
+// Uses in member expressions
+function testMember(x) {
+  var a = x[NUM];
+  var b = x.NUM;
+  return a + b;
+}
+
+// Shadowing: parameter shadows outer const
+function testParamShadow(NUM) {
+  return NUM;
+}
+
+// Shadowing: local const shadows outer const
+function testLocalConstShadow() {
+  const NUM = 100;
+  return NUM;
+}
+
+// Shadowing: local var shadows outer const
+function testLocalVarShadow() {
+  if (true) {
+    var NUM = 200;
+  }
+  return NUM;
+}
+
+// Shadowing: catch parameter shadows outer const
+function testCatchShadow() {
+  try {
+    throw 1;
+  } catch (NUM) {
+    return NUM;
+  }
+}
+
+// Arrow function with concise body
+const arrow = (x) => x + NUM;
+
+// Export specifier
+export { NUM };
diff --git a/test/test_other.py b/test/test_other.py
index 5ad2542..48bfc73 100644
--- a/test/test_other.py
+++ b/test/test_other.py
@@ -3121,6 +3121,8 @@
   @parameterized({
     'minifyGlobals': (['minifyGlobals'],),
     'minifyLocals': (['minifyLocals'],),
+    'inlineConstants': (['inlineConstants', '--export-es6'],),
+    'inlineConstants-dce': (['inlineConstants', 'JSDCE', '--export-es6'], 'inlineConstants-dce.js'),
     'JSDCE': (['JSDCE', '--export-es6'],),
     'JSDCE-sourcePhaseImports': (['JSDCE', '--export-es6'],),
     'JSDCE-hasOwnProperty': (['JSDCE'],),
diff --git a/tools/acorn-optimizer.mjs b/tools/acorn-optimizer.mjs
index feefd73..7db8e5e 100755
--- a/tools/acorn-optimizer.mjs
+++ b/tools/acorn-optimizer.mjs
@@ -428,6 +428,324 @@
     }
   });
 }
+// Inlines immutable primitive constants (numbers, booleans, bigints) defined via
+// `const` into their expression uses. This allows constants defined in library code
+// (such as pthread/worker command IDs, socket states, etc.) to be inlined and then
+// eliminated by JSDCE in builds that do not run Closure Compiler.
+function inlineConstants(ast) {
+  function isInlinableConstant(node) {
+    if (!node) return false;
+    if (node.type === 'Literal') {
+      const val = node.value;
+      return (
+        (typeof val === 'number' && Number.isFinite(val)) ||
+        typeof val === 'boolean' ||
+        typeof val === 'bigint'
+      );
+    }
+    if (
+      node.type === 'UnaryExpression' &&
+      node.prefix === true &&
+      (node.operator === '-' || node.operator === '+') &&
+      node.argument.type === 'Literal'
+    ) {
+      const val = node.argument.value;
+      return (typeof val === 'number' && Number.isFinite(val)) || typeof val === 'bigint';
+    }
+    return false;
+  }
+
+  function cloneConstant(node) {
+    if (node.type === 'Literal') {
+      return {
+        type: 'Literal',
+        value: node.value,
+        raw: node.raw,
+        bigint: node.bigint,
+      };
+    }
+    if (node.type === 'UnaryExpression') {
+      return {
+        type: 'UnaryExpression',
+        operator: node.operator,
+        prefix: true,
+        argument: cloneConstant(node.argument),
+      };
+    }
+    throw new Error('unexpected constant node type: ' + node.type);
+  }
+
+  function replaceIdentifier(node, val) {
+    const replacement = cloneConstant(val);
+    for (const k of Object.keys(node)) {
+      delete node[k];
+    }
+    Object.assign(node, replacement);
+  }
+
+  class Scope {
+    constructor(isFunctionOrProgram = false) {
+      this.isFunctionOrProgram = isFunctionOrProgram;
+      this.bindings = new Map();
+    }
+  }
+
+  const scopes = [];
+
+  function addVarBinding(name) {
+    for (let i = scopes.length - 1; i >= 0; i--) {
+      if (scopes[i].isFunctionOrProgram) {
+        scopes[i].bindings.set(name, null);
+        return;
+      }
+    }
+    scopes[0].bindings.set(name, null);
+  }
+
+  function lookup(name) {
+    for (let i = scopes.length - 1; i >= 0; i--) {
+      if (scopes[i].bindings.has(name)) {
+        return scopes[i].bindings.get(name);
+      }
+    }
+    return null;
+  }
+
+  function collectDeclarations(bodyList, scope) {
+    if (!Array.isArray(bodyList)) return;
+    const pendingConsts = [];
+
+    for (const stmt of bodyList) {
+      let declNode = null;
+      if (stmt.type === 'VariableDeclaration') {
+        declNode = stmt;
+      } else if (stmt.type === 'ExportNamedDeclaration' && stmt.declaration?.type === 'VariableDeclaration') {
+        declNode = stmt.declaration;
+      } else if (stmt.type === 'FunctionDeclaration') {
+        if (stmt.id) scope.bindings.set(stmt.id.name, null);
+      } else if (stmt.type === 'ExportNamedDeclaration' && stmt.declaration?.type === 'FunctionDeclaration') {
+        if (stmt.declaration.id) scope.bindings.set(stmt.declaration.id.name, null);
+      } else if (stmt.type === 'ClassDeclaration') {
+        if (stmt.id) scope.bindings.set(stmt.id.name, null);
+      } else if (stmt.type === 'ExportNamedDeclaration' && stmt.declaration?.type === 'ClassDeclaration') {
+        if (stmt.declaration.id) scope.bindings.set(stmt.declaration.id.name, null);
+      }
+
+      if (declNode) {
+        if (declNode.kind === 'const') {
+          for (const decl of declNode.declarations) {
+            if (decl.id.type === 'Identifier') {
+              if (isInlinableConstant(decl.init)) {
+                scope.bindings.set(decl.id.name, cloneConstant(decl.init));
+              } else if (decl.init?.type === 'Identifier') {
+                pendingConsts.push({ name: decl.id.name, initName: decl.init.name });
+              } else {
+                scope.bindings.set(decl.id.name, null);
+              }
+            } else {
+              walkPattern(decl.id, () => {}, (name) => scope.bindings.set(name, null));
+            }
+          }
+        } else if (declNode.kind === 'let') {
+          for (const decl of declNode.declarations) {
+            walkPattern(decl.id, () => {}, (name) => scope.bindings.set(name, null));
+          }
+        } else if (declNode.kind === 'var') {
+          for (const decl of declNode.declarations) {
+            walkPattern(decl.id, () => {}, (name) => addVarBinding(name));
+          }
+        }
+      }
+    }
+
+    // Resolve any const aliases: const A = B;
+    let progress = true;
+    while (progress && pendingConsts.length > 0) {
+      progress = false;
+      for (let i = pendingConsts.length - 1; i >= 0; i--) {
+        const { name, initName } = pendingConsts[i];
+        const val = lookup(initName) || scope.bindings.get(initName);
+        if (val) {
+          scope.bindings.set(name, cloneConstant(val));
+          pendingConsts.splice(i, 1);
+          progress = true;
+        }
+      }
+    }
+    for (const { name } of pendingConsts) {
+      scope.bindings.set(name, null);
+    }
+  }
+
+  function handleFunction(node, c) {
+    const scope = new Scope(true /* isFunctionOrProgram */);
+    scopes.push(scope);
+    if (node.type === 'FunctionExpression' && node.id) {
+      scope.bindings.set(node.id.name, null);
+    }
+    for (const param of node.params) {
+      walkPattern(param, (expr) => c(expr), (name) => scope.bindings.set(name, null));
+    }
+    c(node.body);
+    scopes.pop();
+  }
+
+  const rootScope = new Scope(true /* isFunctionOrProgram */);
+  scopes.push(rootScope);
+  collectDeclarations(ast.body, rootScope);
+
+  recursiveWalk(ast, {
+    BlockStatement(node, c) {
+      const scope = new Scope(false);
+      scopes.push(scope);
+      collectDeclarations(node.body, scope);
+      for (const stmt of node.body) {
+        c(stmt);
+      }
+      scopes.pop();
+    },
+    FunctionDeclaration(node, c) {
+      handleFunction(node, c);
+    },
+    FunctionExpression(node, c) {
+      handleFunction(node, c);
+    },
+    ArrowFunctionExpression(node, c) {
+      handleFunction(node, c);
+    },
+    ForStatement(node, c) {
+      const scope = new Scope(false);
+      scopes.push(scope);
+      if (node.init?.type === 'VariableDeclaration') {
+        if (node.init.kind === 'var') {
+          for (const decl of node.init.declarations) {
+            walkPattern(decl.id, () => {}, (name) => addVarBinding(name));
+          }
+        } else {
+          for (const decl of node.init.declarations) {
+            walkPattern(decl.id, () => {}, (name) => scope.bindings.set(name, null));
+          }
+        }
+      }
+      visitChildren(node, c);
+      scopes.pop();
+    },
+    ForInStatement(node, c) {
+      const scope = new Scope(false);
+      scopes.push(scope);
+      if (node.left.type === 'VariableDeclaration') {
+        if (node.left.kind === 'var') {
+          for (const decl of node.left.declarations) {
+            walkPattern(decl.id, () => {}, (name) => addVarBinding(name));
+          }
+        } else {
+          for (const decl of node.left.declarations) {
+            walkPattern(decl.id, () => {}, (name) => scope.bindings.set(name, null));
+          }
+        }
+      }
+      visitChildren(node, c);
+      scopes.pop();
+    },
+    ForOfStatement(node, c) {
+      const scope = new Scope(false);
+      scopes.push(scope);
+      if (node.left.type === 'VariableDeclaration') {
+        if (node.left.kind === 'var') {
+          for (const decl of node.left.declarations) {
+            walkPattern(decl.id, () => {}, (name) => addVarBinding(name));
+          }
+        } else {
+          for (const decl of node.left.declarations) {
+            walkPattern(decl.id, () => {}, (name) => scope.bindings.set(name, null));
+          }
+        }
+      }
+      visitChildren(node, c);
+      scopes.pop();
+    },
+    CatchClause(node, c) {
+      const scope = new Scope(false);
+      scopes.push(scope);
+      if (node.param) {
+        walkPattern(node.param, () => {}, (name) => scope.bindings.set(name, null));
+      }
+      c(node.body);
+      scopes.pop();
+    },
+    VariableDeclarator(node, c) {
+      if (node.init) c(node.init);
+    },
+    ExportNamedDeclaration(node, c) {
+      if (node.declaration) c(node.declaration);
+    },
+    ExportDefaultDeclaration(node, c) {
+      c(node.declaration);
+    },
+    ExportSpecifier() {},
+    ImportDeclaration() {},
+    MemberExpression(node, c) {
+      c(node.object);
+      if (node.computed) c(node.property);
+    },
+    ObjectExpression(node, c) {
+      for (const prop of node.properties) {
+        if (prop.type === 'Property') {
+          if (prop.computed) c(prop.key);
+          if (prop.value) {
+            if (prop.shorthand && prop.value.type === 'Identifier') {
+              const val = lookup(prop.value.name);
+              if (val) {
+                prop.shorthand = false;
+                replaceIdentifier(prop.value, val);
+                continue;
+              }
+            }
+            c(prop.value);
+          }
+        } else if (prop.type === 'SpreadElement') {
+          c(prop.argument);
+        }
+      }
+    },
+    AssignmentExpression(node, c) {
+      if (node.left.type !== 'Identifier') c(node.left);
+      c(node.right);
+    },
+    UpdateExpression(node, c) {
+      if (node.argument.type !== 'Identifier') c(node.argument);
+    },
+    ClassDeclaration(node, c) {
+      if (node.superClass) c(node.superClass);
+      c(node.body);
+    },
+    ClassExpression(node, c) {
+      if (node.superClass) c(node.superClass);
+      c(node.body);
+    },
+    MethodDefinition(node, c) {
+      if (node.computed) c(node.key);
+      c(node.value);
+    },
+    PropertyDefinition(node, c) {
+      if (node.computed) c(node.key);
+      if (node.value) c(node.value);
+    },
+    BreakStatement() {},
+    ContinueStatement() {},
+    LabeledStatement(node, c) {
+      c(node.body);
+    },
+    Identifier(node, _c) {
+      const val = lookup(node.name);
+      if (val) {
+        replaceIdentifier(node, val);
+      }
+    },
+  });
+
+  scopes.pop();
+}
 
 function isWasmImportsAssign(node) {
   // var wasmImports = ..
@@ -1877,6 +2195,7 @@
   JSDCE,
   AJSDCE,
   stripDefaultUndefined,
+  inlineConstants,
   applyImportAndExportNameChanges,
   emitDCEGraph,
   applyDCEGraphRemovals,
diff --git a/tools/building.py b/tools/building.py
index 8cb2b78..e1e0878 100644
--- a/tools/building.py
+++ b/tools/building.py
@@ -790,7 +790,7 @@
   # start with JSDCE, to clean up obvious JS garbage. When optimizing for size,
   # use AJSDCE (aggressive JS DCE, performs multiple iterations). Clean up
   # whitespace if necessary too.
-  passes = []
+  passes = ['inlineConstants']
   if not settings.LINKABLE:
     passes.append('JSDCE' if not expensive_optimizations else 'AJSDCE')
   if not settings.USE_CLOSURE_COMPILER:
diff --git a/tools/js_optimizer.py b/tools/js_optimizer.py
index 9f0cb8a..8b01950 100755
--- a/tools/js_optimizer.py
+++ b/tools/js_optimizer.py
@@ -286,7 +286,7 @@
         elif cleanup:
           if DEBUG:
             print('running cleanup on shell code', file=sys.stderr)
-          acorn_passes = ['JSDCE']
+          acorn_passes = ['inlineConstants', 'JSDCE']
           if '--minify-whitespace' in passes:
             acorn_passes.append('--minify-whitespace')
           cld = building.acorn_optimizer(cld, acorn_passes)