Fix: digit-leading generalized-identifier segment mislexed as numeric literal - #416
Open
acinep wants to merge 1 commit into
Open
Fix: digit-leading generalized-identifier segment mislexed as numeric literal#416acinep wants to merge 1 commit into
acinep wants to merge 1 commit into
Conversation
Author
|
@microsoft-github-policy-service agree |
acinep
force-pushed
the
pr/digit-leading-identifier-lexing
branch
from
September 10, 2026 22:38
a74bda3 to
a880d5c
Compare
… literal
A run of digits inside an unquoted [fieldName] selector, immediately
preceded by whitespace and followed by "." then a non-digit identifier
character (e.g. [Maintenance vendors 20230329.Department]), was
mislexed: readNumericLiteral greedily consumed the digit run as a
NumericLiteral token, leaving a bare "." that tokenizeDefault has no
valid interpretation for and throws on. TaskUtils.tryLexParse (the
entry point actually used by consumers) surfaces this as a Lex-stage
error via errorLineMap, even though Lexer.tryLex + Lexer.trySnapshot
called directly happen to still succeed.
This is a real-world pattern, not a contrived edge case: it's exactly
the naming convention Table.ExpandTableColumn itself generates for an
expanded/merged column ("<other query's display name>.<column>"), so
any M document with a merge/expand step whose other query's display
name ends in digits right before the generated "." hits this.
Fix: when a digit-first token's matched numeric span is immediately
followed by "." then a non-digit identifier-part character (the one
sequence a genuine numeric literal can never produce, since a decimal
point is always followed by more digits), re-read the whole span as an
identifier instead - mirroring the digit-tolerant continuation that
identifier tokens starting with a letter already get via
IdentifierUtils.getIdentifierLength.
Verified against the full existing lexer/parser test suite (399 -> 402
library tests, 102 resource tests) with zero regressions, plus the two
real-world repro cases.
acinep
force-pushed
the
pr/digit-leading-identifier-lexing
branch
from
September 10, 2026 22:43
a880d5c to
c2ba7eb
Compare
acinep
marked this pull request as ready for review
September 10, 2026 22:44
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A run of digits inside an unquoted
[fieldName]selector, immediately preceded by whitespace and immediately followed by.then a non-digit identifier character (e.g.[Maintenance vendors 20000101.Department]), was mislexed.readNumericLiteralgreedily consumed the digit run as aNumericLiteraltoken, leaving a bare.thattokenizeDefaulthas no valid interpretation for and throws on.TaskUtils.tryLexParse- the entry point most consumers actually use - surfaces this as aLex-stage error viaerrorLineMap, even though callingLexer.tryLex+Lexer.trySnapshotdirectly on the identical text happens to still succeed (that inconsistency is what made this tricky to pin down).This is a real-world pattern, not a contrived edge case: it's exactly the naming convention
Table.ExpandTableColumnitself generates for an expanded/merged column ("<other query's display name>.<column name>"), so any M document with a merge/expand step where the other query's display name ends in digits immediately before the generated.hits this.Repro
Fix
When a digit-first token's matched numeric span (
Pattern.Numeric) is immediately followed by.then a non-digit identifier-part character - the one sequence a genuine numeric literal can never produce, since a decimal point is always followed by more digits - re-read the whole span as an identifier instead. This mirrors the digit-tolerant continuation that identifier tokens starting with a letter already get viaIdentifierUtils.getIdentifierLength(e.g.[Foo Bar.Baz]already lexed correctly before this change; only the digit-leading case was missing the equivalent handling).readNumericLiteralis left untouched and still used for the one call site where a leading.is unambiguous (.5-style literals - M identifiers never begin with a literal dot, so there's no ambiguity to resolve there).Testing
lexSimple.test.tscovering: a bare digit-leading identifier segment, one inside a full field selector alongside an ordinaryIdentifier/Equal/etc. token stream, and confirmation that ordinary numeric literals (including ones immediately followed by more digits) are unaffected.npm test(399 → 402 passing, 1 pending, unchanged) andnpm run test:resources(102 passing) both green with zero regressions.