Skip to content

Fix: digit-leading generalized-identifier segment mislexed as numeric literal - #416

Open
acinep wants to merge 1 commit into
microsoft:masterfrom
acinep:pr/digit-leading-identifier-lexing
Open

Fix: digit-leading generalized-identifier segment mislexed as numeric literal#416
acinep wants to merge 1 commit into
microsoft:masterfrom
acinep:pr/digit-leading-identifier-lexing

Conversation

@acinep

@acinep acinep commented Sep 10, 2026

Copy link
Copy Markdown

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. 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 most consumers actually use - surfaces this as a Lex-stage error via errorLineMap, even though calling Lexer.tryLex + Lexer.trySnapshot directly 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.ExpandTableColumn itself 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

import * as PQP from "@microsoft/powerquery-parser";

const result = await PQP.TaskUtils.tryLexParse(PQP.DefaultSettings, `let x = [Maintenance vendors 20000101.Department] in x`);
// Before this fix: stage=Lex, resultKind=Error, error="Error on line(s): 0"
// After this fix:  stage=Parse, resultKind=Ok

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 via IdentifierUtils.getIdentifierLength (e.g. [Foo Bar.Baz] already lexed correctly before this change; only the digit-leading case was missing the equivalent handling).

readNumericLiteral is 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

  • Added 3 new tests to lexSimple.test.ts covering: a bare digit-leading identifier segment, one inside a full field selector alongside an ordinary Identifier/Equal/etc. token stream, and confirmation that ordinary numeric literals (including ones immediately followed by more digits) are unaffected.
  • Full existing suite: npm test (399 → 402 passing, 1 pending, unchanged) and npm run test:resources (102 passing) both green with zero regressions.
  • Verified against the two real-world M queries that surfaced this (via a downstream consumer, not included here) - both now lex and parse successfully end-to-end.

@acinep

acinep commented Sep 10, 2026

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@acinep
acinep force-pushed the pr/digit-leading-identifier-lexing branch from a74bda3 to a880d5c Compare September 10, 2026 22:38
… 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
acinep force-pushed the pr/digit-leading-identifier-lexing branch from a880d5c to c2ba7eb Compare September 10, 2026 22:43
@acinep
acinep marked this pull request as ready for review September 10, 2026 22:44
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