Skip to content

Fix non-ASCII characters breaking unquoted sheet names and names - #52

Open
gthb wants to merge 5 commits into
borgar:masterfrom
gthb:fix-highchar-splits-unquoted-name
Open

Fix non-ASCII characters breaking unquoted sheet names and names#52
gthb wants to merge 5 commits into
borgar:masterfrom
gthb:fix-highchar-splits-unquoted-name

Conversation

@gthb

@gthb gthb commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

What

Fix three ways an unquoted name or sheet name containing a non-ASCII character lexes wrongly.

  • A sheet name starting with a non-ASCII character swallows the rest of the reference: =Ærið!A1 came back as one range_named token spanning the whole string, ! and range included, instead of a context, an operator and a range. =Ærið:Ärger!A1 went the same way.
  • A name starting with an ASCII character breaks at its first non-ASCII one: =Forudsætninger!A1 came back as Foruds followed by a separate ætninger!A1.
  • U+00B4 (´) is refused outright: it cannot start a name (=´!A1 lexed as an unknown token) and it ends one it appears inside (=a´!A1 likewise), while its neighbours U+00B3 and U+00B5 are both accepted.

Downstream, parse threw Invalid syntax on =SUM(Ærið!A1:B2) and Unexpected unknown token on =´!A1, and returned kind: "name" where kind: "range" was meant. A writer that emits sheet names with non-ASCII characters got back a formula referencing nothing.

Cause

lexNameFuncCntx reads a per-character allow-mask out of ALLOWED, falling back to OK_HIGHCHAR for characters past the table's end. Two separate faults are in that one expression.

The continuation loop's s > 180 ? OK_HIGHCHAR : ALLOWED[c - OFFS] tests the token's first character s but indexes with the current character c. Once a name started with a high character, every character after it took the OK_HIGHCHAR branch — ! included — so the loop never stopped. When the name starts with an ASCII character the same expression fails the other way: the lookup runs off the end of the table for a high character, reads undefined, and ends the token there.

ALLOWED has one entry per code point from U+0020 through U+00B3, and the guard admitted only what is beyond U+00B4, leaving that one character in neither. Every code unit from U+00B4 up is allowed exactly where a high character is — which is what OK_HIGHCHAR encodes — so the boundary belongs at U+00B4, not past it.

Fix

Read the mask off c, and move the cut-off to >= 180 in both the loop and the first-character test above it. Each of the three edits has a test that fails without it.

Compatibility

Lexing changes only for input containing a code unit at or above U+00B4, and only from a wrong result to a right one: a sweep of every UTF-16 code unit across a dozen syntactic positions found nothing below U+00B4 that changes, and no input that gains an unknown token. Consumers that switch on token type will see range_named become range (or context + ! + range) for such sheet references, and a name ending in a high character arrive as one token rather than two.

Relation to the other branches

Supersedes #50 (closed); #51 contains the same two lines among unrelated sheet-range work and drops them once this lands.

gthb added 2 commits July 15, 2026 21:54
Fix bug: in `lexNameFuncCntx` the continuation loop tested the token's first
character `s` against the high-char range (`s > 180`) while indexing `ALLOWED`
with the current character `c`.

For a name that starts with an ASCII character, `s > 180` is false, so a later
high character (e.g. æ, code 230) indexed `ALLOWED` out of bounds, resulting in
`undefined`, treated as invalid, and the name split at that character.

For example, `Forudsætninger!A1` tokenized as `Foruds` + `ætninger!A1` instead
of one reference. Names starting with a high character (`Ærø`) were unaffected
because `s > 180` was true.

Test the current character `c` against the high-char range, mirroring the
start-character test above.
A formula whose sheet name starts above the character table lexed as one name
token, losing the "!" and the reference behind it: "Ærið!A1" came back whole.
The previous commit fixed that by reading the mask off the character in hand;
this covers the character the range boundary left out.

ALLOWED holds one entry per code point from U+0020 up to and including U+00B3,
and past it every character is allowed wherever a high one is, which is what
OK_HIGHCHAR says. The guard admitted only what is beyond U+00B4, so that one
character was in neither: it read as undefined out of the table, which is no
mask at all. It refused to start a name ("´!A1" lexed as an unknown token) and
ended one it appeared inside ("a´!A1" likewise). By the grammar the table is
generated from it is a plain name character, and its neighbours U+00B3 and
U+00B5 are both taken, so the guard now takes it too.
These pass without the mask fix as well: the bug needs the token's first
character to be high, and a bracketed prefix starts at "[", so the faulty
`s > 180` test reads false and the loop happens to behave. They pin that
the bracketed path stays unaffected, and carry over the coverage from the
branch closed in favour of this one.
gthb added 2 commits July 31, 2026 17:12
It explains why the cut-off moved rather than what the code does, which is
the PR's job, not the file's. The bound itself needs no gloss: `ALLOWED` is
declared `new Uint8Array(180 - OFFS)` a few lines up.
The bracketed-prefix cases do not reach the allow-mask at all: a token
opening with "[" is handed straight to lexContextUnquoted, which matches
high characters with its own character class, so the name lexer's
continuation loop never runs. Say that, rather than the reverse, and say
what the cases are for.

In the U+00B4 case, "it was in neither" had nothing to refer back to
outside the lexer source.
@gthb
gthb marked this pull request as ready for review July 31, 2026 17:18
@gthb gthb changed the title Fix a non-ASCII sheet name swallowing the rest of the reference Fix non-ASCII characters breaking unquoted sheet names and names Jul 31, 2026
gthb added a commit to gthb/fx that referenced this pull request Jul 31, 2026
"Ærið:Ärger!A1" is one sheet range here and two endpoints in borgar#52, which
carries the same mask fix without sheet ranges. Both assertions meet when
borgar#52 lands and master merges in, and git reports no conflict because they
sit in different parts of the file, so the failure would otherwise read
as broken sheet-range lexing. The comment says which one survives.
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.

1 participant