From 5b97109e8611fd532feca840c9d6dbdf134e86bd Mon Sep 17 00:00:00 2001 From: Greg Brail Date: Sat, 7 Feb 2026 13:22:50 -0800 Subject: [PATCH 01/14] Update version for 1.9.1 development. --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index cbbbbaac99..35e1b4114f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ rootProject.name=rhino-root group=org.mozilla -version=1.9.0 +version=1.9.1-SNAPSHOT mavenReleaseRepo=https://oss.sonatype.org/service/local/staging/deploy/maven2/ mavenSnapshotRepo=https://oss.sonatype.org/content/repositories/snapshots githubPackagesRepo=https://maven.pkg.github.com/mozilla/rhino From 9fed38bccf3037a215262a7c46112b54b86c65b6 Mon Sep 17 00:00:00 2001 From: Duncan MacGregor Date: Sat, 27 Dec 2025 01:20:13 +0000 Subject: [PATCH 02/14] Ensure `global` is present in top level * Set `global` as part of initialisation. * Add a test that `global` and `globalThis` are the same thing. --- .../main/java/org/mozilla/javascript/ScriptRuntime.java | 2 ++ .../java/org/mozilla/javascript/tests/TopLevelTest.java | 7 +++++++ tests/testsrc/jstests/top-level.js | 7 +++++++ 3 files changed, 16 insertions(+) create mode 100644 tests/src/test/java/org/mozilla/javascript/tests/TopLevelTest.java create mode 100644 tests/testsrc/jstests/top-level.js diff --git a/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java b/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java index 8ee7837ce5..7eefee67e8 100644 --- a/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java +++ b/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java @@ -190,6 +190,8 @@ public static ScriptableObject initSafeStandardObjects( ((TopLevel) scope).clearCache(); } + scope.put("global", scope, scope); + scope.associateValue(LIBRARY_SCOPE_KEY, scope); new ClassCache().associate(scope); new ConcurrentFactory().associate(scope); diff --git a/tests/src/test/java/org/mozilla/javascript/tests/TopLevelTest.java b/tests/src/test/java/org/mozilla/javascript/tests/TopLevelTest.java new file mode 100644 index 0000000000..0e552fa633 --- /dev/null +++ b/tests/src/test/java/org/mozilla/javascript/tests/TopLevelTest.java @@ -0,0 +1,7 @@ +package org.mozilla.javascript.tests; + +import org.mozilla.javascript.drivers.RhinoTest; +import org.mozilla.javascript.drivers.ScriptTestsBase; + +@RhinoTest(value = "testsrc/jstests/top-level.js") +public class TopLevelTest extends ScriptTestsBase {} diff --git a/tests/testsrc/jstests/top-level.js b/tests/testsrc/jstests/top-level.js new file mode 100644 index 0000000000..65e83ecc89 --- /dev/null +++ b/tests/testsrc/jstests/top-level.js @@ -0,0 +1,7 @@ +'use strict'; + +load("testsrc/assert.js"); + +assertSame(global, globalThis); + +"success"; From 936745a3769a6e0dc778de72b158d38612c4130f Mon Sep 17 00:00:00 2001 From: "duncan.macgregor" Date: Wed, 24 Dec 2025 15:27:37 +0000 Subject: [PATCH 03/14] Avoid illegal characters in compiled method names. --- .../java/org/mozilla/javascript/optimizer/Codegen.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/rhino/src/main/java/org/mozilla/javascript/optimizer/Codegen.java b/rhino/src/main/java/org/mozilla/javascript/optimizer/Codegen.java index f04e080aaa..cfb1612271 100644 --- a/rhino/src/main/java/org/mozilla/javascript/optimizer/Codegen.java +++ b/rhino/src/main/java/org/mozilla/javascript/optimizer/Codegen.java @@ -17,6 +17,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; import org.mozilla.classfile.ByteCode; import org.mozilla.classfile.ClassFileWriter; import org.mozilla.javascript.CodeGenUtils; @@ -860,6 +861,12 @@ String getBodyMethodName(ScriptNode n, int index) { return "_c_" + cleanName(n) + "_" + index; } + /** + * List of illegal characters in unqualified names as specified in + * https://docs.oracle.com/javase/specs/jvms/se25/html/jvms-4.html#jvms-4.2.2 + */ + private static Pattern illegalChars = Pattern.compile("[.;\\[/<>]"); + /** Gets a Java-compatible "informative" name for the ScriptOrFnNode */ String cleanName(final ScriptNode n) { String result = ""; @@ -873,7 +880,7 @@ String cleanName(final ScriptNode n) { } else { result = "script"; } - return result; + return illegalChars.matcher(result).replaceAll("_"); } String getNonDirectBodyMethodSIgnature(ScriptNode n) { From f7ed7f7a33c3b5e673bf10a1c1499303053c19d4 Mon Sep 17 00:00:00 2001 From: Nicolas Albert Date: Tue, 20 Jan 2026 06:44:50 +0100 Subject: [PATCH 04/14] Fix debugger eval for Script * Fix debugger eval for Script * Fix debugger eval scope for top-level scripts --- .../javascript/tools/debugger/Dim.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/rhino-tools/src/main/java/org/mozilla/javascript/tools/debugger/Dim.java b/rhino-tools/src/main/java/org/mozilla/javascript/tools/debugger/Dim.java index 143ba67e69..9af01cc080 100644 --- a/rhino-tools/src/main/java/org/mozilla/javascript/tools/debugger/Dim.java +++ b/rhino-tools/src/main/java/org/mozilla/javascript/tools/debugger/Dim.java @@ -17,7 +17,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import org.mozilla.javascript.Callable; import org.mozilla.javascript.Context; import org.mozilla.javascript.ContextAction; import org.mozilla.javascript.ContextFactory; @@ -25,6 +24,7 @@ import org.mozilla.javascript.Kit; import org.mozilla.javascript.NativeCall; import org.mozilla.javascript.NativeObject; +import org.mozilla.javascript.Script; import org.mozilla.javascript.ScriptRuntime; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; @@ -745,8 +745,15 @@ private static String do_eval(Context cx, StackFrame frame, String expr) { cx.setInterpretedMode(false); cx.setGeneratingDebug(false); try { - Callable script = (Callable) cx.compileString(expr, "", 0, null); - Object result = script.call(cx, frame.scope, frame.thisObj, ScriptRuntime.emptyArgs); + Scriptable scope = frame.scope; + if (!frame.isFunction && scope != null) { + Scriptable parentScope = scope.getParentScope(); + if (parentScope != null) { + scope = parentScope; + } + } + Script script = cx.compileString(expr, "", 0, null); + Object result = script.exec(cx, scope, frame.thisObj); if (result == Undefined.instance) { resultString = ""; } else { @@ -896,7 +903,7 @@ public DebugFrame getFrame(Context cx, DebuggableScript fnOrScript) { // Can not debug if source is not available return null; } - return new StackFrame(cx, dim, item); + return new StackFrame(cx, dim, item, fnOrScript.isFunction()); } /** Called when compilation is finished. */ @@ -974,6 +981,9 @@ public static class StackFrame implements DebugFrame { /** The 'this' object. */ private Scriptable thisObj; + /** Whether this frame represents a function (vs a top-level script). */ + private boolean isFunction; + /** Information about the function. */ private FunctionSource fsource; @@ -984,10 +994,11 @@ public static class StackFrame implements DebugFrame { private int lineNumber; /** Creates a new StackFrame. */ - private StackFrame(Context cx, Dim dim, FunctionSource fsource) { + private StackFrame(Context cx, Dim dim, FunctionSource fsource, boolean isFunction) { this.dim = dim; this.contextData = ContextData.get(cx); this.fsource = fsource; + this.isFunction = isFunction; this.breakpoints = fsource.sourceInfo().breakpoints; this.lineNumber = fsource.firstLine(); } From 591b4e88dd8ae4eb30c8206bed338813149b06e8 Mon Sep 17 00:00:00 2001 From: "duncan.macgregor" Date: Mon, 19 Jan 2026 12:06:13 +0000 Subject: [PATCH 05/14] Support reserved words in XML attribute names. --- .../java/org/mozilla/javascript/Parser.java | 39 +++++++++++++++++-- .../tests/XMLReservedWordsAttributesTest.java | 10 +++++ .../xml-reserved-words-as-attributes.js | 23 +++++++++++ 3 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 tests/src/test/java/org/mozilla/javascript/tests/XMLReservedWordsAttributesTest.java create mode 100644 tests/testsrc/jstests/xml-reserved-words-as-attributes.js diff --git a/rhino/src/main/java/org/mozilla/javascript/Parser.java b/rhino/src/main/java/org/mozilla/javascript/Parser.java index d9b29d441f..de1de39b11 100644 --- a/rhino/src/main/java/org/mozilla/javascript/Parser.java +++ b/rhino/src/main/java/org/mozilla/javascript/Parser.java @@ -3269,7 +3269,10 @@ private AstNode attributeAccess() throws IOException { // handles: @name, @ns::name, @ns::*, @ns::[expr] case Token.NAME: return propertyName(atPos, 0); - + case Token.RESERVED: + String name = ts.getString(); + saveNameTokenData(ts.tokenBeg, name, lineNumber(), columnNumber()); + return propertyName(atPos, 0); // handles: @*, @*::name, @*::*, @*::[expr] case Token.MUL: saveNameTokenData(ts.tokenBeg, "*", lineNumber(), columnNumber()); @@ -3280,6 +3283,16 @@ private AstNode attributeAccess() throws IOException { return xmlElemRef(atPos, null, -1); default: + { + if (compilerEnv.isReservedKeywordAsIdentifier()) { + // allow keywords as property names, e.g. ({if: 1}) + name = Token.keywordToName(tt); + if (name != null) { + saveNameTokenData(ts.tokenBeg, name, lineNumber(), columnNumber()); + return propertyName(atPos, 0); + } + } + } reportError("msg.no.name.after.xmlAttr"); return makeErrorNode(); } @@ -3304,13 +3317,19 @@ private AstNode propertyName(int atPos, int memberTypeFlags) throws IOException ns = name; colonPos = ts.tokenBeg; - switch (nextToken()) { + int nt = nextToken(); + switch (nt) { // handles name::name case Token.NAME: name = createNameNode(); break; - - // handles name::* + case Token.RESERVED: + { + String realName = ts.getString(); + saveNameTokenData(ts.tokenBeg, realName, lineNumber(), columnNumber()); + name = createNameNode(false, -1); + break; + } case Token.MUL: saveNameTokenData(ts.tokenBeg, "*", lineNumber(), columnNumber()); name = createNameNode(false, -1); @@ -3321,6 +3340,18 @@ private AstNode propertyName(int atPos, int memberTypeFlags) throws IOException return xmlElemRef(atPos, ns, colonPos); default: + { + if (compilerEnv.isReservedKeywordAsIdentifier()) { + // allow keywords as property names, e.g. ({if: 1}) + String realName = Token.keywordToName(nt); + if (name != null) { + saveNameTokenData( + ts.tokenBeg, realName, lineNumber(), columnNumber()); + name = createNameNode(false, -1); + break; + } + } + } reportError("msg.no.name.after.coloncolon"); return makeErrorNode(); } diff --git a/tests/src/test/java/org/mozilla/javascript/tests/XMLReservedWordsAttributesTest.java b/tests/src/test/java/org/mozilla/javascript/tests/XMLReservedWordsAttributesTest.java new file mode 100644 index 0000000000..07be747588 --- /dev/null +++ b/tests/src/test/java/org/mozilla/javascript/tests/XMLReservedWordsAttributesTest.java @@ -0,0 +1,10 @@ +package org.mozilla.javascript.tests; + +import org.mozilla.javascript.Context; +import org.mozilla.javascript.drivers.LanguageVersion; +import org.mozilla.javascript.drivers.RhinoTest; +import org.mozilla.javascript.drivers.ScriptTestsBase; + +@RhinoTest("testsrc/jstests/xml-reserved-words-as-attributes.js") +@LanguageVersion(Context.VERSION_ES6) +public class XMLReservedWordsAttributesTest extends ScriptTestsBase {} diff --git a/tests/testsrc/jstests/xml-reserved-words-as-attributes.js b/tests/testsrc/jstests/xml-reserved-words-as-attributes.js new file mode 100644 index 0000000000..887d7db8f8 --- /dev/null +++ b/tests/testsrc/jstests/xml-reserved-words-as-attributes.js @@ -0,0 +1,23 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Test that reserved words can be used as both XML attribute names +// and as part of a name space qualified name. + +load('testsrc/assert.js'); + +x = + + + + + + +assertEquals("value1", x.bravo.@for.toXMLString()); +assertEquals("value2", x.bravo.charlie.@class.toXMLString()); +n = new Namespace("http://someuri"); +assertEquals("value3", x.bravo.@n::for.toXMLString()); +assertEquals("value4", x.bravo.charlie.@n::class.toXMLString()); + +"success" From 29aba18a22b965f8252f5c2fcf76c7dad992caa4 Mon Sep 17 00:00:00 2001 From: Umesh Gupta Date: Sat, 31 Jan 2026 22:46:30 +0530 Subject: [PATCH 06/14] docs: fix typo in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a6b28445a..52e09e25b4 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ this using the command: ./gradlew -q javaToolchains Not all installers seem to put JDKs in the places where Gradle can find them. When in doubt, -installatioons from [Adoptium](https://adoptium.net) seem to work on most platforms. +installations from [Adoptium](https://adoptium.net) seem to work on most platforms. ### Testing on Android From b6675dce951532ebc5dfb81e17033a8e390ffde0 Mon Sep 17 00:00:00 2001 From: Balaji Rao Date: Tue, 6 Jan 2026 17:09:58 +0100 Subject: [PATCH 07/14] Introduce AbstractEcmaStringOperations.ReplacementOperation This is to avoid repeatedly parsing the replacement template in case of multiple matches. --- .../AbstractEcmaStringOperations.java | 316 ++++++++++++++---- .../org/mozilla/javascript/NativeString.java | 24 +- .../javascript/regexp/NativeRegExp.java | 14 +- 3 files changed, 275 insertions(+), 79 deletions(-) diff --git a/rhino/src/main/java/org/mozilla/javascript/AbstractEcmaStringOperations.java b/rhino/src/main/java/org/mozilla/javascript/AbstractEcmaStringOperations.java index 98f23c749c..cc7751d404 100644 --- a/rhino/src/main/java/org/mozilla/javascript/AbstractEcmaStringOperations.java +++ b/rhino/src/main/java/org/mozilla/javascript/AbstractEcmaStringOperations.java @@ -1,57 +1,46 @@ package org.mozilla.javascript; +import java.util.ArrayList; +import java.util.List; + /** Abstract operations for string manipulation as defined by EcmaScript */ public class AbstractEcmaStringOperations { - /** - * GetSubstitution(matched, str, position, captures, namedCaptures, replacementTemplate) - * - *

22.1.3.19.1 - * GetSubstitution (matched, str, position, captures, namedCaptures, replacementTemplate) - */ - public static String getSubstitution( - Context cx, - Scriptable scope, - String matched, - String str, - int position, - NativeArray capturesArray, - Object namedCaptures, - String replacementTemplate) { - // See ECMAScript spec 22.1.3.19.1 - int stringLength = str.length(); - if (position > stringLength) Kit.codeBug(); - StringBuilder result = new StringBuilder(); - String templateRemainder = replacementTemplate; - while (!templateRemainder.isEmpty()) { - String ref = templateRemainder.substring(0, 1); - String refReplacement = ref; - - if (templateRemainder.charAt(0) == '$') { - if (templateRemainder.length() > 1) { - char c = templateRemainder.charAt(1); + + public static List buildReplacementList(String replacementTemplate) { + List ops = new ArrayList<>(); + int position = 0; + int start = 0; + + while (position < replacementTemplate.length()) { + if (replacementTemplate.charAt(position) == '$') { + if (position < (replacementTemplate.length() - 1)) { + if (start != position) { + ops.add( + new LiteralReplacement( + replacementTemplate.substring(start, position))); + } + String ref = replacementTemplate.substring(position, position + 1); + char c = replacementTemplate.charAt(position + 1); switch (c) { case '$': ref = "$$"; - refReplacement = "$"; + ops.add(new LiteralReplacement("$")); break; case '`': ref = "$`"; - refReplacement = str.substring(0, position); + ops.add(new FromStartToMatchReplacement()); break; case '&': ref = "$&"; - refReplacement = matched; + ops.add(new MatchedReplacement()); break; case '\'': { ref = "$'"; - int matchLength = matched.length(); - int tailPos = position + matchLength; - refReplacement = str.substring(Math.min(tailPos, stringLength)); + ops.add(new FromMatchToEndReplacement()); break; } @@ -67,66 +56,78 @@ public static String getSubstitution( case '9': { int digitCount = 1; - if (templateRemainder.length() > 2) { - char c2 = templateRemainder.charAt(2); + if (replacementTemplate.length() > position + 2) { + char c2 = replacementTemplate.charAt(position + 2); if (isAsciiDigit(c2)) { digitCount = 2; } } - String digits = templateRemainder.substring(1, 1 + digitCount); + String digits = + replacementTemplate.substring( + position + 1, position + 1 + digitCount); + ref = + replacementTemplate.substring( + position, position + 1 + digitCount); // No need for ScriptRuntime version; we know the string is one or // two characters and // contains only [0-9] int index = Integer.parseInt(digits); - long captureLen = capturesArray.getLength(); - if (index > captureLen && digitCount == 2) { - digitCount = 1; - digits = digits.substring(0, 1); - index = Integer.parseInt(digits); - } - ref = templateRemainder.substring(0, 1 + digitCount); - if (1 <= index && index <= captureLen) { - Object capture = capturesArray.get(index - 1); - if (capture - == null) { // Undefined or missing are returned as null - refReplacement = ""; - } else { - refReplacement = ScriptRuntime.toString(capture); - } + if (digits.length() == 1) { + ops.add(new OneDigitCaptureReplacement(index)); } else { - refReplacement = ref; + ops.add(new TwoDigitCaptureReplacement(index)); } break; } case '<': { - int gtPos = templateRemainder.indexOf('>'); - if (gtPos == -1 || Undefined.isUndefined(namedCaptures)) { + int gtPos = replacementTemplate.indexOf('>', position + 2); + if (gtPos == -1) { ref = "$<"; - refReplacement = ref; + ops.add(new LiteralReplacement(ref)); } else { - ref = templateRemainder.substring(0, gtPos + 1); - String groupName = templateRemainder.substring(2, gtPos); - Object capture = - ScriptRuntime.getObjectProp( - namedCaptures, groupName, cx, scope); - if (Undefined.isUndefined(capture)) { - refReplacement = ""; - } else { - refReplacement = ScriptRuntime.toString(capture); - } + ref = replacementTemplate.substring(position, gtPos + 1); + String groupName = + replacementTemplate.substring(position + 2, gtPos); + ops.add(new NamedCaptureReplacement(groupName)); } } break; + default: + ops.add(new LiteralReplacement(ref)); + break; } + position += ref.length(); + start = position; + } else { + position++; } + } else { + position++; } + } + if (start != position) { + ops.add(new LiteralReplacement(replacementTemplate.substring(start, position))); + } + return ops; + } - int refLength = ref.length(); - templateRemainder = templateRemainder.substring(refLength); - result.append(refReplacement); + public static String getSubstitution( + Context cx, + Scriptable scope, + String matched, + String str, + int position, + List capturesList, + Object namedCaptures, + List replacementTemplate) { + if (position > str.length()) Kit.codeBug(); + StringBuilder result = new StringBuilder(); + for (var op : replacementTemplate) { + result.append( + op.replacement(cx, scope, matched, str, position, capturesList, namedCaptures)); } return result.toString(); } @@ -149,4 +150,181 @@ private static boolean isAsciiDigit(char c) { return false; } } + + public abstract static class ReplacementOperation { + + abstract String replacement( + Context cx, + Scriptable scope, + String matched, + String str, + int position, + List captures, + Object namedCaptures); + } + + private static class LiteralReplacement extends ReplacementOperation { + + private final String replacement; + + LiteralReplacement(String replacement) { + this.replacement = replacement; + } + + @Override + String replacement( + Context cx, + Scriptable scope, + String matched, + String str, + int position, + List captures, + Object namedCaptures) { + return replacement; + } + } + + private static class OneDigitCaptureReplacement extends ReplacementOperation { + private final int capture; + + OneDigitCaptureReplacement(int capture) { + this.capture = capture; + } + + @Override + String replacement( + Context cx, + Scriptable scope, + String matched, + String str, + int position, + List captures, + Object namedCaptures) { + if (capture >= 1 && capture <= captures.size()) { + var v = captures.get(capture - 1); + if (v == null || v == Undefined.instance) { + return ""; + } else { + return v.toString(); + } + } else { + return "$" + Integer.toString(capture); + } + } + } + + private static class TwoDigitCaptureReplacement extends ReplacementOperation { + private final int capture; + + TwoDigitCaptureReplacement(int capture) { + this.capture = capture; + } + + @Override + String replacement( + Context cx, + Scriptable scope, + String matched, + String str, + int position, + List captures, + Object namedCaptures) { + int i = capture; + if (i > 9 && i > captures.size() && i / 10 <= captures.size()) { + i = i / 10; // Just take the first digit. + var v = captures.get(i - 1); + if (v == null || v == Undefined.instance) { + return "" + Integer.toString(capture % 10); + } else { + return v.toString() + Integer.toString(capture % 10); + } + } else if (i >= 1 && i <= captures.size()) { + var v = captures.get(i - 1); + if (v == null || v == Undefined.instance) { + return ""; + } else { + return v.toString(); + } + } else { + return (capture >= 10 ? "$" : "$0") + Integer.toString(capture); + } + } + } + + private static class FromStartToMatchReplacement extends ReplacementOperation { + @Override + String replacement( + Context cx, + Scriptable scope, + String matched, + String str, + int position, + List captures, + Object namedCaptures) { + return str.substring(0, position); + } + } + + private static class MatchedReplacement extends ReplacementOperation { + @Override + String replacement( + Context cx, + Scriptable scope, + String matched, + String str, + int position, + List captures, + Object namedCaptures) { + return matched; + } + } + + private static class FromMatchToEndReplacement extends ReplacementOperation { + @Override + String replacement( + Context cx, + Scriptable scope, + String matched, + String str, + int position, + List captures, + Object namedCaptures) { + int matchLength = matched.length(); + int tailPos = position + matchLength; + return str.substring(Math.min(str.length(), tailPos)); + } + } + + private static class NamedCaptureReplacement extends ReplacementOperation { + final String groupName; + + NamedCaptureReplacement(String groupName) { + this.groupName = groupName; + } + + @Override + String replacement( + Context cx, + Scriptable scope, + String matched, + String str, + int position, + List captures, + Object namedCaptures) { + if (Undefined.isUndefined(namedCaptures)) { + List ops = buildReplacementList(groupName); + return "$<" + + getSubstitution( + cx, scope, matched, str, position, captures, namedCaptures, ops) + + ">"; + } + + Object capture = ScriptRuntime.getObjectProp(namedCaptures, groupName, cx, scope); + if (Undefined.isUndefined(capture)) { + return ""; + } else { + return ScriptRuntime.toString(capture); + } + } + } } diff --git a/rhino/src/main/java/org/mozilla/javascript/NativeString.java b/rhino/src/main/java/org/mozilla/javascript/NativeString.java index 90a8b9a9d7..97271653f0 100644 --- a/rhino/src/main/java/org/mozilla/javascript/NativeString.java +++ b/rhino/src/main/java/org/mozilla/javascript/NativeString.java @@ -16,6 +16,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import org.mozilla.javascript.AbstractEcmaStringOperations.ReplacementOperation; import org.mozilla.javascript.ScriptRuntime.StringIdOrIndex; /** @@ -910,8 +911,14 @@ private static Object js_replace( String string = ScriptRuntime.toString(o); String searchString = ScriptRuntime.toString(searchValue); boolean functionalReplace = replaceValue instanceof Callable; + List replaceOps; + if (!functionalReplace) { - replaceValue = ScriptRuntime.toString(replaceValue); + replaceOps = + AbstractEcmaStringOperations.buildReplacementList( + ScriptRuntime.toString(replaceValue)); + } else { + replaceOps = List.of(); } int searchLength = searchString.length(); int position = string.indexOf(searchString); @@ -937,7 +944,7 @@ private static Object js_replace( }); replacement = ScriptRuntime.toString(replacementObj); } else { - NativeArray captures = (NativeArray) cx.newArray(scope, 0); + List captures = List.of(); replacement = AbstractEcmaStringOperations.getSubstitution( cx, @@ -947,7 +954,7 @@ private static Object js_replace( position, captures, Undefined.SCRIPTABLE_UNDEFINED, - (String) replaceValue); + replaceOps); } return preceding + replacement + following; } @@ -992,8 +999,13 @@ private static Object js_replaceAll( String string = ScriptRuntime.toString(o); String searchString = ScriptRuntime.toString(searchValue); boolean functionalReplace = replaceValue instanceof Callable; + List replaceOps; if (!functionalReplace) { - replaceValue = ScriptRuntime.toString(replaceValue); + replaceOps = + AbstractEcmaStringOperations.buildReplacementList( + ScriptRuntime.toString(replaceValue)); + } else { + replaceOps = List.of(); } int searchLength = searchString.length(); int advanceBy = Math.max(1, searchLength); @@ -1029,7 +1041,7 @@ private static Object js_replaceAll( }); replacement = ScriptRuntime.toString(replacementObj); } else { - NativeArray captures = (NativeArray) cx.newArray(scope, 0); + List captures = List.of(); replacement = AbstractEcmaStringOperations.getSubstitution( cx, @@ -1039,7 +1051,7 @@ private static Object js_replaceAll( p, captures, Undefined.SCRIPTABLE_UNDEFINED, - (String) replaceValue); + replaceOps); } result.append(preserved); result.append(replacement); diff --git a/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java b/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java index a3781dbf3b..57c8637b59 100644 --- a/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java +++ b/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java @@ -13,6 +13,7 @@ import java.util.Map; import org.mozilla.javascript.AbstractEcmaObjectOperations; import org.mozilla.javascript.AbstractEcmaStringOperations; +import org.mozilla.javascript.AbstractEcmaStringOperations.ReplacementOperation; import org.mozilla.javascript.Callable; import org.mozilla.javascript.Constructable; import org.mozilla.javascript.Context; @@ -4070,8 +4071,14 @@ private Object js_SymbolReplace( int lengthS = s.length(); Object replaceValue = args.length > 1 ? args[1] : Undefined.instance; boolean functionalReplace = replaceValue instanceof Callable; + List replaceOps; + if (!functionalReplace) { - replaceValue = ScriptRuntime.toString(replaceValue); + replaceOps = + AbstractEcmaStringOperations.buildReplacementList( + ScriptRuntime.toString(replaceValue)); + } else { + replaceOps = List.of(); } String flags = ScriptRuntime.toString(ScriptRuntime.getObjectProp(thisObj, "flags", cx)); boolean global = flags.indexOf('g') != -1; @@ -4154,7 +4161,6 @@ private Object js_SymbolReplace( namedCaptures = ScriptRuntime.toObject(scope, namedCaptures); } - NativeArray capturesArray = (NativeArray) cx.newArray(scope, captures.toArray()); replacementString = AbstractEcmaStringOperations.getSubstitution( cx, @@ -4162,9 +4168,9 @@ private Object js_SymbolReplace( matched, s, position, - capturesArray, + captures, namedCaptures, - (String) replaceValue); + replaceOps); } if (position >= nextSourcePosition) { From 6dc41fa4c3168ccc350f1c2b6033510776693cf3 Mon Sep 17 00:00:00 2001 From: Balaji Rao Date: Tue, 6 Jan 2026 17:27:36 +0100 Subject: [PATCH 08/14] Refactor --- .../javascript/regexp/NativeRegExp.java | 102 ++++++++++++------ 1 file changed, 68 insertions(+), 34 deletions(-) diff --git a/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java b/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java index 57c8637b59..e48d154d1c 100644 --- a/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java +++ b/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java @@ -4072,12 +4072,15 @@ private Object js_SymbolReplace( Object replaceValue = args.length > 1 ? args[1] : Undefined.instance; boolean functionalReplace = replaceValue instanceof Callable; List replaceOps; + Callable replaceFn; if (!functionalReplace) { + replaceFn = null; replaceOps = AbstractEcmaStringOperations.buildReplacementList( ScriptRuntime.toString(replaceValue)); } else { + replaceFn = (Callable) replaceValue; replaceOps = List.of(); } String flags = ScriptRuntime.toString(ScriptRuntime.getObjectProp(thisObj, "flags", cx)); @@ -4138,40 +4141,26 @@ private Object js_SymbolReplace( } Object namedCaptures = ScriptRuntime.getObjectProp(result, "groups", cx, scope); - String replacementString; - - if (functionalReplace) { - List replacerArgs = new ArrayList<>(); - replacerArgs.add(matched); - replacerArgs.addAll(captures); - replacerArgs.add(position); - replacerArgs.add(s); - if (!Undefined.isUndefined(namedCaptures)) { - replacerArgs.add(namedCaptures); - } - - Scriptable callThis = - ScriptRuntime.getApplyOrCallThis( - cx, scope, null, 0, (Callable) replaceValue); - Object replacementValue = - ((Callable) replaceValue).call(cx, scope, callThis, replacerArgs.toArray()); - replacementString = ScriptRuntime.toString(replacementValue); - } else { - if (!Undefined.isUndefined(namedCaptures)) { - namedCaptures = ScriptRuntime.toObject(scope, namedCaptures); - } - - replacementString = - AbstractEcmaStringOperations.getSubstitution( - cx, - scope, - matched, - s, - position, - captures, - namedCaptures, - replaceOps); - } + String replacementString = + functionalReplace + ? makeComplexReplacement( + cx, + scope, + matched, + captures, + position, + s, + namedCaptures, + replaceFn) + : makeSimpleReplacement( + cx, + scope, + matched, + captures, + position, + s, + namedCaptures, + replaceOps); if (position >= nextSourcePosition) { accumulatedResult.append(s, nextSourcePosition, position); @@ -4188,6 +4177,51 @@ private Object js_SymbolReplace( } } + private String makeComplexReplacement( + Context cx, + Scriptable scope, + String matched, + List captures, + int position, + String s, + Object namedCaptures, + Callable replaceFunction) { + Object[] replacerArgs = + new Object[1 + captures.size() + (Undefined.isUndefined(namedCaptures) ? 2 : 3)]; + replacerArgs[0] = matched; + int i = 1; + for (; i <= captures.size(); i++) { + var capture = captures.get(i - 1); + replacerArgs[i] = capture == null ? Undefined.instance : capture; + } + replacerArgs[i++] = position; + replacerArgs[i++] = s; + if (!Undefined.isUndefined(namedCaptures)) { + replacerArgs[i++] = namedCaptures; + } + + Scriptable callThis = ScriptRuntime.getApplyOrCallThis(cx, scope, null, 0, replaceFunction); + Object replacementValue = replaceFunction.call(cx, scope, callThis, replacerArgs); + return ScriptRuntime.toString(replacementValue); + } + + private String makeSimpleReplacement( + Context cx, + Scriptable scope, + String matched, + List captures, + int position, + String s, + Object namedCaptures, + List replaceOps) { + if (!Undefined.isUndefined(namedCaptures)) { + namedCaptures = ScriptRuntime.toObject(scope, namedCaptures); + } + + return AbstractEcmaStringOperations.getSubstitution( + cx, scope, matched, s, position, captures, namedCaptures, replaceOps); + } + private Object js_SymbolSplit(Context cx, Scriptable scope, Scriptable rx, Object[] args) { // See ECMAScript spec 22.2.6.14 if (!ScriptRuntime.isObject(rx)) { From d3b83209c3c4b3dee053dab88fa6e0c8fc98c486 Mon Sep 17 00:00:00 2001 From: Balaji Rao Date: Wed, 7 Jan 2026 11:30:45 +0100 Subject: [PATCH 09/14] Introduce fast path for RegExp.prototype[Symbol.replace] --- .../javascript/regexp/NativeRegExp.java | 240 +++++++++++++++--- 1 file changed, 203 insertions(+), 37 deletions(-) diff --git a/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java b/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java index e48d154d1c..be49d6ed19 100644 --- a/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java +++ b/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java @@ -9,6 +9,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.mozilla.javascript.AbstractEcmaObjectOperations; @@ -3505,10 +3506,70 @@ && upcase(matchCh) == upcase((char) anchorCodePoint))) { return false; } + private static class ExecResult { + final String match; + final ArrayList captures = new ArrayList<>(); + final LinkedHashMap groups = new LinkedHashMap<>(); + final int index; + final String input; + + ExecResult(int index, String input) { + this.match = null; + this.index = index; + this.input = input; + } + + ExecResult(int index, String input, String match) { + this.match = match; + this.index = index; + this.input = input; + } + } + + Object executeRegExp( + Context cx, Scriptable scope, RegExpImpl res, String str, int[] indexp, int matchType) { + var result = executeRegExpInternal(cx, scope, res, str, indexp, matchType); + + if (result == null) { + if (matchType != PREFIX) return null; + return Undefined.instance; + } else if (matchType == TEST) { + /* + * Testing for a match and updating cx.regExpImpl: don't allocate + * an array object, do return true. + */ + return Boolean.TRUE; + } else { + Object[] captures = result.captures.toArray(); + Scriptable obj = cx.newArray(scope, captures.length + 1); + + obj.put(0, obj, result.match); + for (int i = 0; i < captures.length; i++) { + obj.put(i + 1, obj, (captures[i] == null) ? Undefined.instance : captures[i]); + } + + obj.put("index", obj, Integer.valueOf(result.index)); + obj.put("input", obj, str); + if (!result.groups.isEmpty()) { + var groups = new NativeObject(); + for (var g : result.groups.entrySet()) { + groups.put( + g.getKey(), + groups, + g.getValue() == null ? Undefined.instance : g.getValue()); + } + obj.put("groups", obj, groups); + } else { + obj.put("groups", obj, Undefined.instance); + } + return obj; + } + } + /* * indexp is assumed to be an array of length 1 */ - Object executeRegExp( + ExecResult executeRegExpInternal( Context cx, Scriptable scope, RegExpImpl res, String str, int[] indexp, int matchType) { REGlobalData gData = new REGlobalData(); @@ -3519,25 +3580,16 @@ Object executeRegExp( // Call the recursive matcher to do the real work. // boolean matches = matchRegExp(cx, gData, re, str, start, end, res.multiline); - if (!matches) { - if (matchType != PREFIX) return null; - return Undefined.instance; - } + if (!matches) return null; + int index = gData.cp; int ep = indexp[0] = index; int matchlen = ep - (start + gData.skipped); index -= matchlen; - Object result; - Scriptable obj; - Scriptable groups = Undefined.SCRIPTABLE_UNDEFINED; + ExecResult result; if (matchType == TEST) { - /* - * Testing for a match and updating cx.regExpImpl: don't allocate - * an array object, do return true. - */ - result = Boolean.TRUE; - obj = null; + result = new ExecResult(index, str); } else { /* * The array returned on match has element 0 bound to the matched @@ -3545,11 +3597,9 @@ Object executeRegExp( * matches, an index property telling the length of the left context, * and an input property referring to the input string. */ - result = cx.newArray(scope, 0); - obj = (Scriptable) result; String matchstr = str.substring(index, index + matchlen); - obj.put(0, obj, matchstr); + result = new ExecResult(index, str, matchstr); } if (re.parenCount == 0) { @@ -3563,11 +3613,6 @@ Object executeRegExp( if (matchType != TEST) { namedCaptureGroups = new String[re.parenCount]; - if (!re.namedCaptureGroups.isEmpty()) { - // We do a new NativeObject() and not cx.newObject(scope) - // since we want the groups to have null as prototype - groups = new NativeObject(); - } for (Map.Entry> entry : re.namedCaptureGroups.entrySet()) { String key = entry.getKey(); List indices = entry.getValue(); @@ -3585,17 +3630,17 @@ Object executeRegExp( parsub = new SubString(str, cap_index, cap_length); res.parens[num] = parsub; if (matchType != TEST) { - obj.put(num + 1, obj, parsub.toString()); + result.captures.add(parsub.toString()); if (namedCaptureGroups[num] != null) { - groups.put(namedCaptureGroups[num], groups, parsub.toString()); + result.groups.put(namedCaptureGroups[num], parsub.toString()); } } } else { + result.captures.add(null); if (matchType != TEST) { - obj.put(num + 1, obj, Undefined.instance); if (namedCaptureGroups[num] != null - && !groups.has(namedCaptureGroups[num], groups)) { - groups.put(namedCaptureGroups[num], groups, Undefined.instance); + && !result.groups.containsKey(namedCaptureGroups[num])) { + result.groups.put(namedCaptureGroups[num], null); } } } @@ -3603,16 +3648,6 @@ Object executeRegExp( res.lastParen = parsub; } - if (!(matchType == TEST)) { - /* - * Define the index and input properties last for better for/in loop - * order (so they come after the elements). - */ - obj.put("index", obj, Integer.valueOf(start + gData.skipped)); - obj.put("input", obj, str); - obj.put("groups", obj, groups); - } - if (res.lastMatch == null) { res.lastMatch = new SubString(); res.leftContext = new SubString(); @@ -4062,6 +4097,137 @@ private Object js_SymbolMatchAll( private Object js_SymbolReplace( Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + if (thisObj instanceof NativeRegExp) { + var regexp = (NativeRegExp) thisObj; + var exec = ScriptableObject.getProperty(regexp, "exec"); + if ((regexp.lastIndexAttr & READONLY) == 0 + && exec instanceof IdFunctionObject + && ((IdFunctionObject) exec).methodId() == Id_exec + && ((IdFunctionObject) exec).getTag() == REGEXP_TAG) + return regexp.js_SymbolReplaceFast(cx, scope, (NativeRegExp) thisObj, args); + } + return js_SymbolReplaceSlow(cx, scope, thisObj, args); + } + + private Object js_SymbolReplaceFast( + Context cx, Scriptable scope, NativeRegExp thisObj, Object[] args) { + String s = ScriptRuntime.toString(args.length > 0 ? args[0] : Undefined.instance); + int lengthS = s.length(); + Object replaceValue = args.length > 1 ? args[1] : Undefined.instance; + boolean functionalReplace = replaceValue instanceof Callable; + List replaceOps; + Callable replaceFn; + if (!functionalReplace) { + replaceFn = null; + replaceOps = + AbstractEcmaStringOperations.buildReplacementList( + ScriptRuntime.toString(replaceValue)); + } else { + replaceFn = (Callable) replaceValue; + replaceOps = List.of(); + } + String flags = ScriptRuntime.toString(ScriptRuntime.getObjectProp(thisObj, "flags", cx)); + boolean fullUnicode = flags.indexOf('u') != -1 || flags.indexOf('v') != -1; + + List results = new ArrayList<>(); + boolean done = false; + + RegExpImpl reImpl = getImpl(cx); + boolean sticky = (re.flags & JSREG_STICKY) != 0; + boolean global = (re.flags & JSREG_GLOB) != 0; + + int[] indexp = {0}; + if (sticky) { + indexp[0] = (int) getLastIndex(cx, thisObj); + } + while (!done) { + ExecResult result; + if (indexp[0] < 0 || indexp[0] > s.length()) { + result = null; + } else { + result = executeRegExpInternal(cx, scope, reImpl, s, indexp, MATCH); + } + if (result == null) { + if (global || sticky) { + indexp[0] = 0; + } + done = true; + } else { + results.add(result); + if (!global) { + done = true; + } else { + String matchStr = result.match; + if (matchStr.isEmpty()) { + indexp[0] = + (int) ScriptRuntime.advanceStringIndex(s, indexp[0], fullUnicode); + } + } + } + } + setLastIndex(thisObj, indexp[0]); + + StringBuilder accumulatedResult = new StringBuilder(); + int nextSourcePosition = 0; + for (ExecResult result : results) { + String matched = result.match; + int matchLength = matched.length(); + double positionDbl = result.index; + int position = ScriptRuntime.clamp((int) positionDbl, 0, lengthS); + + List captures = result.captures; + Object namedCaptures; + if (!result.groups.isEmpty()) { + var groups = new NativeObject(); + for (var g : result.groups.entrySet()) { + groups.put( + g.getKey(), + groups, + g.getValue() == null ? Undefined.instance : g.getValue()); + } + namedCaptures = groups; + } else { + namedCaptures = Undefined.instance; + } + + String replacementString = + functionalReplace + ? makeComplexReplacement( + cx, + scope, + matched, + captures, + position, + s, + namedCaptures, + replaceFn) + : makeSimpleReplacement( + cx, + scope, + matched, + captures, + position, + s, + namedCaptures, + replaceOps); + + if (position >= nextSourcePosition) { + accumulatedResult.append(s, nextSourcePosition, position); + accumulatedResult.append(replacementString); + nextSourcePosition = position + matchLength; + } + } + + if (nextSourcePosition >= lengthS) { + return accumulatedResult.toString(); + } else { + accumulatedResult.append(s.substring(nextSourcePosition)); + return accumulatedResult.toString(); + } + } + + private Object js_SymbolReplaceSlow( + Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { // See ECMAScript spec 22.2.6.11 if (!ScriptRuntime.isObject(thisObj)) { throw ScriptRuntime.typeErrorById("msg.arg.not.object", ScriptRuntime.typeof(thisObj)); From 50fadbb4866bfdacbc4e13360e2823efefa7cb1a Mon Sep 17 00:00:00 2001 From: Balaji Rao Date: Wed, 7 Jan 2026 11:31:13 +0100 Subject: [PATCH 10/14] Introduce fast path for RegExp.prototype[Symbol.split] --- .../javascript/regexp/NativeRegExp.java | 97 ++++++++++++++++++- 1 file changed, 92 insertions(+), 5 deletions(-) diff --git a/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java b/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java index be49d6ed19..496840658d 100644 --- a/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java +++ b/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java @@ -3843,7 +3843,7 @@ private void setLastIndex(ScriptableObject thisObj, Object value) { setLastIndex((Scriptable) thisObj, value); } - private void setLastIndex(Scriptable thisObj, Object value) { + private static void setLastIndex(Scriptable thisObj, Object value) { ScriptableObject.putProperty(thisObj, "lastIndex", value); } @@ -4404,13 +4404,10 @@ private Object js_SymbolSplit(Context cx, Scriptable scope, Scriptable rx, Objec String flags = ScriptRuntime.toString(ScriptRuntime.getObjectProp(rx, "flags", cx)); boolean unicodeMatching = flags.indexOf('u') != -1 || flags.indexOf('v') != -1; + NativeArray a = (NativeArray) cx.newArray(scope, 0); String newFlags = flags.indexOf('y') != -1 ? flags : (flags + "y"); - Scriptable splitter = c.construct(cx, scope, new Object[] {rx, newFlags}); - NativeArray a = (NativeArray) cx.newArray(scope, 0); - int lengthA = 0; - Object limit = args.length > 1 ? args[1] : Undefined.instance; long lim; if (Undefined.isUndefined(limit)) { @@ -4422,6 +4419,30 @@ private Object js_SymbolSplit(Context cx, Scriptable scope, Scriptable rx, Objec return a; } + if (splitter instanceof NativeRegExp) { + var regexp = (NativeRegExp) splitter; + var exec = ScriptableObject.getProperty(regexp, "exec"); + if ((regexp.lastIndexAttr & READONLY) == 0 + && exec instanceof IdFunctionObject + && ((IdFunctionObject) exec).methodId() == Id_exec + && ((IdFunctionObject) exec).getTag() == REGEXP_TAG) + return js_SymbolSplitFast( + cx, scope, (NativeRegExp) splitter, s, lim, unicodeMatching, a); + } + + return js_SymbolSplitSlow(cx, scope, splitter, s, lim, unicodeMatching, a); + } + + private static Object js_SymbolSplitSlow( + Context cx, + Scriptable scope, + Scriptable splitter, + String s, + long lim, + boolean unicodeMatching, + NativeArray a) { + int lengthA = 0; + if (s.isEmpty()) { Object z = regExpExec(splitter, s, cx, scope); if (z != null) { @@ -4476,6 +4497,72 @@ private Object js_SymbolSplit(Context cx, Scriptable scope, Scriptable rx, Objec return a; } + private static Object js_SymbolSplitFast( + Context cx, + Scriptable scope, + NativeRegExp splitter, + String s, + long lim, + boolean unicodeMatching, + NativeArray a) { + int lengthA = 0; + + int[] indexp = {0}; + RegExpImpl reImpl = getImpl(cx); + if (s.isEmpty()) { + ExecResult result = splitter.executeRegExpInternal(cx, scope, reImpl, s, indexp, MATCH); + if (result != null) { + return a; + } + a.put(0, a, s); + return a; + } + + int size = s.length(); + long p = 0; + long q = p; + while (q < size) { + indexp[0] = (int) q; + ExecResult result = splitter.executeRegExpInternal(cx, scope, reImpl, s, indexp, MATCH); + + if (result == null) { + q = ScriptRuntime.advanceStringIndex(s, q, unicodeMatching); + } else { + long e = indexp[0]; + e = Math.min(e, size); + if (e == p) { + q = ScriptRuntime.advanceStringIndex(s, q, unicodeMatching); + } else { + String t = s.substring((int) p, (int) q); + a.put((int) a.getLength(), a, t); + lengthA++; + if (a.getLength() == lim) { + return a; + } + + p = e; + int i = 0; + while (i < result.captures.size()) { + Object nextCapture = result.captures.get(i); + a.put( + (int) a.getLength(), + a, + nextCapture == null ? Undefined.instance : nextCapture); + i = i + 1; + lengthA++; + if (lengthA == lim) { + return a; + } + } + q = p; + } + } + } + String t = s.substring((int) p, size); + a.put((int) a.getLength(), a, t); + return a; + } + private static long getLastIndex(Context cx, Scriptable thisObj) { return ScriptRuntime.toLength(ScriptRuntime.getObjectProp(thisObj, "lastIndex", cx)); } From 3adeccf299ec866f637f63fe3fc299de465b4e82 Mon Sep 17 00:00:00 2001 From: Greg Brail Date: Sat, 7 Feb 2026 14:06:24 -0800 Subject: [PATCH 11/14] Make it possibly to manually run CI --- .github/workflows/gradle.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 891a1db8d0..32568175cd 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -5,6 +5,7 @@ on: branches: [ master ] pull_request: branches: [ master ] + workflow_dispatch: permissions: read-all From a5fa0f818bb0ccded8de9c33bdead81ccc25c1a3 Mon Sep 17 00:00:00 2001 From: Greg Brail Date: Sun, 8 Feb 2026 11:10:09 -0800 Subject: [PATCH 12/14] Update for 1.9.1 release --- RELEASE-NOTES.md | 12 ++++++++++++ gradle.properties | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index ef198d6b8b..28a900fae2 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,3 +1,15 @@ +# Rhino 1.9.1 +## February 10, 2026 + +This release fixes a few small regressions introduced in Rhino 1.9.0. + +* Ensure that the "global" object is present, necessary for core-js to run +* Prevent compiled methods from introducing illegal characters in their names +* Support reserved words like "class" as names of XML attributes +* Fix a performance regression in the RegExp engine. + +Thanks to all who contributed! + # Rhino 1.9.0 ## December 22, 2025 diff --git a/gradle.properties b/gradle.properties index 35e1b4114f..6e76e30e90 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ rootProject.name=rhino-root group=org.mozilla -version=1.9.1-SNAPSHOT +version=1.9.1 mavenReleaseRepo=https://oss.sonatype.org/service/local/staging/deploy/maven2/ mavenSnapshotRepo=https://oss.sonatype.org/content/repositories/snapshots githubPackagesRepo=https://maven.pkg.github.com/mozilla/rhino From 8527ebbf6d0b1599f819b5001af06e5dd9147013 Mon Sep 17 00:00:00 2001 From: Greg Brail Date: Sun, 15 Feb 2026 12:34:26 -0800 Subject: [PATCH 13/14] Update for current release date --- README.md | 1 + RELEASE-NOTES.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 52e09e25b4..e8305716ed 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ The current release is Rhino 1.9.1February 15, 2026 Rhino 1.9.0December 22, 2025 Rhino 1.8.1December 2, 2025 Rhino 1.8.0January 2, 2025 diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 28a900fae2..94aacefae3 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,5 +1,5 @@ # Rhino 1.9.1 -## February 10, 2026 +## February 15, 2026 This release fixes a few small regressions introduced in Rhino 1.9.0. From ff720a14950c087e0ebb229a49fcf52ff762082a Mon Sep 17 00:00:00 2001 From: Finomosec <1665799+Finomosec@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:08:33 +0100 Subject: [PATCH 14/14] fix: use uint16 instead of uint8 for array literal skipIndexesId and sourcePositions When a script contains more than 255 object/array literals, the literalIds table grows beyond 255. The skipIndexesId (used for sparse array literals) and sourcePositions (used for spread in sparse arrays) were encoded as uint8, causing an IllegalStateException (Kit.codeBug) when the values exceeded 255. Changed addUint8 to addUint16 in CodeGenerator.visitArrayLiteral and updated the corresponding reads in Interpreter (DoLiteralNewArray, DoSpread) to use getIndex (uint16) instead of single-byte reads. Co-Authored-By: Claude Opus 4.6 --- gradle.properties | 2 +- .../org/mozilla/javascript/CodeGenerator.java | 4 +- .../org/mozilla/javascript/Interpreter.java | 12 ++- .../tests/ArrayLiteralOverflowTest.java | 90 +++++++++++++++++++ 4 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 rhino/src/test/java/org/mozilla/javascript/tests/ArrayLiteralOverflowTest.java diff --git a/gradle.properties b/gradle.properties index 6e76e30e90..91c1688286 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ rootProject.name=rhino-root group=org.mozilla -version=1.9.1 +version=1.9.1-PATCHED mavenReleaseRepo=https://oss.sonatype.org/service/local/staging/deploy/maven2/ mavenSnapshotRepo=https://oss.sonatype.org/content/repositories/snapshots githubPackagesRepo=https://maven.pkg.github.com/mozilla/rhino diff --git a/rhino/src/main/java/org/mozilla/javascript/CodeGenerator.java b/rhino/src/main/java/org/mozilla/javascript/CodeGenerator.java index a9d91b85ce..dc91c3ccc0 100644 --- a/rhino/src/main/java/org/mozilla/javascript/CodeGenerator.java +++ b/rhino/src/main/java/org/mozilla/javascript/CodeGenerator.java @@ -1552,7 +1552,7 @@ private void visitArrayLiteral(Node node, Node child) { } addIndexOp(Icode_LITERAL_NEW_ARRAY, count - numberOfSpread); - addUint8(skipIndexesId + 1); + addUint16(skipIndexesId + 1); stackChange(1); int childIdx = 0; @@ -1561,7 +1561,7 @@ private void visitArrayLiteral(Node node, Node child) { visitExpression(child.getFirstChild(), 0); addIcode(Icode_SPREAD); if (skipIndexes != null) { - addUint8(sourcePositions[childIdx]); + addUint16(sourcePositions[childIdx]); } stackChange(-1); } else { diff --git a/rhino/src/main/java/org/mozilla/javascript/Interpreter.java b/rhino/src/main/java/org/mozilla/javascript/Interpreter.java index 14a829b861..c3eb585747 100644 --- a/rhino/src/main/java/org/mozilla/javascript/Interpreter.java +++ b/rhino/src/main/java/org/mozilla/javascript/Interpreter.java @@ -1191,6 +1191,10 @@ private static int bytecodeSpan(int bytecode) { // make a copy or not flag return 1 + 1; + case Icode_LITERAL_NEW_ARRAY: + // skip indexes ID (uint16) + return 1 + 2; + case Icode_REG_BIGINT1: // ubyte bigint index return 1 + 1; @@ -4400,8 +4404,8 @@ NewState execute(Context cx, CallFrame frame, InterpreterState state, int op) { // indexReg: number of values in the literal NewLiteralStorage storage = NewLiteralStorage.create(cx, state.indexReg, false); - int skipIdx = 0xFF & frame.idata.itsICode[frame.pc]; - ++frame.pc; + int skipIdx = getIndex(frame.idata.itsICode, frame.pc); + frame.pc += 2; // fill in skip indexes in array literal storage if (skipIdx > 0) { // 0 - no skip index, otherwise subtract 1 from idx @@ -4468,8 +4472,8 @@ NewState execute(Context cx, CallFrame frame, InterpreterState state, int op) { NewLiteralStorage store = (NewLiteralStorage) frame.stack[state.stackTop]; if (store.hasSkipIndexes()) { - int sourcePos = 0xFF & frame.idata.itsICode[frame.pc]; - ++frame.pc; + int sourcePos = getIndex(frame.idata.itsICode, frame.pc); + frame.pc += 2; store.spread(cx, frame.scope, source, sourcePos); } else { store.spread(cx, frame.scope, source, 0); diff --git a/rhino/src/test/java/org/mozilla/javascript/tests/ArrayLiteralOverflowTest.java b/rhino/src/test/java/org/mozilla/javascript/tests/ArrayLiteralOverflowTest.java new file mode 100644 index 0000000000..542215c2b6 --- /dev/null +++ b/rhino/src/test/java/org/mozilla/javascript/tests/ArrayLiteralOverflowTest.java @@ -0,0 +1,90 @@ +package org.mozilla.javascript.tests; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.ContextFactory; +import org.mozilla.javascript.Scriptable; + +/** + * Regression test for addUint8 overflow in CodeGenerator.visitArrayLiteral. + * When a script contains more than 255 object/array literals, the literalIds + * table grows beyond 255, and skipIndexesId (encoded as uint8) overflows. + */ +public class ArrayLiteralOverflowTest { + + @Test + public void testLiteralIdsOverflowWithSparseArrayInterpreted() { + runLiteralIdsOverflow(-1); + } + + @Test + public void testLiteralIdsOverflowWithSparseArrayCompiled() { + runLiteralIdsOverflow(9); + } + + @Test + public void testManySpreadArrayLiteralsInterpreted() { + runManySpreadArrayLiterals(-1); + } + + @Test + public void testManySpreadArrayLiteralsCompiled() { + runManySpreadArrayLiterals(9); + } + + /** + * Each object literal {k:v} adds one entry to literalIds. + * After 256 object literals, the next sparse array literal (with elisions) + * gets a skipIndexesId > 255, which overflows the uint8 encoding. + */ + private void runLiteralIdsOverflow(int optimizationLevel) { + StringBuilder sb = new StringBuilder(); + // Create 260 object literals to push literalIds past 255 + for (int i = 0; i < 260; i++) { + sb.append("var o").append(i).append(" = {k: ").append(i).append("};\n"); + } + // Now create a sparse array (with elision) that needs skipIndexes + // [1, , 3] has a "hole" at index 1 which triggers skipIndexes + sb.append("var sparse = [1, , 3];\n"); + sb.append("sparse[0] + sparse[2];\n"); + String script = sb.toString(); + + ContextFactory factory = new ContextFactory(); + factory.call(cx -> { + cx.setOptimizationLevel(optimizationLevel); + cx.setLanguageVersion(Context.VERSION_ES6); + Scriptable scope = cx.initStandardObjects(); + Object result = cx.evaluateString(scope, script, "test.js", 1, null); + assertEquals(4.0, ((Number) result).doubleValue(), 0.0); + return null; + }); + } + + /** + * Tests spread with sparse arrays when literalIds has grown past 255. + */ + private void runManySpreadArrayLiterals(int optimizationLevel) { + StringBuilder sb = new StringBuilder(); + sb.append("var base = [0];\n"); + // 260 object literals to fill literalIds + for (int i = 0; i < 260; i++) { + sb.append("var o").append(i).append(" = {k: ").append(i).append("};\n"); + } + // Sparse array with spread: triggers both skipIndexesId and sourcePositions overflow + sb.append("var result = [...base, , 42];\n"); + sb.append("result[2];\n"); + String script = sb.toString(); + + ContextFactory factory = new ContextFactory(); + factory.call(cx -> { + cx.setOptimizationLevel(optimizationLevel); + cx.setLanguageVersion(Context.VERSION_ES6); + Scriptable scope = cx.initStandardObjects(); + Object result = cx.evaluateString(scope, script, "test.js", 1, null); + assertEquals(42.0, ((Number) result).doubleValue(), 0.0); + return null; + }); + } +}