Skip to content

[codex] Fix dense symbol map offsets - #3465

Closed
bobzhang wants to merge 2 commits into
mainfrom
codex/fix-symbol-map-dense-offset
Closed

[codex] Fix dense symbol map offsets#3465
bobzhang wants to merge 2 commits into
mainfrom
codex/fix-symbol-map-dense-offset

Conversation

@bobzhang

Copy link
Copy Markdown
Contributor

Summary

Fix dense symbol-map finalization for non-zero lower bounds in bytes/internal/regex_engine/symbol_map.

Dense tables are allocated as ub - lb + 1, but the old implementation indexed them with absolute Rechar values. Calling finalize(10, 12) could therefore index position 10 in a length-3 table and trap. The dense table now stores lb, writes entries at c - lb, and maps with the same offset.

Validation

  • moon fmt bytes/internal/regex_engine/symbol_map
  • moon info bytes/internal/regex_engine/symbol_map
  • moon test bytes/internal/regex_engine/symbol_map
  • moon test bytes/internal/regex_engine
  • moon check
  • moon test

@coveralls

coveralls commented Apr 24, 2026

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 4214

Coverage decreased (-0.001%) to 94.613%

Details

  • Coverage decreased (-0.001%) from the base build.
  • Patch coverage: 1 uncovered change across 1 file (10 of 11 lines covered, 90.91%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
string/internal/regex_engine/symbol_map/symbol_map.mbt 3 2 66.67%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 15668
Covered Lines: 14824
Line Coverage: 94.61%
Coverage Strength: 219014.93 hits per line

💛 - Coveralls

@bobzhang
bobzhang requested a review from hackwaly April 24, 2026 10:14
@hackwaly

Copy link
Copy Markdown
Contributor

The lb is redundant indeed. There are only two known profiles, so assume lb is 0 is safe. If store lb in dense table, it will hurt performance.

@bobzhang

bobzhang commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

@hackwaly You're right that lb is empirically always 0 — I checked all Profile(...) constructions across the repo:

  • bytes/regex_profile.mbt:18valid = char_range(0, 255)
  • bytes/internal/regex_engine/regex_engine_test.mbt:16,320..0x10FFFF, 0..0x7F
  • string/regex_profile.mbt:22,36 — same shape
  • string/internal/regex_engine/regex_engine_test.mbt:16 and string/internal/regex_parser/parser_test.mbt:17,36

Eight profiles, all with lb = 0. And Profile::Profile is only re-exported through bytes/internal/regex_engine and string/internal/regex_enginebytes/pkg.generated.mbti and string/pkg.generated.mbti don't expose it, so external users of moonbitlang/core can't construct a profile with non-zero lb either. So the invariant holds from inside the crate.

On perf: the lookup at dense_table.mbt:19 runs once per input byte through execute.mbt. This PR adds one subtraction plus a field load. Real, but I'd want a benchmark before treating it as decisive — the dominant cost in regex execution is the state-machine work and memory traffic, not arithmetic on a single index.

Where I'd push back: the configuration that produced this bug is exactly "finalize(lb, ub) accepts lb as a parameter, but ignores it and requires it to be 0." Reviewer attention is the only thing keeping lb=0 true; nothing in the type system enforces it. If we accept this PR's approach, we pay the per-byte subtraction. If we reject it on perf grounds, we should at minimum remove lb from finalize's signature so the assumption is enforced by the API rather than by convention. Keeping a parameter you intend to ignore is the trap.

Three options I see, in increasing order of structural safety:

  1. Status quo + comment — fastest, but the next contributor adding a profile with lb≠0 (UTF-16 surrogate range, non-ASCII script) will reintroduce this bug silently. The compiler won't help them.
  2. This PR — correct under any lb, costs one sub per lookup. Defensive but pays for a generality that, per your point, no caller actually uses.
  3. Drop lb from finalize and DenseTable — fastest and makes the invariant unbypassable. finalize(ub : Rechar) and DenseTable(FixedArray[Rechar]), indexed absolutely. Anyone who tries to construct a profile starting above 0 has to update this API, which is the right place for that conversation to happen.

I lean toward (3): it gets your perf back and removes the footgun. If you'd prefer (1) for now I can document the invariant explicitly with an assert in finalize (debug-only or always-on), but I'd rather not leave a parametric API that quietly requires a specific argument value.

Happy to rework either way — what's your call?

@hackwaly

hackwaly commented May 5, 2026

Copy link
Copy Markdown
Contributor

I agree 3) is best.

@hackwaly
hackwaly force-pushed the codex/fix-symbol-map-dense-offset branch from 6af08cc to e1704e2 Compare May 6, 2026 04:53
@hackwaly
hackwaly marked this pull request as ready for review May 6, 2026 04:55
Copilot AI review requested due to automatic review settings May 6, 2026 04:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adjusts regex-engine symbol-map finalization to avoid dense-table out-of-bounds indexing by changing finalize to work with absolute Rechar indices (dropping the explicit lower-bound parameter) and updating call sites accordingly.

Changes:

  • Changed SymbolMap::finalize (bytes + string) signature from (lb, ub) to (ub) and updated compile call sites.
  • Updated bytes symbol-map dense/sparse construction logic (dense allocation and interval iteration) to use absolute indexing.
  • Added a bytes symbol-map unit test around dense-table absolute indexing behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
string/internal/regex_engine/symbol_map/symbol_map.mbt Drops lb from finalize and changes representative (repr) construction.
string/internal/regex_engine/symbol_map/pkg.generated.mbti Regenerates API signature for SymbolMap::finalize.
string/internal/regex_engine/compile.mbt Updates symbol-map finalization call to pass only ub.
bytes/internal/regex_engine/symbol_map/symbol_map.mbt Changes dense/sparse selection and dense table construction to use absolute indexing.
bytes/internal/regex_engine/symbol_map/symbol_map_test.mbt Adds test asserting dense-table absolute indexing behavior.
bytes/internal/regex_engine/symbol_map/pkg.generated.mbti Regenerates API signature for SymbolMap::finalize.
bytes/internal/regex_engine/compile.mbt Updates symbol-map finalization call to pass only ub.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +60 to 64
let table = FixedArray::make(ub + 1, 0)
self.each_intervals(0, ub, (lo, hi) => {
let symbol = repr.length()
repr.push(lo)
for c in lo..<=hi {
Comment on lines +45 to +49
if ub < 1024 {
let (table, repr) = self.finalize_dense(ub)
(table as &Table, repr)
} else {
let (table, repr) = self.finalize_sparse(lb, ub)
let (table, repr) = self.finalize_sparse(ub)
let symbol_map = @symbol_map.new()
symbolize(profile~, symbol_map~, ast)
let (symbol_table, symbol_repr) = symbol_map.finalize(profile.lb, profile.ub)
let (symbol_table, symbol_repr) = symbol_map.finalize(profile.ub)
Comment on lines 48 to 52
self.each_intervals(0, ub, (lo, hi) => {
let symbol = repr.length()
repr.push(Int::max(lo, lb))
repr.push(lo)
for c in lo..<=hi {
table[c] = symbol
Comment on lines +16 to +23
test "dense table uses absolute indexes" {
let symbol_map = @symbol_map.new()
symbol_map.split(@shared_types.RecharSet::char(11))
let (table, repr) = symbol_map.finalize(12)
assert_eq(repr.length(), 3)
assert_eq(repr[0], 0)
assert_eq(repr[1], 11)
assert_eq(repr[2], 12)
@hackwaly

hackwaly commented May 6, 2026

Copy link
Copy Markdown
Contributor

I'll file a new PR for 3).

@hackwaly hackwaly closed this May 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants