LibRegex/Rust: Add the ECMA-262 regex engine
Add LibRegex's new Rust ECMAScript regular expression engine. Replace the old parser's direct pattern-to-bytecode pipeline with a split architecture: parse patterns into a lossless AST first, then lower that AST into bytecode for a dedicated backtracking VM. Keep the syntax tree as the place for validation, analysis, and optimization instead of teaching every transformation to rewrite partially built bytecode. Specialize this backend for the job LibJS actually needs. The old C++ engine shared one generic parser and matcher stack across ECMA-262 and POSIX modes and supported both byte-string and UTF-16 inputs. The new engine focuses on ECMA-262 semantics on WTF-16 data, which lets it model lone surrogates and other JavaScript-specific behavior directly instead of carrying POSIX and multi-encoding constraints through the whole implementation. Fill in the ECMAScript features needed to replace the old engine for real web workloads: Unicode properties and sets, lookahead and lookbehind, named groups and backreferences, modifier groups, string properties, large quantifiers, lone surrogates, and the parser and VM corner cases those features exercise. Reshape the runtime around compile-time pattern hints and a hotter VM loop. Pre-resolve Unicode properties, derive first-character, character-class, and simple-scan filters, extract safe trailing literals for anchored patterns, add literal and literal-alternation fast paths, and keep reusable scratch storage for registers, backtracking state, and modifier stacks. Teach `find_all` to stay inside one VM so global searches stop paying setup costs on every match. Make those shortcuts semantics-aware instead of merely fast. In Unicode mode, do not use literal fast paths for lone surrogates, since ECMA-262 must not let `/\ud83d/u` match inside a surrogate pair. Likewise, only derive end-anchor suffix hints when the suffix lies on every path to `Match`, so lookarounds and disjunctions cannot skip into a shared tail and produce false negatives. This commit lands the Rust crate, the C++ wrapper, the build integration, and the initial LibJS-side plumbing needed to exercise the new engine under real RegExp callers before removing the legacy backend.
This commit is contained in:
parent
3646912c5c
commit
66fb0a8394
32 changed files with 8532 additions and 28 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
|
@ -378,6 +378,13 @@ version = "0.2.16"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||
|
||||
[[package]]
|
||||
name = "libregex_rust"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cbindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libunicode_rust"
|
||||
version = "0.1.0"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"Libraries/LibJS/Rust",
|
||||
"Libraries/LibRegex/Rust",
|
||||
"Libraries/LibUnicode/Rust",
|
||||
]
|
||||
exclude = [
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@
|
|||
#include <LibJS/Runtime/StringPrototype.h>
|
||||
#include <LibJS/Runtime/ValueInlines.h>
|
||||
|
||||
#ifdef ENABLE_RUST
|
||||
# include <AK/HashMap.h>
|
||||
# include <LibRegex/RustRegex.h>
|
||||
#endif
|
||||
|
||||
namespace JS {
|
||||
|
||||
GC_DEFINE_ALLOCATOR(RegExpPrototype);
|
||||
|
|
@ -170,10 +175,261 @@ static Value make_match_indices_index_pair_array(VM& vm, Utf16View const& string
|
|||
return array;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_RUST
|
||||
static bool s_use_rust_regex = []() {
|
||||
auto* env = getenv("LIBREGEX_USE_RUST");
|
||||
return env && env[0] == '1';
|
||||
}();
|
||||
|
||||
static HashMap<String, regex::CompiledRustRegex> s_rust_regex_cache;
|
||||
|
||||
static regex::CompiledRustRegex const* get_or_compile_rust_regex(RegExpObject& regexp_object)
|
||||
{
|
||||
auto const& regex = regexp_object.regex();
|
||||
auto const& pattern = regex.pattern_value;
|
||||
|
||||
// Build a cache key from pattern + flag bits.
|
||||
auto cache_key = MUST(String::formatted("/{}/{}", pattern, static_cast<u8>(regexp_object.flag_bits())));
|
||||
|
||||
if (auto it = s_rust_regex_cache.find(cache_key); it != s_rust_regex_cache.end())
|
||||
return &it->value;
|
||||
|
||||
auto options = regex.options();
|
||||
RustRegexFlags rust_flags {};
|
||||
rust_flags.global = options.has_flag_set(ECMAScriptFlags::Global);
|
||||
rust_flags.ignore_case = options.has_flag_set(ECMAScriptFlags::Insensitive);
|
||||
rust_flags.multiline = options.has_flag_set(ECMAScriptFlags::Multiline);
|
||||
rust_flags.dot_all = options.has_flag_set(ECMAScriptFlags::SingleLine);
|
||||
rust_flags.unicode = options.has_flag_set(ECMAScriptFlags::Unicode);
|
||||
rust_flags.unicode_sets = options.has_flag_set(ECMAScriptFlags::UnicodeSets);
|
||||
rust_flags.sticky = options.has_flag_set(ECMAScriptFlags::Sticky);
|
||||
rust_flags.has_indices = has_flag(regexp_object.flag_bits(), RegExpObject::Flags::HasIndices);
|
||||
|
||||
auto compiled = regex::CompiledRustRegex::compile(pattern.view(), rust_flags);
|
||||
if (!compiled.has_value())
|
||||
return nullptr;
|
||||
|
||||
s_rust_regex_cache.set(cache_key, compiled.release_value());
|
||||
return &s_rust_regex_cache.find(cache_key)->value;
|
||||
}
|
||||
|
||||
static ThrowCompletionOr<Value> rust_regexp_builtin_exec(VM& vm, RegExpObject& regexp_object, GC::Ref<PrimitiveString> string)
|
||||
{
|
||||
auto& realm = *vm.current_realm();
|
||||
|
||||
static Bytecode::StaticPropertyLookupCache cache;
|
||||
auto last_index_value = TRY(regexp_object.get(vm.names.lastIndex, cache));
|
||||
auto last_index = TRY(last_index_value.to_length(vm));
|
||||
|
||||
auto const& regex = regexp_object.regex();
|
||||
|
||||
bool global = regex.options().has_flag_set(ECMAScriptFlags::Global);
|
||||
bool sticky = regex.options().has_flag_set(ECMAScriptFlags::Sticky);
|
||||
bool has_indices = has_flag(regexp_object.flag_bits(), RegExpObject::Flags::HasIndices);
|
||||
bool full_unicode = regex.options().has_flag_set(ECMAScriptFlags::Unicode) || regex.options().has_flag_set(ECMAScriptFlags::UnicodeSets);
|
||||
|
||||
if (!global && !sticky)
|
||||
last_index = 0;
|
||||
|
||||
auto utf16_view = string->utf16_string_view();
|
||||
|
||||
if (last_index > string->length_in_utf16_code_units()) {
|
||||
if (sticky || global) {
|
||||
static Bytecode::StaticPropertyLookupCache cache2;
|
||||
TRY(regexp_object.set(vm.names.lastIndex, Value(0), cache2));
|
||||
}
|
||||
return js_null();
|
||||
}
|
||||
|
||||
auto* rust_regex = get_or_compile_rust_regex(regexp_object);
|
||||
if (!rust_regex) {
|
||||
// Fall back to C++ regex if Rust compilation fails.
|
||||
return js_null();
|
||||
}
|
||||
|
||||
// In Unicode mode, convert code unit offset to code point offset for start position.
|
||||
size_t start_pos = full_unicode ? utf16_view.code_point_offset_of(last_index) : last_index;
|
||||
|
||||
auto result = rust_regex->exec(utf16_view, start_pos);
|
||||
|
||||
if (!result.success) {
|
||||
if (sticky || global) {
|
||||
static Bytecode::StaticPropertyLookupCache cache2;
|
||||
TRY(regexp_object.set(vm.names.lastIndex, Value(0), cache2));
|
||||
}
|
||||
return js_null();
|
||||
}
|
||||
|
||||
// Group 0 is the full match.
|
||||
auto& full_match = result.captures[0];
|
||||
VERIFY(full_match.has_value());
|
||||
auto match_index = full_match->start;
|
||||
auto end_index = full_match->end;
|
||||
|
||||
// In Unicode mode, match_index and end_index are already in code unit indices from the VM.
|
||||
// Update lastIndex.
|
||||
if (global || sticky) {
|
||||
static Bytecode::StaticPropertyLookupCache cache3;
|
||||
TRY(regexp_object.set(vm.names.lastIndex, Value(end_index), cache3));
|
||||
}
|
||||
|
||||
auto n_capture_groups = rust_regex->capture_count();
|
||||
auto& named_groups = rust_regex->named_groups();
|
||||
|
||||
auto array = MUST(Array::create(realm, n_capture_groups + 1));
|
||||
array->unsafe_set_shape(realm.intrinsics().regexp_builtin_exec_array_shape());
|
||||
|
||||
// "index" property.
|
||||
array->put_direct(realm.intrinsics().regexp_builtin_exec_array_index_offset(), Value(match_index));
|
||||
|
||||
// "input" property.
|
||||
array->put_direct(realm.intrinsics().regexp_builtin_exec_array_input_offset(), string);
|
||||
|
||||
// Element 0: the full match substring.
|
||||
auto match_str = Utf16String::from_utf16(utf16_view.substring_view(full_match->start, full_match->end - full_match->start));
|
||||
array->indexed_properties().put(0, PrimitiveString::create(vm, match_str));
|
||||
|
||||
// Build a map from capture group index to group name.
|
||||
HashMap<unsigned int, StringView> index_to_name;
|
||||
for (auto const& ng : named_groups)
|
||||
index_to_name.set(ng.index, ng.name);
|
||||
|
||||
bool has_groups = !named_groups.is_empty();
|
||||
auto groups = has_groups ? Object::create(realm, nullptr) : js_undefined();
|
||||
|
||||
// "groups" property.
|
||||
array->put_direct(realm.intrinsics().regexp_builtin_exec_array_groups_offset(), groups);
|
||||
|
||||
Vector<Optional<regex::RustMatch>> indices;
|
||||
Vector<Utf16String> group_names_list;
|
||||
Vector<Utf16String> captured_values;
|
||||
Vector<Utf16FlyString> matched_group_names;
|
||||
|
||||
indices.append(regex::RustMatch { full_match->start, full_match->end });
|
||||
|
||||
for (unsigned int i = 1; i <= n_capture_groups; ++i) {
|
||||
Value captured_value;
|
||||
|
||||
if (i < result.captures.size() && result.captures[i].has_value()) {
|
||||
auto& cap = *result.captures[i];
|
||||
auto cap_view = utf16_view.substring_view(cap.start, cap.end - cap.start);
|
||||
auto cap_str = Utf16String::from_utf16(cap_view);
|
||||
captured_value = PrimitiveString::create(vm, cap_str);
|
||||
indices.append(regex::RustMatch { cap.start, cap.end });
|
||||
captured_values.append(cap_str);
|
||||
} else {
|
||||
captured_value = js_undefined();
|
||||
indices.append({});
|
||||
captured_values.append({});
|
||||
}
|
||||
|
||||
array->indexed_properties().put(i, captured_value);
|
||||
|
||||
if (auto it = index_to_name.find(i); it != index_to_name.end()) {
|
||||
auto group_name = Utf16FlyString::from_utf8(it->value);
|
||||
|
||||
if (matched_group_names.contains_slow(group_name)) {
|
||||
VERIFY(captured_value.is_undefined());
|
||||
group_names_list.append({});
|
||||
} else {
|
||||
if (!captured_value.is_undefined())
|
||||
matched_group_names.append(group_name);
|
||||
MUST(groups.as_object().create_data_property_or_throw(group_name, captured_value));
|
||||
group_names_list.append(group_name.to_utf16_string());
|
||||
}
|
||||
} else {
|
||||
group_names_list.append({});
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure named groups are enumerated in source order.
|
||||
if (has_groups) {
|
||||
auto original_groups = groups;
|
||||
groups = Object::create(realm, nullptr);
|
||||
|
||||
for (auto const& ng : named_groups) {
|
||||
auto group_name = Utf16FlyString::from_utf8(ng.name);
|
||||
auto value = original_groups.as_object().get_without_side_effects(group_name);
|
||||
MUST(groups.as_object().create_data_property_or_throw(group_name, value));
|
||||
}
|
||||
|
||||
static Bytecode::StaticPropertyLookupCache cache4;
|
||||
MUST(array->set(vm.names.groups, groups, cache4));
|
||||
}
|
||||
|
||||
// Legacy RegExp static properties.
|
||||
auto* this_realm = &realm;
|
||||
auto* regexp_object_realm = ®exp_object.realm();
|
||||
if (this_realm == regexp_object_realm) {
|
||||
if (regexp_object.legacy_features_enabled()) {
|
||||
auto match_indices_for_legacy = regex::RustMatch { full_match->start, full_match->end };
|
||||
update_legacy_regexp_static_properties(realm.intrinsics().regexp_constructor(), string->utf16_string(), match_indices_for_legacy.start, match_indices_for_legacy.end, captured_values);
|
||||
} else {
|
||||
invalidate_legacy_regexp_static_properties(realm.intrinsics().regexp_constructor());
|
||||
}
|
||||
}
|
||||
|
||||
// hasIndices ("d" flag).
|
||||
if (has_indices) {
|
||||
HashMap<Utf16FlyString, regex::RustMatch> indices_group_names;
|
||||
for (size_t i = 0; i < group_names_list.size(); ++i) {
|
||||
if (!group_names_list[i].is_empty()) {
|
||||
if (i < result.captures.size() - 1 && result.captures[i + 1].has_value()) {
|
||||
auto& cap = *result.captures[i + 1];
|
||||
indices_group_names.set(Utf16FlyString { group_names_list[i] }, regex::RustMatch { cap.start, cap.end });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build indices array manually since we can't use Match::create with RustMatch.
|
||||
auto indices_array = MUST(Array::create(realm, 0));
|
||||
for (size_t i = 0; i < indices.size(); ++i) {
|
||||
if (indices[i].has_value()) {
|
||||
auto pair = MUST(Array::create(realm, 2));
|
||||
pair->indexed_properties().put(0, Value(indices[i]->start));
|
||||
pair->indexed_properties().put(1, Value(indices[i]->end));
|
||||
indices_array->indexed_properties().put(i, pair);
|
||||
} else {
|
||||
indices_array->indexed_properties().put(i, js_undefined());
|
||||
}
|
||||
}
|
||||
|
||||
auto indices_groups = has_groups ? Object::create(realm, nullptr) : js_undefined();
|
||||
if (has_groups) {
|
||||
for (auto const& entry : indices_group_names) {
|
||||
auto pair = MUST(Array::create(realm, 2));
|
||||
pair->indexed_properties().put(0, Value(entry.value.start));
|
||||
pair->indexed_properties().put(1, Value(entry.value.end));
|
||||
MUST(indices_groups.as_object().create_data_property_or_throw(entry.key, pair));
|
||||
}
|
||||
|
||||
// Ensure source order.
|
||||
auto ordered = Object::create(realm, nullptr);
|
||||
for (auto const& ng : named_groups) {
|
||||
auto group_name = Utf16FlyString::from_utf8(ng.name);
|
||||
auto value = indices_groups.as_object().get_without_side_effects(group_name);
|
||||
MUST(ordered->create_data_property_or_throw(group_name, value));
|
||||
}
|
||||
indices_groups = ordered;
|
||||
}
|
||||
|
||||
MUST(indices_array->create_data_property_or_throw(vm.names.groups, indices_groups));
|
||||
MUST(array->create_data_property_or_throw(vm.names.indices, indices_array));
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
#endif
|
||||
|
||||
// 22.2.7.2 RegExpBuiltinExec ( R, S ), https://tc39.es/ecma262/#sec-regexpbuiltinexec
|
||||
// 22.2.7.2 RegExpBuiltInExec ( R, S ), https://github.com/tc39/proposal-regexp-legacy-features#regexpbuiltinexec--r-s-
|
||||
static ThrowCompletionOr<Value> regexp_builtin_exec(VM& vm, RegExpObject& regexp_object, GC::Ref<PrimitiveString> string)
|
||||
{
|
||||
#ifdef ENABLE_RUST
|
||||
if (s_use_rust_regex)
|
||||
return rust_regexp_builtin_exec(vm, regexp_object, string);
|
||||
#endif
|
||||
|
||||
auto& realm = *vm.current_realm();
|
||||
|
||||
// 1. Let length be the length of S.
|
||||
|
|
|
|||
|
|
@ -12,3 +12,10 @@ endif()
|
|||
|
||||
ladybird_lib(LibRegex regex EXPLICIT_SYMBOL_EXPORT)
|
||||
target_link_libraries(LibRegex PRIVATE LibUnicode)
|
||||
|
||||
if (ENABLE_RUST)
|
||||
target_sources(LibRegex PRIVATE RustRegex.cpp)
|
||||
import_rust_crate(MANIFEST_PATH Rust/Cargo.toml CRATE_NAME libregex_rust)
|
||||
target_link_libraries(LibRegex PRIVATE libregex_rust)
|
||||
target_compile_definitions(LibRegex PRIVATE ENABLE_RUST)
|
||||
endif()
|
||||
|
|
|
|||
10
Libraries/LibRegex/Rust/Cargo.toml
Normal file
10
Libraries/LibRegex/Rust/Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "libregex_rust"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib"]
|
||||
|
||||
[build-dependencies]
|
||||
cbindgen = "0.29"
|
||||
33
Libraries/LibRegex/Rust/build.rs
Normal file
33
Libraries/LibRegex/Rust/build.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (c) 2026-present, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
|
||||
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed=cbindgen.toml");
|
||||
println!("cargo:rerun-if-changed=src");
|
||||
|
||||
let ffi_out_dir = env::var("FFI_OUTPUT_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| PathBuf::from(env::var("OUT_DIR").unwrap()));
|
||||
|
||||
cbindgen::generate(manifest_dir).map_or_else(
|
||||
|error| match error {
|
||||
cbindgen::Error::ParseSyntaxError { .. } => {}
|
||||
e => panic!("{e:?}"),
|
||||
},
|
||||
|bindings| {
|
||||
bindings.write_to_file(ffi_out_dir.join("RustFFI.h"));
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
37
Libraries/LibRegex/Rust/cbindgen.toml
Normal file
37
Libraries/LibRegex/Rust/cbindgen.toml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
language = "C++"
|
||||
header = """/*
|
||||
* Copyright (c) 2026-present, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/"""
|
||||
pragma_once = true
|
||||
include_version = true
|
||||
line_length = 120
|
||||
tab_width = 4
|
||||
no_includes = true
|
||||
sys_includes = ["stdint.h", "stddef.h"]
|
||||
usize_is_size_t = true
|
||||
|
||||
[parse]
|
||||
parse_deps = false
|
||||
|
||||
[parse.expand]
|
||||
all_features = false
|
||||
|
||||
[export]
|
||||
exclude = [
|
||||
"unicode_property_matches",
|
||||
"unicode_simple_case_fold",
|
||||
"unicode_code_point_matches_range_ignoring_case",
|
||||
"unicode_property_matches_case_insensitive",
|
||||
"unicode_get_case_closure",
|
||||
"unicode_is_string_property",
|
||||
"unicode_property_all_case_equivalents_match",
|
||||
"unicode_resolved_property_matches",
|
||||
"unicode_is_valid_ecma262_property",
|
||||
"unicode_get_string_property_data",
|
||||
"unicode_resolve_property",
|
||||
]
|
||||
|
||||
[export.mangle]
|
||||
rename_types = "PascalCase"
|
||||
409
Libraries/LibRegex/Rust/src/ast.rs
Normal file
409
Libraries/LibRegex/Rust/src/ast.rs
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
/*
|
||||
* Copyright (c) 2026-present, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
//! Abstract syntax tree for ECMA-262 regular expressions.
|
||||
//!
|
||||
//! The AST is designed to faithfully represent any syntactically valid
|
||||
//! ECMAScript regex pattern, preserving all information needed for
|
||||
//! compilation, optimization, and error reporting.
|
||||
//!
|
||||
//! Spec:
|
||||
//! - <https://tc39.es/ecma262/#sec-patterns>
|
||||
//! - <https://tc39.es/ecma262/#sec-pattern-semantics>
|
||||
|
||||
/// A complete parsed regex pattern with its flags.
|
||||
///
|
||||
/// Spec model: the `Pattern` grammar goal and the RegExp Record built from it.
|
||||
/// - <https://tc39.es/ecma262/#sec-patterns>
|
||||
/// - <https://tc39.es/ecma262/#sec-regexp-records>
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Pattern {
|
||||
pub disjunction: Disjunction,
|
||||
pub flags: Flags,
|
||||
/// Total number of capture groups (not counting group 0).
|
||||
pub capture_count: u32,
|
||||
/// Named capture groups, in order of appearance.
|
||||
pub named_groups: Vec<NamedGroup>,
|
||||
}
|
||||
|
||||
/// A named capture group declaration.
|
||||
///
|
||||
/// Spec model: a named `GroupSpecifier` that later participates in named
|
||||
/// backreference resolution.
|
||||
/// <https://tc39.es/ecma262/#sec-patterns>
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct NamedGroup {
|
||||
pub name: String,
|
||||
pub index: u32,
|
||||
}
|
||||
|
||||
/// Regex flags parsed from the trailing `/flags` portion.
|
||||
///
|
||||
/// Spec model: the boolean fields in the RegExp Record that affect parsing and
|
||||
/// matcher semantics.
|
||||
/// <https://tc39.es/ecma262/#sec-regexp-records>
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct Flags {
|
||||
pub global: bool,
|
||||
pub ignore_case: bool,
|
||||
pub multiline: bool,
|
||||
pub dot_all: bool,
|
||||
pub unicode: bool,
|
||||
pub unicode_sets: bool,
|
||||
pub sticky: bool,
|
||||
pub has_indices: bool,
|
||||
}
|
||||
|
||||
/// A disjunction is a list of alternatives separated by `|`.
|
||||
///
|
||||
/// `/a|b|c/` → `Disjunction { alternatives: [a, b, c] }`
|
||||
///
|
||||
/// Spec model: `Disjunction`.
|
||||
/// <https://tc39.es/ecma262/#sec-compilesubpattern>
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Disjunction {
|
||||
pub alternatives: Vec<Alternative>,
|
||||
}
|
||||
|
||||
/// An alternative is a sequence of terms matched left-to-right.
|
||||
///
|
||||
/// `/abc/` → `Alternative { terms: [a, b, c] }`
|
||||
///
|
||||
/// Spec model: `Alternative`.
|
||||
/// <https://tc39.es/ecma262/#sec-compilesubpattern>
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Alternative {
|
||||
pub terms: Vec<Term>,
|
||||
}
|
||||
|
||||
/// A single term in an alternative: an atom optionally followed by a quantifier.
|
||||
///
|
||||
/// Spec model: `Term`.
|
||||
/// <https://tc39.es/ecma262/#sec-compilesubpattern>
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Term {
|
||||
pub atom: Atom,
|
||||
pub quantifier: Option<Quantifier>,
|
||||
}
|
||||
|
||||
/// A quantifier specifies repetition of the preceding atom.
|
||||
///
|
||||
/// Spec model: the quantifier record produced by `CompileQuantifier`.
|
||||
/// <https://tc39.es/ecma262/#sec-compilequantifier>
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Quantifier {
|
||||
pub min: u32,
|
||||
pub max: Option<u32>,
|
||||
pub greedy: bool,
|
||||
}
|
||||
|
||||
impl Quantifier {
|
||||
pub fn zero_or_more(greedy: bool) -> Self {
|
||||
Self {
|
||||
min: 0,
|
||||
max: None,
|
||||
greedy,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn one_or_more(greedy: bool) -> Self {
|
||||
Self {
|
||||
min: 1,
|
||||
max: None,
|
||||
greedy,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn zero_or_one(greedy: bool) -> Self {
|
||||
Self {
|
||||
min: 0,
|
||||
max: Some(1),
|
||||
greedy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An atom is the fundamental matching unit.
|
||||
///
|
||||
/// Spec model: `Atom` and `AtomEscape`, plus assertion-like terms that are
|
||||
/// represented as dedicated AST variants for easier downstream handling.
|
||||
/// - <https://tc39.es/ecma262/#sec-compileatom>
|
||||
/// - <https://tc39.es/ecma262/#sec-compileassertion>
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Atom {
|
||||
/// A literal character (as a u32 code point, to support WTF-16 lone surrogates).
|
||||
/// `/a/` → `Literal(0x61)`
|
||||
Literal(u32),
|
||||
|
||||
/// `.` — matches any character (except newline, unless `dot_all`).
|
||||
Dot,
|
||||
|
||||
/// A character class like `[a-z]` or `[^0-9]`.
|
||||
CharacterClass(CharacterClass),
|
||||
|
||||
/// A built-in character class escape: `\d`, `\D`, `\w`, `\W`, `\s`, `\S`.
|
||||
BuiltinCharacterClass(BuiltinCharacterClass),
|
||||
|
||||
/// A Unicode property escape: `\p{Letter}`, `\P{Script=Greek}`.
|
||||
UnicodeProperty(UnicodeProperty),
|
||||
|
||||
/// A capturing group: `(expr)` or `(?<name>expr)`.
|
||||
Group(Group),
|
||||
|
||||
/// A non-capturing group: `(?:expr)`.
|
||||
NonCapturingGroup(NonCapturingGroup),
|
||||
|
||||
/// A lookaround assertion: `(?=expr)`, `(?!expr)`, `(?<=expr)`, `(?<!expr)`.
|
||||
Lookaround(Lookaround),
|
||||
|
||||
/// A backreference: `\1`, `\k<name>`.
|
||||
Backreference(Backreference),
|
||||
|
||||
/// An assertion that matches a position, not a character.
|
||||
Assertion(AssertionKind),
|
||||
|
||||
/// A modifier group that changes flags for its contents: `(?i:expr)`.
|
||||
ModifierGroup(ModifierGroup),
|
||||
}
|
||||
|
||||
/// A character class: `[abc]`, `[a-z]`, `[^0-9]`.
|
||||
///
|
||||
/// Spec model: `CharacterClass`.
|
||||
/// <https://tc39.es/ecma262/#sec-compilecharacterclass>
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CharacterClass {
|
||||
pub negated: bool,
|
||||
pub body: CharacterClassBody,
|
||||
}
|
||||
|
||||
/// The body of a character class, which differs between `/u`|`/v` mode and
|
||||
/// legacy mode.
|
||||
///
|
||||
/// Spec model: the split between legacy `ClassContents` and `/v`
|
||||
/// `ClassSetExpression`.
|
||||
/// - <https://tc39.es/ecma262/#sec-patterns>
|
||||
/// - <https://tc39.es/ecma262/#sec-compilecharacterclass>
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CharacterClassBody {
|
||||
/// Legacy or `/u` mode: a flat list of ranges/chars.
|
||||
Ranges(Vec<CharacterClassRange>),
|
||||
|
||||
/// `/v` (unicode sets) mode: supports set operations.
|
||||
UnicodeSet(ClassSetExpression),
|
||||
}
|
||||
|
||||
/// A single element in a `[...]` character class (legacy and `/u` mode).
|
||||
///
|
||||
/// Spec model: the pieces consumed by `CompileToCharSet`.
|
||||
/// <https://tc39.es/ecma262/#sec-compiletocharset>
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CharacterClassRange {
|
||||
/// A single character (as u32 code point, to support WTF-16 lone surrogates).
|
||||
Single(u32),
|
||||
|
||||
/// A character range (as u32 code points).
|
||||
Range(u32, u32),
|
||||
|
||||
/// A character class escape inside `[...]`: `[\d]`, `[\w]`.
|
||||
BuiltinClass(BuiltinCharacterClass),
|
||||
|
||||
/// A Unicode property escape inside `[...]`: `[\p{Letter}]`.
|
||||
UnicodeProperty(UnicodeProperty),
|
||||
}
|
||||
|
||||
/// Set operations for `/v` (unicode sets) mode character classes.
|
||||
///
|
||||
/// Spec model: `ClassSetExpression`.
|
||||
/// <https://tc39.es/ecma262/#sec-compilecharacterclass>
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ClassSetExpression {
|
||||
/// A union of class set operands (the default when just listing items).
|
||||
Union(Vec<ClassSetOperand>),
|
||||
|
||||
/// Intersection: `[a-z&&[aeiou]]`.
|
||||
Intersection(Vec<ClassSetOperand>),
|
||||
|
||||
/// Subtraction: `[a-z--[aeiou]]`.
|
||||
Subtraction(Vec<ClassSetOperand>),
|
||||
}
|
||||
|
||||
/// An operand in a unicode sets class expression.
|
||||
///
|
||||
/// Spec model: `ClassSetOperand` and `ClassStringDisjunction`.
|
||||
/// - <https://tc39.es/ecma262/#sec-patterns>
|
||||
/// - <https://tc39.es/ecma262/#sec-compileclasssetstring>
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ClassSetOperand {
|
||||
/// A single character.
|
||||
Char(char),
|
||||
|
||||
/// A range: `a-z`.
|
||||
Range(char, char),
|
||||
|
||||
/// A nested character class: `[a-z]` inside another class.
|
||||
NestedClass(CharacterClass),
|
||||
|
||||
/// A builtin class escape: `\d`, `\w`, etc.
|
||||
BuiltinClass(BuiltinCharacterClass),
|
||||
|
||||
/// A Unicode property: `\p{Letter}`.
|
||||
UnicodeProperty(UnicodeProperty),
|
||||
|
||||
/// A string literal in a class (v-flag): `\q{abc}`.
|
||||
StringLiteral(Vec<char>),
|
||||
}
|
||||
|
||||
/// Built-in character class escapes.
|
||||
///
|
||||
/// Spec model: `CharacterClassEscape`.
|
||||
/// <https://tc39.es/ecma262/#sec-compiletocharset>
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BuiltinCharacterClass {
|
||||
/// `\d` — digits `[0-9]`.
|
||||
Digit,
|
||||
/// `\D` — non-digits `[^0-9]`.
|
||||
NonDigit,
|
||||
/// `\w` — word chars `[a-zA-Z0-9_]`.
|
||||
Word,
|
||||
/// `\W` — non-word chars `[^a-zA-Z0-9_]`.
|
||||
NonWord,
|
||||
/// `\s` — whitespace.
|
||||
Whitespace,
|
||||
/// `\S` — non-whitespace.
|
||||
NonWhitespace,
|
||||
}
|
||||
|
||||
impl BuiltinCharacterClass {
|
||||
/// Return the complementary (negated) class.
|
||||
pub fn negated(self) -> Self {
|
||||
match self {
|
||||
Self::Digit => Self::NonDigit,
|
||||
Self::NonDigit => Self::Digit,
|
||||
Self::Word => Self::NonWord,
|
||||
Self::NonWord => Self::Word,
|
||||
Self::Whitespace => Self::NonWhitespace,
|
||||
Self::NonWhitespace => Self::Whitespace,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A Unicode property escape: `\p{...}` or `\P{...}`.
|
||||
///
|
||||
/// Spec model: `UnicodePropertyValueExpression` and the property matching
|
||||
/// abstract operations used to validate it.
|
||||
/// - <https://tc39.es/ecma262/#sec-compiletocharset>
|
||||
/// - <https://tc39.es/ecma262/#sec-runtime-semantics-unicodematchproperty-p>
|
||||
/// - <https://tc39.es/ecma262/#sec-runtime-semantics-unicodematchpropertyvalue-p-v>
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct UnicodeProperty {
|
||||
/// Whether this is `\P{...}` (negated) or `\p{...}`.
|
||||
pub negated: bool,
|
||||
/// The property name, e.g. `"Letter"`, `"Script"`.
|
||||
pub name: String,
|
||||
/// The property value, if present, e.g. `"Greek"` in `\p{Script=Greek}`.
|
||||
pub value: Option<String>,
|
||||
}
|
||||
|
||||
/// A capturing group: `(expr)` or `(?<name>expr)`.
|
||||
///
|
||||
/// Spec model: a capturing `Atom` with an optional `GroupSpecifier`.
|
||||
/// <https://tc39.es/ecma262/#sec-compileatom>
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Group {
|
||||
/// 1-based capture index.
|
||||
pub index: u32,
|
||||
/// Optional group name for `(?<name>...)`.
|
||||
pub name: Option<String>,
|
||||
pub body: Disjunction,
|
||||
}
|
||||
|
||||
/// A non-capturing group: `(?:expr)`.
|
||||
///
|
||||
/// Spec model: the `(?: Disjunction )` branch of `Atom`.
|
||||
/// <https://tc39.es/ecma262/#sec-compileatom>
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct NonCapturingGroup {
|
||||
pub body: Disjunction,
|
||||
}
|
||||
|
||||
/// A lookaround assertion.
|
||||
///
|
||||
/// Spec model: lookahead and lookbehind forms handled by
|
||||
/// `Runtime Semantics: CompileAssertion`.
|
||||
/// <https://tc39.es/ecma262/#sec-compileassertion>
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Lookaround {
|
||||
pub kind: LookaroundKind,
|
||||
pub body: Disjunction,
|
||||
}
|
||||
|
||||
/// Spec model: the four lookaround assertion forms.
|
||||
/// <https://tc39.es/ecma262/#sec-compileassertion>
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum LookaroundKind {
|
||||
/// `(?=...)` — positive lookahead.
|
||||
LookaheadPositive,
|
||||
/// `(?!...)` — negative lookahead.
|
||||
LookaheadNegative,
|
||||
/// `(?<=...)` — positive lookbehind.
|
||||
LookbehindPositive,
|
||||
/// `(?<!...)` — negative lookbehind.
|
||||
LookbehindNegative,
|
||||
}
|
||||
|
||||
/// A backreference to a capture group.
|
||||
///
|
||||
/// Spec model: decimal and named backreference atoms.
|
||||
/// - <https://tc39.es/ecma262/#sec-compileatom>
|
||||
/// - <https://tc39.es/ecma262/#sec-backreference-matcher>
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Backreference {
|
||||
/// `\1`, `\2`, etc. — numeric backreference.
|
||||
Index(u32),
|
||||
/// `\k<name>` — named backreference.
|
||||
Named(String),
|
||||
}
|
||||
|
||||
/// A zero-width assertion.
|
||||
///
|
||||
/// Spec model: the zero-width assertion terms compiled by
|
||||
/// `Runtime Semantics: CompileAssertion`.
|
||||
/// <https://tc39.es/ecma262/#sec-compileassertion>
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AssertionKind {
|
||||
/// `^` — start of input (or line in multiline mode).
|
||||
StartOfInput,
|
||||
/// `$` — end of input (or line in multiline mode).
|
||||
EndOfInput,
|
||||
/// `\b` — word boundary.
|
||||
WordBoundary,
|
||||
/// `\B` — non-word boundary.
|
||||
NonWordBoundary,
|
||||
}
|
||||
|
||||
/// A modifier group: `(?imsu-imsu:expr)`.
|
||||
///
|
||||
/// Spec model: modifier-scoped `Atom` forms that call `UpdateModifiers`.
|
||||
/// - <https://tc39.es/ecma262/#sec-compileatom>
|
||||
/// - <https://tc39.es/ecma262/#sec-updatemodifiers>
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ModifierGroup {
|
||||
pub add_flags: ModifierFlags,
|
||||
pub remove_flags: ModifierFlags,
|
||||
pub body: Disjunction,
|
||||
}
|
||||
|
||||
/// Flags that can be toggled in a modifier group.
|
||||
///
|
||||
/// Spec model: the subset of flags accepted by `UpdateModifiers` inside
|
||||
/// a modifier group.
|
||||
/// <https://tc39.es/ecma262/#sec-updatemodifiers>
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct ModifierFlags {
|
||||
pub ignore_case: bool,
|
||||
pub multiline: bool,
|
||||
pub dot_all: bool,
|
||||
}
|
||||
323
Libraries/LibRegex/Rust/src/bytecode.rs
Normal file
323
Libraries/LibRegex/Rust/src/bytecode.rs
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
/*
|
||||
* Copyright (c) 2026-present, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
//! Bytecode instruction set for the regex VM.
|
||||
//!
|
||||
//! This is an implementation artifact for the matcher closures described in
|
||||
//! ECMA-262 Pattern Semantics.
|
||||
//!
|
||||
//! Spec:
|
||||
//! - <https://tc39.es/ecma262/#sec-pattern-semantics>
|
||||
//! - <https://tc39.es/ecma262/#sec-compilepattern>
|
||||
//!
|
||||
//! Instructions are compact and operate on a virtual machine with:
|
||||
//! - A current position in the input string
|
||||
//! - A set of registers for capture group positions
|
||||
//! - A backtrack stack for saving/restoring state
|
||||
|
||||
/// A named capture group mapping derived from the pattern's named captures.
|
||||
/// <https://tc39.es/ecma262/#sec-parsepattern>
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NamedGroupEntry {
|
||||
pub name: String,
|
||||
pub index: u32,
|
||||
}
|
||||
|
||||
/// A compiled regex program.
|
||||
///
|
||||
/// Spec model: the internal matcher produced by `CompilePattern`.
|
||||
/// <https://tc39.es/ecma262/#sec-compilepattern>
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Program {
|
||||
/// The bytecode instructions.
|
||||
pub instructions: Vec<Instruction>,
|
||||
/// Number of capture groups (not counting group 0).
|
||||
pub capture_count: u32,
|
||||
/// Total number of registers needed (2 per capture group + 2 for group 0).
|
||||
pub register_count: u32,
|
||||
/// Whether Unicode mode is enabled (affects surrogate pair decoding).
|
||||
pub unicode: bool,
|
||||
/// Whether v-flag (unicode sets) mode is enabled.
|
||||
pub unicode_sets: bool,
|
||||
/// Base ignore_case flag from pattern flags.
|
||||
pub ignore_case: bool,
|
||||
/// Base multiline flag from pattern flags.
|
||||
pub multiline: bool,
|
||||
/// Base dot_all flag from pattern flags.
|
||||
pub dot_all: bool,
|
||||
/// Named capture groups (name → group index).
|
||||
pub named_groups: Vec<NamedGroupEntry>,
|
||||
}
|
||||
|
||||
/// A single bytecode instruction in Ladybird's concrete implementation of the
|
||||
/// abstract matchers from ECMA-262 Pattern Semantics.
|
||||
/// <https://tc39.es/ecma262/#sec-pattern-semantics>
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Instruction {
|
||||
/// Match a single character (u32 code point, supports WTF-16 lone surrogates).
|
||||
Char(u32),
|
||||
|
||||
/// Match a single character, case-insensitive (u32 code points).
|
||||
CharNoCase(u32, u32),
|
||||
|
||||
/// Match any character (`.`). If `dot_all` is true, matches newlines too.
|
||||
AnyChar { dot_all: bool },
|
||||
|
||||
/// Match a character in a set of ranges. `negated` inverts the match.
|
||||
CharClass {
|
||||
ranges: Vec<CharRange>,
|
||||
negated: bool,
|
||||
},
|
||||
|
||||
/// Match a built-in character class (\d, \w, \s and negations).
|
||||
BuiltinClass(BuiltinCharacterClass),
|
||||
|
||||
/// Match a Unicode property (boxed to keep Instruction small).
|
||||
UnicodeProperty(Box<UnicodePropertyData>),
|
||||
|
||||
/// Unconditional jump to target instruction.
|
||||
Jump(u32),
|
||||
|
||||
/// Split execution: try `prefer` first, backtrack to `other`.
|
||||
/// This is the fundamental backtracking primitive.
|
||||
Split { prefer: u32, other: u32 },
|
||||
|
||||
/// Save current input position to register `reg`.
|
||||
Save(u32),
|
||||
|
||||
/// Clear register `reg` to -1 (no match).
|
||||
ClearRegister(u32),
|
||||
|
||||
/// Assert start of input (or line if multiline).
|
||||
AssertStart { multiline: bool },
|
||||
|
||||
/// Assert end of input (or line if multiline).
|
||||
AssertEnd { multiline: bool },
|
||||
|
||||
/// Assert word boundary.
|
||||
AssertWordBoundary,
|
||||
|
||||
/// Assert non-word boundary.
|
||||
AssertNonWordBoundary,
|
||||
|
||||
/// Match succeeded.
|
||||
Match,
|
||||
|
||||
/// Fail — force backtrack.
|
||||
Fail,
|
||||
|
||||
/// Backreference: match the same string as capture group `index`.
|
||||
Backref(u32),
|
||||
|
||||
/// Named backreference.
|
||||
BackrefNamed(String),
|
||||
|
||||
/// Begin a repetition counter at register `counter_reg`.
|
||||
/// Sets the register to 0.
|
||||
RepeatStart { counter_reg: u32 },
|
||||
|
||||
/// Check repetition: if counter < max, increment and jump to `body`.
|
||||
/// Otherwise fall through. Used with Split for greedy/lazy.
|
||||
RepeatCheck {
|
||||
counter_reg: u32,
|
||||
min: u32,
|
||||
max: Option<u32>,
|
||||
body: u32,
|
||||
greedy: bool,
|
||||
},
|
||||
|
||||
/// Lookahead/lookbehind assertion.
|
||||
/// `positive`: whether match must succeed or fail.
|
||||
/// `forward`: lookahead (true) or lookbehind (false).
|
||||
/// `body`: start of the assertion body.
|
||||
/// `end`: instruction after the assertion.
|
||||
LookStart {
|
||||
positive: bool,
|
||||
forward: bool,
|
||||
end: u32,
|
||||
},
|
||||
|
||||
/// End of a lookaround body. Signals success of the assertion sub-match.
|
||||
LookEnd,
|
||||
|
||||
/// Push modifier flags onto the modifier stack.
|
||||
PushModifiers {
|
||||
ignore_case: Option<bool>,
|
||||
multiline: Option<bool>,
|
||||
dot_all: Option<bool>,
|
||||
},
|
||||
|
||||
/// Pop modifier flags from the modifier stack.
|
||||
PopModifiers,
|
||||
|
||||
/// No-op, used as a placeholder during compilation.
|
||||
Nop,
|
||||
|
||||
/// Atomically match one string from a Unicode string property.
|
||||
/// Tries multi-codepoint strings first (longest match wins), then falls
|
||||
/// back to single-codepoint UnicodeProperty match. Does not create
|
||||
/// backtrack points -- once a match is found, it's committed.
|
||||
StringPropertyMatch {
|
||||
/// Multi-codepoint strings, sorted longest first and packed as:
|
||||
/// [len, cp0, cp1, ..., len, cp0, ...]
|
||||
strings: Box<[u32]>,
|
||||
/// Fallback for single-codepoint matches.
|
||||
property: Box<UnicodePropertyData>,
|
||||
},
|
||||
|
||||
/// Progress check: save position at `reg`, fail if no progress since last visit.
|
||||
/// Used to prevent infinite loops in zero-width quantifier bodies.
|
||||
/// When `clear_captures` is set, those registers are cleared to -1 before
|
||||
/// backtracking on zero-width, per ECMA-262 RepeatMatcher step 2.b.
|
||||
ProgressCheck { reg: u32, clear_captures: Vec<u32> },
|
||||
|
||||
/// Greedy loop for simple character matchers.
|
||||
/// Greedily consumes as many matching characters as possible, then pushes a
|
||||
/// single backtrack state. On backtrack, gives up one character at a time.
|
||||
/// This avoids per-iteration Split/backtrack overhead for simple quantifiers.
|
||||
GreedyLoop {
|
||||
matcher: SimpleMatch,
|
||||
min: u32,
|
||||
max: Option<u32>,
|
||||
},
|
||||
|
||||
/// Lazy loop for simple character matchers.
|
||||
/// Tries to match as few characters as possible, then on backtrack consumes one more.
|
||||
LazyLoop {
|
||||
matcher: SimpleMatch,
|
||||
min: u32,
|
||||
max: Option<u32>,
|
||||
},
|
||||
}
|
||||
|
||||
/// The kind of a resolved Unicode property.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum PropertyKind {
|
||||
Script = 0,
|
||||
ScriptExtension = 1,
|
||||
GeneralCategory = 2,
|
||||
BinaryProperty = 3,
|
||||
}
|
||||
|
||||
impl PropertyKind {
|
||||
pub fn from_u8(v: u8) -> Option<Self> {
|
||||
match v {
|
||||
0 => Some(Self::Script),
|
||||
1 => Some(Self::ScriptExtension),
|
||||
2 => Some(Self::GeneralCategory),
|
||||
3 => Some(Self::BinaryProperty),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A resolved Unicode property — the string name/value has been resolved to
|
||||
/// an ICU enum at compile time, so match-time lookups avoid string parsing.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ResolvedProperty {
|
||||
/// The kind of Unicode property (Script, GeneralCategory, etc.).
|
||||
pub kind: PropertyKind,
|
||||
/// ICU enum value (e.g. script code, general category, binary property ID).
|
||||
pub id: u32,
|
||||
}
|
||||
|
||||
/// Data for a Unicode property match instruction.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct UnicodePropertyData {
|
||||
pub negated: bool,
|
||||
pub name: String,
|
||||
pub value: Option<String>,
|
||||
/// Resolved property for fast O(1) ICU trie lookups at match time.
|
||||
pub resolved: Option<ResolvedProperty>,
|
||||
}
|
||||
|
||||
/// A simple character matcher for optimized greedy/lazy loops.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SimpleMatch {
|
||||
/// Any character (`.`), with dot_all flag.
|
||||
AnyChar { dot_all: bool },
|
||||
/// A single character.
|
||||
Char(u32),
|
||||
/// Case-insensitive character.
|
||||
CharNoCase(u32, u32),
|
||||
/// Character class (set of ranges), negated flag.
|
||||
CharClass {
|
||||
ranges: Vec<CharRange>,
|
||||
negated: bool,
|
||||
},
|
||||
/// Built-in class (\d, \w, \s, etc.)
|
||||
BuiltinClass(BuiltinCharacterClass),
|
||||
/// Unicode property (\p{...}, \P{...}).
|
||||
UnicodeProperty(Box<UnicodePropertyData>),
|
||||
}
|
||||
|
||||
/// A character range for CharClass instructions (u32 code points).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CharRange {
|
||||
pub start: u32,
|
||||
pub end: u32,
|
||||
}
|
||||
|
||||
/// Re-export for use by the compiler and VM.
|
||||
pub use crate::ast::BuiltinCharacterClass;
|
||||
|
||||
impl Default for Program {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Program {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
instructions: Vec::new(),
|
||||
capture_count: 0,
|
||||
register_count: 2, // group 0 always exists
|
||||
unicode: false,
|
||||
unicode_sets: false,
|
||||
ignore_case: false,
|
||||
multiline: false,
|
||||
dot_all: false,
|
||||
named_groups: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emit(&mut self, inst: Instruction) -> u32 {
|
||||
let idx = self.instructions.len() as u32;
|
||||
self.instructions.push(inst);
|
||||
idx
|
||||
}
|
||||
|
||||
pub fn current_offset(&self) -> u32 {
|
||||
self.instructions.len() as u32
|
||||
}
|
||||
|
||||
pub fn patch_jump(&mut self, at: u32, target: u32) {
|
||||
match &mut self.instructions[at as usize] {
|
||||
Instruction::Jump(t) => *t = target,
|
||||
Instruction::Split { prefer, .. } if *prefer == u32::MAX => *prefer = target,
|
||||
Instruction::Split { other, .. } if *other == u32::MAX => *other = target,
|
||||
inst => panic!("cannot patch non-jump instruction: {inst:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode a Unicode code point as WTF-16 into `out`.
|
||||
/// Returns `None` if the code point is out of range (> U+10FFFF).
|
||||
pub fn append_code_point_wtf16(out: &mut Vec<u16>, cp: u32) -> Option<()> {
|
||||
if cp <= 0xFFFF {
|
||||
out.push(cp as u16);
|
||||
return Some(());
|
||||
}
|
||||
if cp > 0x10FFFF {
|
||||
return None;
|
||||
}
|
||||
let cp = cp - 0x10000;
|
||||
out.push(0xD800 | ((cp >> 10) as u16));
|
||||
out.push(0xDC00 | ((cp & 0x3FF) as u16));
|
||||
Some(())
|
||||
}
|
||||
1056
Libraries/LibRegex/Rust/src/compiler.rs
Normal file
1056
Libraries/LibRegex/Rust/src/compiler.rs
Normal file
File diff suppressed because it is too large
Load diff
277
Libraries/LibRegex/Rust/src/ffi.rs
Normal file
277
Libraries/LibRegex/Rust/src/ffi.rs
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
/*
|
||||
* Copyright (c) 2026-present, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
/// C FFI for the Rust regex engine.
|
||||
///
|
||||
/// Provides a C-compatible API for use from C++ (LibJS, LibURL).
|
||||
/// All functions use `extern "C"` linkage and opaque pointer types.
|
||||
use crate::ast::Flags;
|
||||
use crate::regex::Regex;
|
||||
use std::slice;
|
||||
|
||||
/// Opaque regex handle.
|
||||
pub struct RustRegex(Regex);
|
||||
|
||||
/// Flags passed from C++.
|
||||
#[repr(C)]
|
||||
pub struct RustRegexFlags {
|
||||
pub global: bool,
|
||||
pub ignore_case: bool,
|
||||
pub multiline: bool,
|
||||
pub dot_all: bool,
|
||||
pub unicode: bool,
|
||||
pub unicode_sets: bool,
|
||||
pub sticky: bool,
|
||||
pub has_indices: bool,
|
||||
}
|
||||
|
||||
/// Compile a regex pattern. Returns an opaque handle, or null on error.
|
||||
/// On error, writes the error message to `error_out` and `error_len_out`.
|
||||
/// The caller must free the error string with `rust_regex_free_error`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `pattern` must be a valid pointer to `pattern_len` bytes of UTF-8.
|
||||
/// `error_out` and `error_len_out` must be valid pointers.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_regex_compile(
|
||||
pattern: *const u8,
|
||||
pattern_len: usize,
|
||||
flags: RustRegexFlags,
|
||||
error_out: *mut *const u8,
|
||||
error_len_out: *mut usize,
|
||||
) -> *mut RustRegex {
|
||||
if pattern.is_null() {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
let pattern_bytes = unsafe { slice::from_raw_parts(pattern, pattern_len) };
|
||||
let Ok(pattern_str) = std::str::from_utf8(pattern_bytes) else {
|
||||
return std::ptr::null_mut();
|
||||
};
|
||||
|
||||
let flags = Flags {
|
||||
global: flags.global,
|
||||
ignore_case: flags.ignore_case,
|
||||
multiline: flags.multiline,
|
||||
dot_all: flags.dot_all,
|
||||
unicode: flags.unicode,
|
||||
unicode_sets: flags.unicode_sets,
|
||||
sticky: flags.sticky,
|
||||
has_indices: flags.has_indices,
|
||||
};
|
||||
|
||||
match Regex::compile(pattern_str, flags) {
|
||||
Ok(regex) => Box::into_raw(Box::new(RustRegex(regex))),
|
||||
Err(e) => {
|
||||
if !error_out.is_null() && !error_len_out.is_null() {
|
||||
let msg = e.to_string();
|
||||
let leaked = msg.into_boxed_str();
|
||||
unsafe {
|
||||
*error_len_out = leaked.len();
|
||||
*error_out = Box::into_raw(leaked) as *const u8;
|
||||
}
|
||||
}
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Free an error string returned by `rust_regex_compile`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `error` must be a pointer returned via `rust_regex_compile`'s error_out, or null.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_regex_free_error(error: *mut u8, len: usize) {
|
||||
if !error.is_null() {
|
||||
drop(unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(error, len)) });
|
||||
}
|
||||
}
|
||||
|
||||
/// Free a compiled regex.
|
||||
///
|
||||
/// # Safety
|
||||
/// `regex` must be a valid pointer returned by `rust_regex_compile`, or null.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_regex_free(regex: *mut RustRegex) {
|
||||
if !regex.is_null() {
|
||||
drop(unsafe { Box::from_raw(regex) });
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a regex, writing captures into a caller-provided buffer.
|
||||
/// Returns 1 on match, 0 on no match, -1 on step limit exceeded.
|
||||
///
|
||||
/// # Safety
|
||||
/// - `regex` must be a valid pointer from `rust_regex_compile`.
|
||||
/// - `input` must point to `input_len` u16 code units.
|
||||
/// - `out_captures` must point to a buffer of at least `capture_count * 2` i32s,
|
||||
/// where `capture_count` is `rust_regex_capture_count(regex) + 1`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_regex_exec_into(
|
||||
regex: *const RustRegex,
|
||||
input: *const u16,
|
||||
input_len: usize,
|
||||
start_pos: usize,
|
||||
out_captures: *mut i32,
|
||||
out_capture_slots: u32,
|
||||
) -> i32 {
|
||||
use crate::vm::VmResult;
|
||||
if regex.is_null() || out_captures.is_null() {
|
||||
return 0;
|
||||
}
|
||||
let regex = unsafe { &*regex };
|
||||
let input = if input.is_null() {
|
||||
&[]
|
||||
} else {
|
||||
unsafe { slice::from_raw_parts(input, input_len) }
|
||||
};
|
||||
let out = unsafe { slice::from_raw_parts_mut(out_captures, out_capture_slots as usize) };
|
||||
match regex.0.exec_into(input, start_pos, out) {
|
||||
VmResult::Match => 1,
|
||||
VmResult::NoMatch => 0,
|
||||
VmResult::LimitExceeded => -1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Test whether a regex matches anywhere in the input.
|
||||
/// Returns 1 on match, 0 on no match, -1 on step limit exceeded.
|
||||
///
|
||||
/// # Safety
|
||||
/// Same requirements as `rust_regex_exec`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_regex_test(
|
||||
regex: *const RustRegex,
|
||||
input: *const u16,
|
||||
input_len: usize,
|
||||
start_pos: usize,
|
||||
) -> i32 {
|
||||
use crate::vm::VmResult;
|
||||
if regex.is_null() {
|
||||
return 0;
|
||||
}
|
||||
let regex = unsafe { &*regex };
|
||||
let input = if input.is_null() {
|
||||
&[]
|
||||
} else {
|
||||
unsafe { slice::from_raw_parts(input, input_len) }
|
||||
};
|
||||
match regex.0.test(input, start_pos) {
|
||||
VmResult::Match => 1,
|
||||
VmResult::NoMatch => 0,
|
||||
VmResult::LimitExceeded => -1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the number of capture groups (not counting group 0).
|
||||
///
|
||||
/// # Safety
|
||||
/// `regex` must be a valid pointer from `rust_regex_compile`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_regex_capture_count(regex: *const RustRegex) -> u32 {
|
||||
if regex.is_null() {
|
||||
return 0;
|
||||
}
|
||||
let regex = unsafe { &*regex };
|
||||
regex.0.capture_count()
|
||||
}
|
||||
|
||||
/// Find all non-overlapping matches and return a flat array of (start, end) i32 pairs.
|
||||
/// Returns the number of matches (NOT the number of i32s). The output buffer must have
|
||||
/// space for at least `max_matches * 2` i32s. Returns -1 if the buffer is too small.
|
||||
///
|
||||
/// # Safety
|
||||
/// - `regex` must be a valid pointer from `rust_regex_compile`.
|
||||
/// - `input` must point to `input_len` u16 code units.
|
||||
/// - `out` must point to at least `out_capacity` i32s.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_regex_find_all(
|
||||
regex: *const RustRegex,
|
||||
input: *const u16,
|
||||
input_len: usize,
|
||||
start_pos: usize,
|
||||
out: *mut i32,
|
||||
out_capacity: u32,
|
||||
) -> i32 {
|
||||
if regex.is_null() || out.is_null() {
|
||||
return 0;
|
||||
}
|
||||
let regex = unsafe { &*regex };
|
||||
let input = if input.is_null() {
|
||||
&[]
|
||||
} else {
|
||||
unsafe { slice::from_raw_parts(input, input_len) }
|
||||
};
|
||||
let out_slice = unsafe { slice::from_raw_parts_mut(out, out_capacity as usize) };
|
||||
regex.0.find_all_into(input, start_pos, out_slice)
|
||||
}
|
||||
|
||||
/// A named group entry returned across FFI.
|
||||
#[repr(C)]
|
||||
pub struct RustRegexNamedGroup {
|
||||
/// The group name as a UTF-8 string (pointer to Rust-owned memory).
|
||||
pub name: *const u8,
|
||||
/// Length of the name in bytes.
|
||||
pub name_len: usize,
|
||||
/// The 1-based capture group index.
|
||||
pub index: u32,
|
||||
}
|
||||
|
||||
/// Get the named capture groups. Returns an array of RustRegexNamedGroup.
|
||||
/// The caller must free the array (but NOT the name pointers) with `rust_regex_free_named_groups`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `regex` must be a valid pointer from `rust_regex_compile`.
|
||||
/// `out_count` will be set to the number of named groups.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_regex_get_named_groups(
|
||||
regex: *const RustRegex,
|
||||
out_count: *mut u32,
|
||||
) -> *mut RustRegexNamedGroup {
|
||||
if regex.is_null() || out_count.is_null() {
|
||||
if !out_count.is_null() {
|
||||
unsafe { *out_count = 0 };
|
||||
}
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
let regex = unsafe { &*regex };
|
||||
let groups = regex.0.named_groups();
|
||||
let count = groups.len();
|
||||
unsafe { *out_count = count as u32 };
|
||||
|
||||
if count == 0 {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
|
||||
let mut result: Vec<RustRegexNamedGroup> = Vec::with_capacity(count);
|
||||
for g in groups {
|
||||
// SAFETY: The name pointer borrows from the Regex's named_groups Vec, which
|
||||
// lives as long as the RustRegex handle. The C++ caller (RustRegex.cpp) copies
|
||||
// these name strings into its own m_named_groups map immediately, so the Rust
|
||||
// pointers are only used transiently before this function returns.
|
||||
result.push(RustRegexNamedGroup {
|
||||
name: g.name.as_ptr(),
|
||||
name_len: g.name.len(),
|
||||
index: g.index,
|
||||
});
|
||||
}
|
||||
|
||||
let boxed = result.into_boxed_slice();
|
||||
Box::into_raw(boxed) as *mut RustRegexNamedGroup
|
||||
}
|
||||
|
||||
/// Free the named groups array returned by `rust_regex_get_named_groups`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `groups` must be a pointer returned by `rust_regex_get_named_groups`, or null.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn rust_regex_free_named_groups(
|
||||
groups: *mut RustRegexNamedGroup,
|
||||
count: u32,
|
||||
) {
|
||||
if !groups.is_null() {
|
||||
let len = count as usize;
|
||||
drop(unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(groups, len)) });
|
||||
}
|
||||
}
|
||||
14
Libraries/LibRegex/Rust/src/lib.rs
Normal file
14
Libraries/LibRegex/Rust/src/lib.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright (c) 2026-present, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
pub mod ast;
|
||||
pub mod bytecode;
|
||||
pub mod compiler;
|
||||
pub mod ffi;
|
||||
pub mod parser;
|
||||
pub mod regex;
|
||||
mod unicode_ffi;
|
||||
pub mod vm;
|
||||
1767
Libraries/LibRegex/Rust/src/parser.rs
Normal file
1767
Libraries/LibRegex/Rust/src/parser.rs
Normal file
File diff suppressed because it is too large
Load diff
396
Libraries/LibRegex/Rust/src/regex.rs
Normal file
396
Libraries/LibRegex/Rust/src/regex.rs
Normal file
|
|
@ -0,0 +1,396 @@
|
|||
/*
|
||||
* Copyright (c) 2026-present, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
/// High-level regex API.
|
||||
///
|
||||
/// This is the main entry point for using the regex engine.
|
||||
use crate::ast::{Atom, Flags, Pattern};
|
||||
use crate::bytecode::{Instruction, NamedGroupEntry, append_code_point_wtf16};
|
||||
use crate::{compiler, parser, vm};
|
||||
use std::cell::RefCell;
|
||||
|
||||
/// A compiled regular expression.
|
||||
pub struct Regex {
|
||||
/// Program for the backtracking VM (with fused loop optimizations).
|
||||
program: crate::bytecode::Program,
|
||||
flags: Flags,
|
||||
hints: vm::PatternHints,
|
||||
/// Pre-computed u16 literal for whole-pattern literal search.
|
||||
literal_u16: Option<Vec<u16>>,
|
||||
/// Pre-computed u16 alternatives for fast literal alternation matching.
|
||||
/// Alternatives stay in source order to preserve leftmost-first semantics.
|
||||
literal_alt_u16: Option<Vec<Vec<u16>>>,
|
||||
/// Cached VM scratch space for reuse across exec calls.
|
||||
scratch: RefCell<vm::VmScratch>,
|
||||
}
|
||||
|
||||
impl Regex {
|
||||
/// Compile a regex pattern with the given flags.
|
||||
pub fn compile(pattern: &str, flags: Flags) -> Result<Self, parser::Error> {
|
||||
let parsed = parser::parse(pattern, flags)?;
|
||||
let mut program = compiler::compile(&parsed);
|
||||
Self::resolve_properties(&mut program);
|
||||
let hints = vm::analyze_pattern(&program);
|
||||
|
||||
let literal_u16 = extract_literal_u16(&parsed, flags);
|
||||
let literal_alt_u16 = extract_literal_alternatives_u16(&parsed, flags);
|
||||
|
||||
Ok(Self {
|
||||
program,
|
||||
flags,
|
||||
hints,
|
||||
literal_u16,
|
||||
literal_alt_u16,
|
||||
scratch: RefCell::new(vm::VmScratch::new()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Execute the regex, writing captures directly into a provided buffer.
|
||||
/// Returns a VmResult indicating match, no-match, or limit exceeded.
|
||||
/// The buffer should have `(capture_count + 1) * 2` i32 slots.
|
||||
/// On match, captures are written as pairs of (start, end) i32 values.
|
||||
pub fn exec_into(&self, input: &[u16], start: usize, out: &mut [i32]) -> vm::VmResult {
|
||||
// Fast path for literal patterns: use fast substring search.
|
||||
// NB: Literal searches never hit the step limit.
|
||||
if let Some(ref needle) = self.literal_u16 {
|
||||
return if Self::literal_search(input, start, needle, &self.flags, out) {
|
||||
vm::VmResult::Match
|
||||
} else {
|
||||
vm::VmResult::NoMatch
|
||||
};
|
||||
}
|
||||
// Fast path for literal alternation patterns.
|
||||
if let Some(ref alts) = self.literal_alt_u16 {
|
||||
return if Self::literal_alt_search(input, start, alts, out) {
|
||||
vm::VmResult::Match
|
||||
} else {
|
||||
vm::VmResult::NoMatch
|
||||
};
|
||||
}
|
||||
let scratch = &mut *self.scratch.borrow_mut();
|
||||
vm::execute_into_with_scratch(&self.program, input, start, &self.hints, out, scratch)
|
||||
}
|
||||
|
||||
/// Test whether the regex matches anywhere in the input.
|
||||
pub fn test(&self, input: &[u16], start: usize) -> vm::VmResult {
|
||||
if let Some(ref needle) = self.literal_u16 {
|
||||
return if Self::literal_test(input, start, needle, &self.flags) {
|
||||
vm::VmResult::Match
|
||||
} else {
|
||||
vm::VmResult::NoMatch
|
||||
};
|
||||
}
|
||||
if let Some(ref alts) = self.literal_alt_u16 {
|
||||
let mut out = [-1i32; 2];
|
||||
return if Self::literal_alt_search(input, start, alts, &mut out) {
|
||||
vm::VmResult::Match
|
||||
} else {
|
||||
vm::VmResult::NoMatch
|
||||
};
|
||||
}
|
||||
// Reuse cached scratch space for the VM. Only need group 0 for test().
|
||||
let mut out = [-1i32; 2];
|
||||
let scratch = &mut *self.scratch.borrow_mut();
|
||||
vm::execute_into_with_scratch(&self.program, input, start, &self.hints, &mut out, scratch)
|
||||
}
|
||||
|
||||
/// Fast literal substring search for whole-pattern literal fast paths.
|
||||
fn literal_search(
|
||||
input: &[u16],
|
||||
start: usize,
|
||||
needle: &[u16],
|
||||
flags: &Flags,
|
||||
out: &mut [i32],
|
||||
) -> bool {
|
||||
if needle.is_empty() {
|
||||
// Empty pattern matches at start position.
|
||||
if out.len() >= 2 {
|
||||
out[0] = start as i32;
|
||||
out[1] = start as i32;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if flags.ignore_case {
|
||||
// Case-insensitive literal search — fall back to linear scan with case folding.
|
||||
let needle_len = needle.len();
|
||||
if start + needle_len > input.len() {
|
||||
return false;
|
||||
}
|
||||
'outer: for pos in start..=input.len() - needle_len {
|
||||
for j in 0..needle_len {
|
||||
if !vm::case_fold_eq(
|
||||
input[pos + j] as u32,
|
||||
needle[j] as u32,
|
||||
flags.unicode || flags.unicode_sets,
|
||||
) {
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
if out.len() >= 2 {
|
||||
out[0] = pos as i32;
|
||||
out[1] = (pos + needle_len) as i32;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Case-sensitive: use fast first-character scan + verify.
|
||||
let first = needle[0];
|
||||
let needle_len = needle.len();
|
||||
if start + needle_len > input.len() {
|
||||
return false;
|
||||
}
|
||||
let mut pos = start;
|
||||
let end = input.len() - needle_len + 1;
|
||||
while pos < end {
|
||||
// Bulk scan for first character.
|
||||
match input[pos..end].iter().position(|&c| c == first) {
|
||||
Some(offset) => pos += offset,
|
||||
None => return false,
|
||||
}
|
||||
// Verify rest of needle.
|
||||
if input[pos..pos + needle_len] == *needle {
|
||||
if out.len() >= 2 {
|
||||
out[0] = pos as i32;
|
||||
out[1] = (pos + needle_len) as i32;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Fast literal test (no captures needed).
|
||||
fn literal_test(input: &[u16], start: usize, needle: &[u16], flags: &Flags) -> bool {
|
||||
let mut out = [0i32; 2];
|
||||
Self::literal_search(input, start, needle, flags, &mut out)
|
||||
}
|
||||
|
||||
/// Fast literal alternation search: find the first matching alternative.
|
||||
/// Alternatives are in source order to preserve ECMAScript leftmost-first semantics.
|
||||
fn literal_alt_search(input: &[u16], start: usize, alts: &[Vec<u16>], out: &mut [i32]) -> bool {
|
||||
for pos in start..input.len() {
|
||||
let first_ch = input[pos];
|
||||
for alt in alts {
|
||||
if alt[0] != first_ch {
|
||||
continue;
|
||||
}
|
||||
let alt_len = alt.len();
|
||||
if pos + alt_len > input.len() {
|
||||
continue;
|
||||
}
|
||||
if input[pos..pos + alt_len] == alt[..] {
|
||||
if out.len() >= 2 {
|
||||
out[0] = pos as i32;
|
||||
out[1] = (pos + alt_len) as i32;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Find all literal matches, writing (start, end) pairs into result_buf.
|
||||
fn literal_find_all(
|
||||
input: &[u16],
|
||||
start: usize,
|
||||
needle: &[u16],
|
||||
flags: &Flags,
|
||||
result_buf: &mut [i32],
|
||||
) -> i32 {
|
||||
let capacity = result_buf.len();
|
||||
let mut count = 0i32;
|
||||
let mut pos = start;
|
||||
let mut out = [-1i32; 2];
|
||||
loop {
|
||||
if pos > input.len() {
|
||||
break;
|
||||
}
|
||||
out[0] = -1;
|
||||
out[1] = -1;
|
||||
if !Self::literal_search(input, pos, needle, flags, &mut out) {
|
||||
break;
|
||||
}
|
||||
let idx = count as usize * 2;
|
||||
if idx + 1 >= capacity {
|
||||
return -1;
|
||||
}
|
||||
result_buf[idx] = out[0];
|
||||
result_buf[idx + 1] = out[1];
|
||||
count += 1;
|
||||
if out[1] == out[0] {
|
||||
pos = out[1] as usize + 1;
|
||||
} else {
|
||||
pos = out[1] as usize;
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
/// Find all literal alternation matches, writing (start, end) pairs into result_buf.
|
||||
fn literal_alt_find_all(
|
||||
input: &[u16],
|
||||
start: usize,
|
||||
alts: &[Vec<u16>],
|
||||
result_buf: &mut [i32],
|
||||
) -> i32 {
|
||||
let capacity = result_buf.len();
|
||||
let mut count = 0i32;
|
||||
let mut pos = start;
|
||||
let mut out = [-1i32; 2];
|
||||
loop {
|
||||
if pos > input.len() {
|
||||
break;
|
||||
}
|
||||
out[0] = -1;
|
||||
out[1] = -1;
|
||||
if !Self::literal_alt_search(input, pos, alts, &mut out) {
|
||||
break;
|
||||
}
|
||||
let idx = count as usize * 2;
|
||||
if idx + 1 >= capacity {
|
||||
return -1;
|
||||
}
|
||||
result_buf[idx] = out[0];
|
||||
result_buf[idx + 1] = out[1];
|
||||
count += 1;
|
||||
if out[1] == out[0] {
|
||||
pos = out[1] as usize + 1;
|
||||
} else {
|
||||
pos = out[1] as usize;
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
/// Post-process the program to resolve Unicode property names to ICU enum IDs.
|
||||
/// This allows O(1) trie lookups at match time instead of string-based resolution.
|
||||
fn resolve_properties(program: &mut crate::bytecode::Program) {
|
||||
let resolve = |data: &mut crate::bytecode::UnicodePropertyData| {
|
||||
if data.resolved.is_none() {
|
||||
data.resolved = compiler::resolve_property(&data.name, data.value.as_deref());
|
||||
}
|
||||
};
|
||||
for inst in &mut program.instructions {
|
||||
match inst {
|
||||
Instruction::UnicodeProperty(data) => {
|
||||
resolve(data);
|
||||
}
|
||||
Instruction::StringPropertyMatch { property, .. } => {
|
||||
resolve(property);
|
||||
}
|
||||
Instruction::GreedyLoop { matcher, .. } | Instruction::LazyLoop { matcher, .. } => {
|
||||
if let crate::bytecode::SimpleMatch::UnicodeProperty(data) = matcher {
|
||||
resolve(data);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the number of capture groups (not counting group 0).
|
||||
pub fn capture_count(&self) -> u32 {
|
||||
self.program.capture_count
|
||||
}
|
||||
|
||||
/// Get the named capture groups (in order of appearance in the pattern).
|
||||
pub fn named_groups(&self) -> &[NamedGroupEntry] {
|
||||
&self.program.named_groups
|
||||
}
|
||||
|
||||
/// Find all non-overlapping matches starting from `start`.
|
||||
/// Writes (match_start, match_end) i32 pairs directly into `result_buf`.
|
||||
/// Returns number of matches found, or -1 if buffer is too small.
|
||||
pub fn find_all_into(&self, input: &[u16], start: usize, result_buf: &mut [i32]) -> i32 {
|
||||
// Fast path for literal patterns.
|
||||
if let Some(ref needle) = self.literal_u16 {
|
||||
return Self::literal_find_all(input, start, needle, &self.flags, result_buf);
|
||||
}
|
||||
// Fast path for literal alternation patterns.
|
||||
if let Some(ref alts) = self.literal_alt_u16 {
|
||||
return Self::literal_alt_find_all(input, start, alts, result_buf);
|
||||
}
|
||||
// Use the VM-internal find_all loop which reuses a single VM across matches.
|
||||
let scratch = &mut *self.scratch.borrow_mut();
|
||||
vm::find_all_with_scratch(
|
||||
&self.program,
|
||||
input,
|
||||
start,
|
||||
&self.hints,
|
||||
result_buf,
|
||||
scratch,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_literal_u16(pattern: &Pattern, flags: Flags) -> Option<Vec<u16>> {
|
||||
if flags.ignore_case && (flags.unicode || flags.unicode_sets) {
|
||||
return None;
|
||||
}
|
||||
if pattern.disjunction.alternatives.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let alternative = &pattern.disjunction.alternatives[0];
|
||||
let mut literal = Vec::new();
|
||||
for term in &alternative.terms {
|
||||
if term.quantifier.is_some() {
|
||||
return None;
|
||||
}
|
||||
let Atom::Literal(cp) = term.atom else {
|
||||
return None;
|
||||
};
|
||||
if is_unsafe_unicode_literal(cp, flags) {
|
||||
return None;
|
||||
}
|
||||
append_code_point_wtf16(&mut literal, cp)?;
|
||||
}
|
||||
|
||||
Some(literal)
|
||||
}
|
||||
|
||||
fn extract_literal_alternatives_u16(pattern: &Pattern, flags: Flags) -> Option<Vec<Vec<u16>>> {
|
||||
if flags.ignore_case {
|
||||
return None;
|
||||
}
|
||||
|
||||
let alternatives = &pattern.disjunction.alternatives;
|
||||
if alternatives.len() < 2 || pattern.capture_count > 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut literal_alternatives = Vec::with_capacity(alternatives.len());
|
||||
for alternative in alternatives {
|
||||
let mut literal = Vec::new();
|
||||
for term in &alternative.terms {
|
||||
if term.quantifier.is_some() {
|
||||
return None;
|
||||
}
|
||||
let Atom::Literal(cp) = term.atom else {
|
||||
return None;
|
||||
};
|
||||
if is_unsafe_unicode_literal(cp, flags) {
|
||||
return None;
|
||||
}
|
||||
append_code_point_wtf16(&mut literal, cp)?;
|
||||
}
|
||||
if literal.is_empty() {
|
||||
return None;
|
||||
}
|
||||
literal_alternatives.push(literal);
|
||||
}
|
||||
|
||||
Some(literal_alternatives)
|
||||
}
|
||||
|
||||
fn is_unsafe_unicode_literal(cp: u32, flags: Flags) -> bool {
|
||||
(flags.unicode || flags.unicode_sets) && (0xD800..=0xDFFF).contains(&cp)
|
||||
}
|
||||
246
Libraries/LibRegex/Rust/src/unicode_ffi.rs
Normal file
246
Libraries/LibRegex/Rust/src/unicode_ffi.rs
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
/*
|
||||
* Copyright (c) 2026-present, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
use crate::bytecode::{PropertyKind, ResolvedProperty};
|
||||
|
||||
unsafe extern "C" {
|
||||
fn unicode_property_matches(
|
||||
code_point: u32,
|
||||
name_ptr: *const u8,
|
||||
name_len: usize,
|
||||
value_ptr: *const u8,
|
||||
value_len: usize,
|
||||
has_value: i32,
|
||||
) -> bool;
|
||||
|
||||
fn unicode_simple_case_fold(code_point: u32, unicode_mode: i32) -> u32;
|
||||
|
||||
fn unicode_code_point_matches_range_ignoring_case(
|
||||
code_point: u32,
|
||||
from: u32,
|
||||
to: u32,
|
||||
unicode_mode: i32,
|
||||
) -> i32;
|
||||
|
||||
fn unicode_property_matches_case_insensitive(
|
||||
code_point: u32,
|
||||
name_ptr: *const u8,
|
||||
name_len: usize,
|
||||
value_ptr: *const u8,
|
||||
value_len: usize,
|
||||
has_value: i32,
|
||||
) -> i32;
|
||||
|
||||
fn unicode_get_case_closure(code_point: u32, out_buffer: *mut u32, buffer_capacity: u32)
|
||||
-> u32;
|
||||
|
||||
fn unicode_is_string_property(name_ptr: *const u8, name_len: usize) -> i32;
|
||||
|
||||
fn unicode_property_all_case_equivalents_match(
|
||||
code_point: u32,
|
||||
name_ptr: *const u8,
|
||||
name_len: usize,
|
||||
value_ptr: *const u8,
|
||||
value_len: usize,
|
||||
has_value: i32,
|
||||
) -> i32;
|
||||
|
||||
fn unicode_resolved_property_matches(code_point: u32, kind: u8, id: u32) -> i32;
|
||||
|
||||
fn unicode_is_valid_ecma262_property(
|
||||
name_ptr: *const u8,
|
||||
name_len: usize,
|
||||
value_ptr: *const u8,
|
||||
value_len: usize,
|
||||
has_value: i32,
|
||||
) -> i32;
|
||||
|
||||
fn unicode_get_string_property_data(
|
||||
name_ptr: *const u8,
|
||||
name_len: usize,
|
||||
out: *mut u32,
|
||||
capacity: u32,
|
||||
) -> u32;
|
||||
|
||||
fn unicode_resolve_property(
|
||||
name_ptr: *const u8,
|
||||
name_len: usize,
|
||||
value_ptr: *const u8,
|
||||
value_len: usize,
|
||||
has_value: i32,
|
||||
out_kind: *mut u8,
|
||||
out_id: *mut u32,
|
||||
) -> i32;
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn optional_value_parts(value: Option<&str>) -> (*const u8, usize, i32) {
|
||||
match value {
|
||||
Some(value) => (value.as_ptr(), value.len(), 1),
|
||||
None => (std::ptr::null(), 0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn property_matches(code_point: u32, name: &str, value: Option<&str>) -> bool {
|
||||
let (value_ptr, value_len, has_value) = optional_value_parts(value);
|
||||
// SAFETY: `name` and `value` remain valid for the duration of this call,
|
||||
// and the C++ helper only reads through those pointers.
|
||||
unsafe {
|
||||
unicode_property_matches(
|
||||
code_point,
|
||||
name.as_ptr(),
|
||||
name.len(),
|
||||
value_ptr,
|
||||
value_len,
|
||||
has_value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn simple_case_fold(code_point: u32, unicode_mode: bool) -> u32 {
|
||||
// SAFETY: This forwards only scalar values to the C++ helper.
|
||||
unsafe { unicode_simple_case_fold(code_point, if unicode_mode { 1 } else { 0 }) }
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn code_point_matches_range_ignoring_case(
|
||||
code_point: u32,
|
||||
from: u32,
|
||||
to: u32,
|
||||
unicode_mode: bool,
|
||||
) -> bool {
|
||||
// SAFETY: This forwards only scalar values to the C++ helper.
|
||||
unsafe {
|
||||
unicode_code_point_matches_range_ignoring_case(
|
||||
code_point,
|
||||
from,
|
||||
to,
|
||||
if unicode_mode { 1 } else { 0 },
|
||||
) != 0
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn property_matches_case_insensitive(
|
||||
code_point: u32,
|
||||
name: &str,
|
||||
value: Option<&str>,
|
||||
) -> bool {
|
||||
let (value_ptr, value_len, has_value) = optional_value_parts(value);
|
||||
// SAFETY: `name` and `value` remain valid for the duration of this call,
|
||||
// and the C++ helper only reads through those pointers.
|
||||
unsafe {
|
||||
unicode_property_matches_case_insensitive(
|
||||
code_point,
|
||||
name.as_ptr(),
|
||||
name.len(),
|
||||
value_ptr,
|
||||
value_len,
|
||||
has_value,
|
||||
) != 0
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_case_closure(code_point: u32, out_buffer: &mut [u32]) -> usize {
|
||||
let capacity = out_buffer.len().min(u32::MAX as usize) as u32;
|
||||
// SAFETY: `out_buffer` is writable for `capacity` elements and the C++
|
||||
// helper writes at most that many entries.
|
||||
unsafe { unicode_get_case_closure(code_point, out_buffer.as_mut_ptr(), capacity) as usize }
|
||||
}
|
||||
|
||||
pub(crate) fn is_string_property(name: &str) -> bool {
|
||||
// SAFETY: `name` remains valid for the duration of this call and the C++
|
||||
// helper only reads through that pointer.
|
||||
unsafe { unicode_is_string_property(name.as_ptr(), name.len()) != 0 }
|
||||
}
|
||||
|
||||
pub(crate) fn property_all_case_equivalents_match(
|
||||
code_point: u32,
|
||||
name: &str,
|
||||
value: Option<&str>,
|
||||
) -> bool {
|
||||
let (value_ptr, value_len, has_value) = optional_value_parts(value);
|
||||
// SAFETY: `name` and `value` remain valid for the duration of this call,
|
||||
// and the C++ helper only reads through those pointers.
|
||||
unsafe {
|
||||
unicode_property_all_case_equivalents_match(
|
||||
code_point,
|
||||
name.as_ptr(),
|
||||
name.len(),
|
||||
value_ptr,
|
||||
value_len,
|
||||
has_value,
|
||||
) != 0
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn resolved_property_matches(code_point: u32, property: ResolvedProperty) -> bool {
|
||||
// SAFETY: This forwards only scalar values to the C++ helper.
|
||||
unsafe { unicode_resolved_property_matches(code_point, property.kind as u8, property.id) != 0 }
|
||||
}
|
||||
|
||||
pub(crate) fn is_valid_ecma262_property(name: &str, value: Option<&str>) -> bool {
|
||||
let (value_ptr, value_len, has_value) = optional_value_parts(value);
|
||||
// SAFETY: `name` and `value` remain valid for the duration of this call,
|
||||
// and the C++ helper only reads through those pointers.
|
||||
unsafe {
|
||||
unicode_is_valid_ecma262_property(
|
||||
name.as_ptr(),
|
||||
name.len(),
|
||||
value_ptr,
|
||||
value_len,
|
||||
has_value,
|
||||
) != 0
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_string_property_data(name: &str) -> Vec<u32> {
|
||||
// SAFETY: Passing a null output buffer with zero capacity is the API's
|
||||
// documented query mode, and `name` remains valid during the call.
|
||||
let needed = unsafe {
|
||||
unicode_get_string_property_data(name.as_ptr(), name.len(), std::ptr::null_mut(), 0)
|
||||
};
|
||||
if needed == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut buffer = vec![0u32; needed as usize];
|
||||
// SAFETY: `buffer` has room for `needed` elements and the C++ helper
|
||||
// writes at most that many u32 values.
|
||||
let written = unsafe {
|
||||
unicode_get_string_property_data(name.as_ptr(), name.len(), buffer.as_mut_ptr(), needed)
|
||||
};
|
||||
if written == 0 || written > needed {
|
||||
return Vec::new();
|
||||
}
|
||||
buffer.truncate(written as usize);
|
||||
buffer
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_property(name: &str, value: Option<&str>) -> Option<ResolvedProperty> {
|
||||
let (value_ptr, value_len, has_value) = optional_value_parts(value);
|
||||
let mut kind: u8 = 0;
|
||||
let mut id: u32 = 0;
|
||||
// SAFETY: `name` and `value` remain valid for the duration of this call,
|
||||
// and `kind`/`id` are valid out-pointers to local storage.
|
||||
let ok = unsafe {
|
||||
unicode_resolve_property(
|
||||
name.as_ptr(),
|
||||
name.len(),
|
||||
value_ptr,
|
||||
value_len,
|
||||
has_value,
|
||||
&mut kind,
|
||||
&mut id,
|
||||
)
|
||||
};
|
||||
if ok != 0 {
|
||||
PropertyKind::from_u8(kind).map(|kind| ResolvedProperty { kind, id })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
3043
Libraries/LibRegex/Rust/src/vm.rs
Normal file
3043
Libraries/LibRegex/Rust/src/vm.rs
Normal file
File diff suppressed because it is too large
Load diff
531
Libraries/LibRegex/RustRegex.cpp
Normal file
531
Libraries/LibRegex/RustRegex.cpp
Normal file
|
|
@ -0,0 +1,531 @@
|
|||
/*
|
||||
* Copyright (c) 2026-present, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibRegex/RustRegex.h>
|
||||
|
||||
#ifdef ENABLE_RUST
|
||||
|
||||
# include <LibUnicode/CharacterTypes.h>
|
||||
|
||||
// Forward declarations for C++ functions called from Rust.
|
||||
extern "C" {
|
||||
bool unicode_property_matches(uint32_t, unsigned char const*, size_t, unsigned char const*, size_t, int);
|
||||
uint32_t unicode_simple_case_fold(uint32_t, int);
|
||||
int unicode_code_point_matches_range_ignoring_case(uint32_t, uint32_t, uint32_t, int);
|
||||
int unicode_property_matches_case_insensitive(uint32_t, unsigned char const*, size_t, unsigned char const*, size_t, int);
|
||||
int unicode_property_all_case_equivalents_match(uint32_t, unsigned char const*, size_t, unsigned char const*, size_t, int);
|
||||
unsigned int unicode_get_case_closure(uint32_t, uint32_t*, unsigned int);
|
||||
int unicode_is_string_property(unsigned char const*, size_t);
|
||||
int unicode_is_valid_ecma262_property(unsigned char const*, size_t, unsigned char const*, size_t, int);
|
||||
uint32_t unicode_get_string_property_data(unsigned char const*, size_t, uint32_t*, uint32_t);
|
||||
int unicode_resolve_property(unsigned char const*, size_t, unsigned char const*, size_t, int, unsigned char*, uint32_t*);
|
||||
int unicode_resolved_property_matches(uint32_t, unsigned char, uint32_t);
|
||||
}
|
||||
|
||||
extern "C" bool unicode_property_matches(
|
||||
uint32_t code_point,
|
||||
unsigned char const* name_ptr, size_t name_len,
|
||||
unsigned char const* value_ptr, size_t value_len,
|
||||
int has_value)
|
||||
{
|
||||
auto name = StringView { reinterpret_cast<char const*>(name_ptr), name_len };
|
||||
auto value = has_value
|
||||
? StringView { reinterpret_cast<char const*>(value_ptr), value_len }
|
||||
: StringView {};
|
||||
|
||||
// If there's a value, the name is a category key like "Script", "General_Category", etc.
|
||||
if (has_value) {
|
||||
// Script or Script_Extensions
|
||||
if (name.is_one_of("sc"sv, "Script"sv, "scx"sv, "Script_Extensions"sv)) {
|
||||
auto script = Unicode::script_from_string(value);
|
||||
if (!script.has_value())
|
||||
return false;
|
||||
if (name.is_one_of("sc"sv, "Script"sv))
|
||||
return Unicode::code_point_has_script(code_point, *script);
|
||||
return Unicode::code_point_has_script_extension(code_point, *script);
|
||||
}
|
||||
// General_Category
|
||||
if (name.is_one_of("gc"sv, "General_Category"sv)) {
|
||||
auto category = Unicode::general_category_from_string(value);
|
||||
if (!category.has_value())
|
||||
return false;
|
||||
return Unicode::code_point_has_general_category(code_point, *category);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// No value: could be a property name, general category, or script.
|
||||
// Try as a binary property first.
|
||||
auto prop = Unicode::property_from_string(name);
|
||||
if (prop.has_value())
|
||||
return Unicode::code_point_has_property(code_point, *prop);
|
||||
|
||||
// Try as a general category.
|
||||
auto category = Unicode::general_category_from_string(name);
|
||||
if (category.has_value())
|
||||
return Unicode::code_point_has_general_category(code_point, *category);
|
||||
|
||||
// Try as a script.
|
||||
auto script = Unicode::script_from_string(name);
|
||||
if (script.has_value())
|
||||
return Unicode::code_point_has_script(code_point, *script);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Property kind constants (must match Rust ResolvedProperty::kind).
|
||||
enum ResolvedPropertyKind : uint8_t {
|
||||
Script = 0,
|
||||
ScriptExtension = 1,
|
||||
GeneralCategory = 2,
|
||||
BinaryProperty = 3,
|
||||
};
|
||||
|
||||
/// Resolve a Unicode property name/value pair to a (kind, id) pair.
|
||||
/// Returns 1 on success, 0 if the property is not recognized.
|
||||
extern "C" int unicode_resolve_property(
|
||||
unsigned char const* name_ptr, size_t name_len,
|
||||
unsigned char const* value_ptr, size_t value_len,
|
||||
int has_value,
|
||||
unsigned char* out_kind, uint32_t* out_id)
|
||||
{
|
||||
auto name = StringView { reinterpret_cast<char const*>(name_ptr), name_len };
|
||||
auto value = has_value
|
||||
? StringView { reinterpret_cast<char const*>(value_ptr), value_len }
|
||||
: StringView {};
|
||||
|
||||
if (has_value) {
|
||||
if (name.is_one_of("sc"sv, "Script"sv, "scx"sv, "Script_Extensions"sv)) {
|
||||
auto script = Unicode::script_from_string(value);
|
||||
if (!script.has_value())
|
||||
return 0;
|
||||
*out_kind = name.is_one_of("scx"sv, "Script_Extensions"sv)
|
||||
? ResolvedPropertyKind::ScriptExtension
|
||||
: ResolvedPropertyKind::Script;
|
||||
*out_id = script->value();
|
||||
return 1;
|
||||
}
|
||||
if (name.is_one_of("gc"sv, "General_Category"sv)) {
|
||||
auto category = Unicode::general_category_from_string(value);
|
||||
if (!category.has_value())
|
||||
return 0;
|
||||
*out_kind = ResolvedPropertyKind::GeneralCategory;
|
||||
*out_id = category->value();
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// No value: try binary property, general category, script.
|
||||
auto prop = Unicode::property_from_string(name);
|
||||
if (prop.has_value()) {
|
||||
*out_kind = ResolvedPropertyKind::BinaryProperty;
|
||||
*out_id = prop->value();
|
||||
return 1;
|
||||
}
|
||||
auto category = Unicode::general_category_from_string(name);
|
||||
if (category.has_value()) {
|
||||
*out_kind = ResolvedPropertyKind::GeneralCategory;
|
||||
*out_id = category->value();
|
||||
return 1;
|
||||
}
|
||||
auto script = Unicode::script_from_string(name);
|
||||
if (script.has_value()) {
|
||||
*out_kind = ResolvedPropertyKind::Script;
|
||||
*out_id = script->value();
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Check if a code point matches a resolved property. Direct ICU trie lookup, no string parsing.
|
||||
extern "C" int unicode_resolved_property_matches(uint32_t code_point, unsigned char kind, uint32_t id)
|
||||
{
|
||||
switch (kind) {
|
||||
case ResolvedPropertyKind::Script:
|
||||
return Unicode::code_point_has_script(code_point, Unicode::Script { id }) ? 1 : 0;
|
||||
case ResolvedPropertyKind::ScriptExtension:
|
||||
return Unicode::code_point_has_script_extension(code_point, Unicode::Script { id }) ? 1 : 0;
|
||||
case ResolvedPropertyKind::GeneralCategory:
|
||||
return Unicode::code_point_has_general_category(code_point, Unicode::GeneralCategory { id }) ? 1 : 0;
|
||||
case ResolvedPropertyKind::BinaryProperty:
|
||||
return Unicode::code_point_has_property(code_point, Unicode::Property { id }) ? 1 : 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" uint32_t unicode_simple_case_fold(uint32_t code_point, int unicode_mode)
|
||||
{
|
||||
return Unicode::canonicalize(code_point, unicode_mode != 0);
|
||||
}
|
||||
|
||||
extern "C" int unicode_code_point_matches_range_ignoring_case(uint32_t code_point, uint32_t from, uint32_t to, int unicode_mode)
|
||||
{
|
||||
return Unicode::code_point_matches_range_ignoring_case(code_point, from, to, unicode_mode != 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
// Check if a code point matches a Unicode property, considering case closure.
|
||||
// Returns 1 if the code point or any of its case equivalents has the property.
|
||||
extern "C" int unicode_property_matches_case_insensitive(
|
||||
uint32_t code_point,
|
||||
unsigned char const* name_ptr, size_t name_len,
|
||||
unsigned char const* value_ptr, size_t value_len,
|
||||
int has_value)
|
||||
{
|
||||
// First check the code point itself.
|
||||
auto name = StringView { reinterpret_cast<char const*>(name_ptr), name_len };
|
||||
auto value = has_value
|
||||
? StringView { reinterpret_cast<char const*>(value_ptr), value_len }
|
||||
: StringView {};
|
||||
|
||||
auto check_property = [&](u32 cp) -> bool {
|
||||
if (has_value) {
|
||||
if (name.is_one_of("sc"sv, "Script"sv, "scx"sv, "Script_Extensions"sv)) {
|
||||
auto script = Unicode::script_from_string(value);
|
||||
if (!script.has_value())
|
||||
return false;
|
||||
if (name.is_one_of("sc"sv, "Script"sv))
|
||||
return Unicode::code_point_has_script(cp, *script);
|
||||
return Unicode::code_point_has_script_extension(cp, *script);
|
||||
}
|
||||
if (name.is_one_of("gc"sv, "General_Category"sv)) {
|
||||
auto category = Unicode::general_category_from_string(value);
|
||||
if (!category.has_value())
|
||||
return false;
|
||||
return Unicode::code_point_has_general_category(cp, *category);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
auto prop = Unicode::property_from_string(name);
|
||||
if (prop.has_value())
|
||||
return Unicode::code_point_has_property(cp, *prop);
|
||||
auto category = Unicode::general_category_from_string(name);
|
||||
if (category.has_value())
|
||||
return Unicode::code_point_has_general_category(cp, *category);
|
||||
auto script = Unicode::script_from_string(name);
|
||||
if (script.has_value())
|
||||
return Unicode::code_point_has_script(cp, *script);
|
||||
return false;
|
||||
};
|
||||
|
||||
// Check using case closure — all case-equivalent code points.
|
||||
bool found = false;
|
||||
Unicode::for_each_case_folded_code_point(code_point, [&](u32 cp) {
|
||||
if (check_property(cp)) {
|
||||
found = true;
|
||||
return IterationDecision::Break;
|
||||
}
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
return found ? 1 : 0;
|
||||
}
|
||||
|
||||
// Check if ALL case-equivalents of a code point have the property.
|
||||
// Returns 1 only if every case-equivalent has the property.
|
||||
// Used for u-flag negated case-insensitive: \P{X}/iu matches if NOT all have X.
|
||||
extern "C" int unicode_property_all_case_equivalents_match(
|
||||
uint32_t code_point,
|
||||
unsigned char const* name_ptr, size_t name_len,
|
||||
unsigned char const* value_ptr, size_t value_len,
|
||||
int has_value)
|
||||
{
|
||||
auto name = StringView { reinterpret_cast<char const*>(name_ptr), name_len };
|
||||
auto value = has_value
|
||||
? StringView { reinterpret_cast<char const*>(value_ptr), value_len }
|
||||
: StringView {};
|
||||
|
||||
auto check_property = [&](u32 cp) -> bool {
|
||||
if (has_value) {
|
||||
if (name.is_one_of("sc"sv, "Script"sv, "scx"sv, "Script_Extensions"sv)) {
|
||||
auto script = Unicode::script_from_string(value);
|
||||
if (!script.has_value())
|
||||
return false;
|
||||
if (name.is_one_of("sc"sv, "Script"sv))
|
||||
return Unicode::code_point_has_script(cp, *script);
|
||||
return Unicode::code_point_has_script_extension(cp, *script);
|
||||
}
|
||||
if (name.is_one_of("gc"sv, "General_Category"sv)) {
|
||||
auto category = Unicode::general_category_from_string(value);
|
||||
if (!category.has_value())
|
||||
return false;
|
||||
return Unicode::code_point_has_general_category(cp, *category);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
auto prop = Unicode::property_from_string(name);
|
||||
if (prop.has_value())
|
||||
return Unicode::code_point_has_property(cp, *prop);
|
||||
auto category = Unicode::general_category_from_string(name);
|
||||
if (category.has_value())
|
||||
return Unicode::code_point_has_general_category(cp, *category);
|
||||
auto script = Unicode::script_from_string(name);
|
||||
if (script.has_value())
|
||||
return Unicode::code_point_has_script(cp, *script);
|
||||
return false;
|
||||
};
|
||||
|
||||
bool all_match = true;
|
||||
Unicode::for_each_case_folded_code_point(code_point, [&](u32 cp) {
|
||||
if (!check_property(cp)) {
|
||||
all_match = false;
|
||||
return IterationDecision::Break;
|
||||
}
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
return all_match ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" unsigned int unicode_get_case_closure(
|
||||
uint32_t code_point,
|
||||
uint32_t* out_buffer,
|
||||
unsigned int buffer_capacity)
|
||||
{
|
||||
unsigned int count = 0;
|
||||
Unicode::for_each_case_folded_code_point(code_point, [&](u32 cp) {
|
||||
if (count < buffer_capacity) {
|
||||
out_buffer[count] = cp;
|
||||
count++;
|
||||
}
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
extern "C" int unicode_is_string_property(
|
||||
unsigned char const* name_ptr, size_t name_len)
|
||||
{
|
||||
auto name = StringView { reinterpret_cast<char const*>(name_ptr), name_len };
|
||||
auto prop = Unicode::property_from_string(name);
|
||||
if (!prop.has_value())
|
||||
return 0;
|
||||
return Unicode::is_ecma262_string_property(*prop) ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" int unicode_is_valid_ecma262_property(
|
||||
unsigned char const* name_ptr, size_t name_len,
|
||||
unsigned char const* value_ptr, size_t value_len,
|
||||
int has_value)
|
||||
{
|
||||
auto name = StringView { reinterpret_cast<char const*>(name_ptr), name_len };
|
||||
auto value = has_value
|
||||
? StringView { reinterpret_cast<char const*>(value_ptr), value_len }
|
||||
: StringView {};
|
||||
|
||||
if (has_value) {
|
||||
// Key=Value form: Script, Script_Extensions, General_Category
|
||||
if (name.is_one_of("sc"sv, "Script"sv, "scx"sv, "Script_Extensions"sv))
|
||||
return Unicode::script_from_string(value).has_value() ? 1 : 0;
|
||||
if (name.is_one_of("gc"sv, "General_Category"sv))
|
||||
return Unicode::general_category_from_string(value).has_value() ? 1 : 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Lone name: try as ECMA-262 binary property or General_Category value.
|
||||
// Note: Script names (e.g. "Hiragana") are NOT valid as lone names per
|
||||
// ECMA-262 -- they require Script= or sc= prefix.
|
||||
auto prop = Unicode::property_from_string(name);
|
||||
if (prop.has_value())
|
||||
return (Unicode::is_ecma262_property(*prop) || Unicode::is_ecma262_string_property(*prop)) ? 1 : 0;
|
||||
if (Unicode::general_category_from_string(name).has_value())
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Get all multi-codepoint strings for a Unicode string property.
|
||||
/// Writes packed data: [string_count, len1, cp1_0, cp1_1, ..., len2, cp2_0, ...]
|
||||
/// Returns total number of u32 values written.
|
||||
/// If out is null, returns the total size needed.
|
||||
extern "C" uint32_t unicode_get_string_property_data(
|
||||
unsigned char const* name_ptr, size_t name_len,
|
||||
uint32_t* out, uint32_t capacity)
|
||||
{
|
||||
auto name = StringView { reinterpret_cast<char const*>(name_ptr), name_len };
|
||||
auto prop = Unicode::property_from_string(name);
|
||||
if (!prop.has_value() || !Unicode::is_ecma262_string_property(*prop))
|
||||
return 0;
|
||||
|
||||
auto strings = Unicode::get_property_strings(*prop);
|
||||
|
||||
// Filter to multi-codepoint strings and compute total size needed.
|
||||
// Format: [count, len1, cp1..., len2, cp2..., ...]
|
||||
Vector<Vector<u32>> multi_cp_strings;
|
||||
for (auto const& str : strings) {
|
||||
Vector<u32> code_points;
|
||||
for (auto cp : str.code_points())
|
||||
code_points.append(cp);
|
||||
if (code_points.size() > 1)
|
||||
multi_cp_strings.append(move(code_points));
|
||||
}
|
||||
|
||||
// Calculate total size: 1 (count) + sum(1 + len) for each string
|
||||
uint32_t total_size = 1;
|
||||
for (auto const& cps : multi_cp_strings)
|
||||
total_size += 1 + static_cast<uint32_t>(cps.size());
|
||||
|
||||
if (!out || capacity < total_size)
|
||||
return total_size;
|
||||
|
||||
uint32_t offset = 0;
|
||||
out[offset++] = static_cast<uint32_t>(multi_cp_strings.size());
|
||||
for (auto const& cps : multi_cp_strings) {
|
||||
out[offset++] = static_cast<uint32_t>(cps.size());
|
||||
for (auto cp : cps)
|
||||
out[offset++] = cp;
|
||||
}
|
||||
|
||||
return total_size;
|
||||
}
|
||||
|
||||
namespace regex {
|
||||
|
||||
ErrorOr<CompiledRustRegex, String> CompiledRustRegex::compile(StringView pattern, RustRegexFlags flags)
|
||||
{
|
||||
unsigned char const* error_ptr = nullptr;
|
||||
size_t error_len = 0;
|
||||
|
||||
auto* regex = rust_regex_compile(
|
||||
reinterpret_cast<unsigned char const*>(pattern.characters_without_null_termination()),
|
||||
pattern.length(),
|
||||
flags,
|
||||
&error_ptr,
|
||||
&error_len);
|
||||
if (!regex) {
|
||||
String error_message = "Invalid pattern"_string;
|
||||
if (error_ptr) {
|
||||
error_message = MUST(String::from_utf8({ reinterpret_cast<char const*>(error_ptr), error_len }));
|
||||
rust_regex_free_error(const_cast<unsigned char*>(error_ptr), error_len);
|
||||
}
|
||||
return error_message;
|
||||
}
|
||||
|
||||
CompiledRustRegex result(regex);
|
||||
|
||||
unsigned int group_count = 0;
|
||||
auto* groups = rust_regex_get_named_groups(regex, &group_count);
|
||||
if (groups) {
|
||||
result.m_named_groups.ensure_capacity(group_count);
|
||||
for (unsigned int i = 0; i < group_count; ++i) {
|
||||
auto name = String::from_utf8({ reinterpret_cast<char const*>(groups[i].name), groups[i].name_len });
|
||||
result.m_named_groups.append(RustNamedCaptureGroup { MUST(name), groups[i].index });
|
||||
}
|
||||
rust_regex_free_named_groups(groups, group_count);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
CompiledRustRegex::~CompiledRustRegex()
|
||||
{
|
||||
if (m_regex)
|
||||
rust_regex_free(m_regex);
|
||||
}
|
||||
|
||||
CompiledRustRegex::CompiledRustRegex(CompiledRustRegex&& other)
|
||||
: m_regex(other.m_regex)
|
||||
, m_named_groups(move(other.m_named_groups))
|
||||
, m_u16_buffer(move(other.m_u16_buffer))
|
||||
, m_capture_buffer(move(other.m_capture_buffer))
|
||||
, m_capture_count(other.m_capture_count)
|
||||
, m_capture_count_cached(other.m_capture_count_cached)
|
||||
, m_find_all_buffer(move(other.m_find_all_buffer))
|
||||
{
|
||||
other.m_regex = nullptr;
|
||||
other.m_capture_count = 0;
|
||||
other.m_capture_count_cached = false;
|
||||
}
|
||||
|
||||
CompiledRustRegex& CompiledRustRegex::operator=(CompiledRustRegex&& other)
|
||||
{
|
||||
if (this != &other) {
|
||||
if (m_regex)
|
||||
rust_regex_free(m_regex);
|
||||
m_regex = other.m_regex;
|
||||
m_named_groups = move(other.m_named_groups);
|
||||
m_u16_buffer = move(other.m_u16_buffer);
|
||||
m_capture_buffer = move(other.m_capture_buffer);
|
||||
m_capture_count = other.m_capture_count;
|
||||
m_capture_count_cached = other.m_capture_count_cached;
|
||||
m_find_all_buffer = move(other.m_find_all_buffer);
|
||||
other.m_regex = nullptr;
|
||||
other.m_capture_count = 0;
|
||||
other.m_capture_count_cached = false;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
CompiledRustRegex::CompiledRustRegex(RustRegex* regex)
|
||||
: m_regex(regex)
|
||||
{
|
||||
}
|
||||
|
||||
unsigned short const* CompiledRustRegex::get_u16_data(Utf16View input, size_t& out_len) const
|
||||
{
|
||||
out_len = input.length_in_code_units();
|
||||
if (input.has_ascii_storage()) {
|
||||
auto const* source = input.ascii_span().data();
|
||||
m_u16_buffer.resize(out_len);
|
||||
for (size_t i = 0; i < out_len; ++i)
|
||||
m_u16_buffer[i] = static_cast<u16>(source[i]);
|
||||
return m_u16_buffer.data();
|
||||
}
|
||||
return reinterpret_cast<unsigned short const*>(input.utf16_span().data());
|
||||
}
|
||||
|
||||
int CompiledRustRegex::exec_internal(Utf16View input, size_t start_pos) const
|
||||
{
|
||||
size_t len;
|
||||
auto* data = get_u16_data(input, len);
|
||||
|
||||
if (!m_capture_count_cached) {
|
||||
m_capture_count = rust_regex_capture_count(m_regex) + 1;
|
||||
m_capture_count_cached = true;
|
||||
}
|
||||
auto slots = m_capture_count * 2;
|
||||
m_capture_buffer.resize(slots);
|
||||
|
||||
return rust_regex_exec_into(m_regex, data, len, start_pos, m_capture_buffer.data(), slots);
|
||||
}
|
||||
|
||||
unsigned int CompiledRustRegex::total_groups() const
|
||||
{
|
||||
if (!m_capture_count_cached) {
|
||||
m_capture_count = rust_regex_capture_count(m_regex) + 1;
|
||||
m_capture_count_cached = true;
|
||||
}
|
||||
return m_capture_count;
|
||||
}
|
||||
|
||||
int CompiledRustRegex::test(Utf16View input, size_t start_pos) const
|
||||
{
|
||||
size_t len;
|
||||
auto* data = get_u16_data(input, len);
|
||||
return rust_regex_test(m_regex, data, len, start_pos);
|
||||
}
|
||||
|
||||
int CompiledRustRegex::find_all(Utf16View input, size_t start_pos) const
|
||||
{
|
||||
size_t len;
|
||||
auto* data = get_u16_data(input, len);
|
||||
// Start with reasonable capacity; keep doubling until it fits.
|
||||
if (m_find_all_buffer.size() < 256)
|
||||
m_find_all_buffer.resize(256);
|
||||
for (;;) {
|
||||
int result = rust_regex_find_all(m_regex, data, len, start_pos, m_find_all_buffer.data(), m_find_all_buffer.size());
|
||||
if (result != -1)
|
||||
return result;
|
||||
m_find_all_buffer.resize(m_find_all_buffer.size() * 2);
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int CompiledRustRegex::capture_count() const
|
||||
{
|
||||
return rust_regex_capture_count(m_regex);
|
||||
}
|
||||
|
||||
} // namespace regex
|
||||
|
||||
#endif // ENABLE_RUST
|
||||
83
Libraries/LibRegex/RustRegex.h
Normal file
83
Libraries/LibRegex/RustRegex.h
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright (c) 2026-present, the Ladybird developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef ENABLE_RUST
|
||||
|
||||
# include <AK/Error.h>
|
||||
# include <AK/Noncopyable.h>
|
||||
# include <AK/String.h>
|
||||
# include <AK/Utf16View.h>
|
||||
# include <AK/Vector.h>
|
||||
# include <LibRegex/Export.h>
|
||||
# include <RustFFI.h>
|
||||
|
||||
namespace regex {
|
||||
|
||||
struct RustNamedCaptureGroup {
|
||||
String name;
|
||||
unsigned int index;
|
||||
};
|
||||
|
||||
class REGEX_API CompiledRustRegex {
|
||||
AK_MAKE_NONCOPYABLE(CompiledRustRegex);
|
||||
|
||||
public:
|
||||
static ErrorOr<CompiledRustRegex, String> compile(StringView pattern, RustRegexFlags flags);
|
||||
|
||||
~CompiledRustRegex();
|
||||
CompiledRustRegex(CompiledRustRegex&& other);
|
||||
CompiledRustRegex& operator=(CompiledRustRegex&& other);
|
||||
|
||||
/// Execute into internal capture buffer. Returns 1 on match, 0 on no match, -1 on limit exceeded.
|
||||
/// After a successful call, read results via capture_slot().
|
||||
int exec_internal(Utf16View input, size_t start_pos) const;
|
||||
/// Read a capture slot from the internal buffer (after exec_internal).
|
||||
/// Even slots are start positions, odd slots are end positions.
|
||||
/// Returns -1 for unmatched captures.
|
||||
int capture_slot(unsigned int slot) const { return m_capture_buffer[slot]; }
|
||||
/// Test for a match. Returns 1 on match, 0 on no match, -1 on limit exceeded.
|
||||
int test(Utf16View input, size_t start_pos = 0) const;
|
||||
unsigned int capture_count() const;
|
||||
/// Total number of capture groups including group 0.
|
||||
unsigned int total_groups() const;
|
||||
|
||||
/// Find all non-overlapping matches. Returns number of matches found.
|
||||
/// Results are written as (start, end) i32 pairs to the internal find_all buffer.
|
||||
/// Access results via find_all_match(i) after calling.
|
||||
int find_all(Utf16View input, size_t start_pos) const;
|
||||
/// Get the i-th match from find_all results. Returns (start, end).
|
||||
struct MatchPair {
|
||||
int start;
|
||||
int end;
|
||||
};
|
||||
MatchPair find_all_match(int index) const { return { m_find_all_buffer[index * 2], m_find_all_buffer[index * 2 + 1] }; }
|
||||
|
||||
Vector<RustNamedCaptureGroup> const& named_groups() const { return m_named_groups; }
|
||||
|
||||
private:
|
||||
explicit CompiledRustRegex(RustRegex* regex);
|
||||
|
||||
/// Get u16 data pointer and length from a Utf16View.
|
||||
/// For ASCII storage, widens to u16 using the cached buffer.
|
||||
unsigned short const* get_u16_data(Utf16View input, size_t& out_len) const;
|
||||
|
||||
RustRegex* m_regex { nullptr };
|
||||
Vector<RustNamedCaptureGroup> m_named_groups;
|
||||
/// Reusable buffer for ASCII→u16 widening, avoiding per-call allocation.
|
||||
mutable Vector<u16> m_u16_buffer;
|
||||
/// Pre-allocated buffer for capture results to avoid per-exec allocation.
|
||||
mutable Vector<int> m_capture_buffer;
|
||||
mutable unsigned int m_capture_count { 0 };
|
||||
mutable bool m_capture_count_cached { false };
|
||||
/// Buffer for find_all results.
|
||||
mutable Vector<int> m_find_all_buffer;
|
||||
};
|
||||
|
||||
} // namespace regex
|
||||
|
||||
#endif // ENABLE_RUST
|
||||
|
|
@ -69,7 +69,7 @@ function assertArrayEquals(expected, actual) {
|
|||
expect(actual).toEqual(expected);
|
||||
}
|
||||
|
||||
test.xfail("es6/regexp-constructor", () => {
|
||||
test("es6/regexp-constructor", () => {
|
||||
"use strict";
|
||||
|
||||
function should_not_be_called() {
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ function assertArrayEquals(expected, actual) {
|
|||
expect(actual).toEqual(expected);
|
||||
}
|
||||
|
||||
test.xfail("es6/unicode-escapes-in-regexps", () => {
|
||||
test("es6/unicode-escapes-in-regexps", () => {
|
||||
function testRegexpHelper(r) {
|
||||
assertTrue(r.test("foo"));
|
||||
assertTrue(r.test("boo"));
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ function assertArrayEquals(expected, actual) {
|
|||
expect(actual).toEqual(expected);
|
||||
}
|
||||
|
||||
test.xfail("es6/unicode-regexp-backrefs", () => {
|
||||
test("es6/unicode-regexp-backrefs", () => {
|
||||
function replace(string) {
|
||||
return string.replace(/L/g, "\ud800").replace(/l/g, "\ud801").replace(/T/g, "\udc00").replace(/\./g, "[^]");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ function assertArrayEquals(expected, actual) {
|
|||
expect(actual).toEqual(expected);
|
||||
}
|
||||
|
||||
test.xfail("es6/unicode-regexp-restricted-syntax", () => {
|
||||
test("es6/unicode-regexp-restricted-syntax", () => {
|
||||
assertThrows("/\\1/u", SyntaxError);
|
||||
// test262/language/literals/regexp/u-invalid-char-range-a
|
||||
assertThrows("/[\\w-a]/u", SyntaxError);
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ function assertArrayEquals(expected, actual) {
|
|||
expect(actual).toEqual(expected);
|
||||
}
|
||||
|
||||
test.xfail("es6/unicode-regexp-zero-length", () => {
|
||||
test("es6/unicode-regexp-zero-length", () => {
|
||||
var L = "\ud800";
|
||||
var T = "\udc00";
|
||||
var x = "x";
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ function assertArrayEquals(expected, actual) {
|
|||
expect(actual).toEqual(expected);
|
||||
}
|
||||
|
||||
test.xfail("regexp-UC16", () => {
|
||||
test("regexp-UC16", () => {
|
||||
assertEquals("x\u03a3\u03c3x,\u03a3", String(/x(.)\1x/i.exec("x\u03a3\u03c3x")), "backref-UC16");
|
||||
assertFalse(/x(...)\1/i.test("x\u03a3\u03c2\u03c3\u03c2\u03c3"), "\\1 ASCII, string short");
|
||||
assertTrue(/\u03a3((?:))\1\1x/i.test("\u03c2x"), "backref-UC16-empty");
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ function assertArrayEquals(expected, actual) {
|
|||
expect(actual).toEqual(expected);
|
||||
}
|
||||
|
||||
test.xfail("regexp-lookahead", () => {
|
||||
test("regexp-lookahead", () => {
|
||||
function stringEscape(string) {
|
||||
// Converts string to source literal.
|
||||
return '"' + string.replace(/["\\]/g, "\\$1") + '"';
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ function assertArrayEquals(expected, actual) {
|
|||
expect(actual).toEqual(expected);
|
||||
}
|
||||
|
||||
test.xfail("regexp-sort", () => {
|
||||
test("regexp-sort", () => {
|
||||
function Test(lower, upper) {
|
||||
var lx = lower + "x";
|
||||
var ux = upper + "x";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
test.xfail("backreferences", () => {
|
||||
test("backreferences", () => {
|
||||
// WebKit assertion compatibility shim for Ladybird's test-js harness
|
||||
|
||||
function description(msg) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
test.xfail("dotstar", () => {
|
||||
test("dotstar", () => {
|
||||
// WebKit assertion compatibility shim for Ladybird's test-js harness
|
||||
|
||||
function description(msg) {
|
||||
|
|
|
|||
15
Tests/LibJS/Runtime/3rdparty/webkit/overflow.js
vendored
15
Tests/LibJS/Runtime/3rdparty/webkit/overflow.js
vendored
|
|
@ -1,4 +1,4 @@
|
|||
test.xfail("overflow", () => {
|
||||
test("overflow", () => {
|
||||
// WebKit assertion compatibility shim for Ladybird's test-js harness
|
||||
|
||||
function description(msg) {
|
||||
|
|
@ -55,13 +55,8 @@ test.xfail("overflow", () => {
|
|||
var regexp3 = new RegExp(s3, "");
|
||||
shouldBe("regexp3.exec(s3)", "null");
|
||||
|
||||
shouldThrow(
|
||||
"function f() { /[^a$]{18446744073709551615}/ }",
|
||||
'"SyntaxError: Invalid regular expression: number too large in {} quantifier"'
|
||||
);
|
||||
|
||||
shouldThrow(
|
||||
"new RegExp('((?=$))??(?:\\\\1){1180591620717411303423,}')",
|
||||
'"SyntaxError: Invalid regular expression: number too large in {} quantifier"'
|
||||
);
|
||||
// Large quantifier values are saturated rather than rejected (matching V8 behavior
|
||||
// and the test262 quantifier-integer-limit test which requires accepting 2^53-1).
|
||||
shouldNotThrow("function f() { /[^a$]{18446744073709551615}/ }");
|
||||
shouldNotThrow("new RegExp('((?=$))??(?:\\\\1){1180591620717411303423,}')");
|
||||
});
|
||||
|
|
|
|||
11
Tests/LibJS/Runtime/3rdparty/webkit/slow.js
vendored
11
Tests/LibJS/Runtime/3rdparty/webkit/slow.js
vendored
|
|
@ -1,4 +1,4 @@
|
|||
test.xfail("slow", () => {
|
||||
test("slow", () => {
|
||||
// WebKit assertion compatibility shim for Ladybird's test-js harness
|
||||
|
||||
function description(msg) {
|
||||
|
|
@ -47,5 +47,12 @@ test.xfail("slow", () => {
|
|||
"Test for expressions that would hang when evaluated due to exponential matching behavior. If the test does not hang it is a success."
|
||||
);
|
||||
|
||||
shouldBe('/(?:[^(?!)]||){23}z/.test("/(?:[^(?!)]||){23}z/")', "false");
|
||||
// This pattern triggers exponential backtracking. The engine may either
|
||||
// return false (if it completes within the step limit) or throw an
|
||||
// InternalError (if the backtrack limit is exceeded). Both are correct.
|
||||
try {
|
||||
shouldBe('/(?:[^(?!)]||){23}z/.test("/(?:[^(?!)]||){23}z/")', "false");
|
||||
} catch (e) {
|
||||
expect(e).toBeInstanceOf(InternalError);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -75,6 +75,13 @@ test("v flag should enable unicode mode", () => {
|
|||
expect(re.test("a\u{10FFFF}")).toBe(true);
|
||||
});
|
||||
|
||||
test("v flag empty character classes", () => {
|
||||
expect(/[]/v.test("a")).toBeFalse();
|
||||
expect("a".match(/[^]/v)).toEqual(["a"]);
|
||||
expect("\n".match(/[^]/v)).toEqual(["\n"]);
|
||||
expect("foo".match(/[^]+?/v)).toEqual(["f"]);
|
||||
});
|
||||
|
||||
test("parsing a large bytestring shouldn't crash", () => {
|
||||
RegExp(new Uint8Array(0x40000));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,17 +2,16 @@ Harness status: OK
|
|||
|
||||
Found 17 tests
|
||||
|
||||
13 Pass
|
||||
4 Fail
|
||||
17 Pass
|
||||
Pass Alternations are tried left to right, with no backtracking into a lookbehind
|
||||
Pass Back-references
|
||||
Fail Back-references to captures inside the lookbehind
|
||||
Fail Capturing matches
|
||||
Fail Captures inside negative lookbehind
|
||||
Pass Back-references to captures inside the lookbehind
|
||||
Pass Capturing matches
|
||||
Pass Captures inside negative lookbehind
|
||||
Pass Do not backtrack into a lookbehind
|
||||
Pass Greedy loop
|
||||
Pass Miscellaneous
|
||||
Fail Mutual recursive capture/back references
|
||||
Pass Mutual recursive capture/back references
|
||||
Pass Negative lookbehinds
|
||||
Pass Nested lookaround
|
||||
Pass Simple fixed-length matches
|
||||
|
|
|
|||
Loading…
Reference in a new issue