Skip to content

[JuliaSyntax] Stop emitting K".." and K"..." in lexer#61992

Merged
Keno merged 1 commit into
JuliaLang:masterfrom
KenoAIStaging:kf/syntax-dots
Jun 3, 2026
Merged

[JuliaSyntax] Stop emitting K".." and K"..." in lexer#61992
Keno merged 1 commit into
JuliaLang:masterfrom
KenoAIStaging:kf/syntax-dots

Conversation

@Keno

@Keno Keno commented Jun 3, 2026

Copy link
Copy Markdown
Member

[This is a forward port of https://github.com/JuliaLang/JuliaSyntax.jl/pull/573, which was approved there, but didn't make the move - I'm not currently working on this code, but I'd like to get my open PRs worked in before everything becomes too stale. Rebased and pendubg review comments addressed by Claude. Original Description Follows]

Unfortunately, the sequences .. and ... do not always refer to the .. operator or the ... syntax. There are two and a half cases where they don't:

  1. After @ in macrocall, where they are both regular identifiers
  2. In import ...A where the dots specify the level
  3. :(...) treats ... as quoted identifier

Case 1 was handled in a previous commit by lexing these as identifiers after 2.

However, as a result of case 2, it is problematic to tokenize these dots together; we essentially have to untokenize them in the import parser. It is also infeasible to change the lexer to have speical context-sensitive lexing in import, because there could be arbitrary interpolations, @eval import A, $(f(x..y)), ..b, so deciding whether a particular .. after import refers to the operator or a level specifier requires the parser.

Currently the parser handles this by splitting the obtained tokens again in the import parser, but this is undesirable, because it invalidates the invariant that the tokens produced by the lexer correspond to the non-terminals of the final parse tree.

This PR attempts to address this by only ever having the lexer emit K"." and having the parser decide which case it refers to. The new non-terminal K"dots" handles the identifier cases (ordinary .. and quoted :(...) ). K"..." is now exclusively used for splat/slurp, and is no longer used in its non-terminal form for case 3.

(cherry picked from commit 6e81eb1c70dd664a9bd888f13cf0b4b4e443f8fa)

@Keno

Keno commented Jun 3, 2026

Copy link
Copy Markdown
Member Author

There is one more PR in this stack, which I will put up once this one merges to save CI time.

@Keno

Keno commented Jun 3, 2026

Copy link
Copy Markdown
Member Author

(There was an open question on the PR whether to go back to bump_glue to rewrite the token in place after the decision is made - I have decided for now not to go back to that. My intuition is that not rewriting tokens is a useful invariant of the data structure, although I don't have any concrete use cases in mind at the moment. If it's particularly annoying in practice, we can revisit this in another PR).

Unfortunately, the sequences `..` and `...` do not always refer
to the `..` operator or the `...` syntax. There are two and a half cases
where they don't:

1. After `@` in macrocall, where they are both regular identifiers
2. In `import ...A` where the dots specify the level
3. `:(...)` treats `...` as quoted identifier

Case 1 was handled in a previous commit by lexing these as identifiers
after `2`.

However, as a result of case 2, it is problematic to tokenize these dots
together; we essentially have to untokenize them in the import parser. It
is also infeasible to change the lexer to have speical context-sensitive
lexing in `import`, because there could be arbitrary interpolations,
`@eval import A, $(f(x..y)), ..b`, so deciding whether a particular
`..` after import refers to the operator or a level specifier requires
the parser.

Currently the parser handles this by splitting the obtained tokens
again in the import parser, but this is undesirable, because it
invalidates the invariant that the tokens produced by the lexer
correspond to the non-terminals of the final parse tree.

This PR attempts to address this by only ever having the lexer emit
`K"."` and having the parser decide which case it refers to.
The new non-terminal `K"dots"` handles the identifier cases (ordinary
`..` and quoted `:(...)` ). K"..." is now exclusively used for
splat/slurp, and is no longer used in its non-terminal form for
case 3.

(cherry picked from commit 6e81eb1c70dd664a9bd888f13cf0b4b4e443f8fa)
@Keno
Keno force-pushed the kf/syntax-dots branch from 733d39c to af7926b Compare June 3, 2026 04:06
@Keno
Keno merged commit 939229d into JuliaLang:master Jun 3, 2026
9 checks passed
Keno added a commit to KenoAIStaging/julia that referenced this pull request Jun 10, 2026
`a .≔ b` (likewise `.⩴` and `.≕`) crashed the parser with an internal
"Unexpected dotted operator" error since the dots were split into
separate tokens (JuliaLang#61992), because `dotted()` only handled the heads
`.= .&& .|| .op=`. Add dedicated `K".≔"`/`K".⩴"`/`K".≕"` heads -
matching `K".="` - so these parse to `(.≔ a b)` and convert to
`Expr(:.≔, a, b)` as in the reference parser. Dotted `:=` is not valid
syntax (the reference parser rejects it too), so emit a proper parse
error for it instead of crashing.

Also a few smaller items from review of JuliaLang#62000:
* The new heads also fix `Expr(:.≔, a, b)` conversion into a
  `SyntaxTree`, which previously went through JuliaLowering's compound
  assignment `unknown_head` path. That path now additionally requires a
  trailing `=` in the head name, so operator-named heads such as a
  hand-written `Expr(:⊕, a, b)` produce a lowering error rather than
  being mangled into an `op=` form.
* Add `K"Operator"` to `_nonunique_kind_names`, since the kind name
  doesn't determine the token text.
* Remove duplicated `·` (U+00B7) and `·` (U+0387) entries from the
  `PREC_TIMES` operator table (they are listed - with comments - at the
  end of the list).
* Fix an asymmetry in `Expr` conversion of one-argument `K".op="` vs
  `K"op="` heads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keno added a commit to KenoAIStaging/julia that referenced this pull request Jun 10, 2026
`a .≔ b` (likewise `.⩴` and `.≕`) crashed the parser with an internal
"Unexpected dotted operator" error since the dots were split into
separate tokens (JuliaLang#61992), because `dotted()` only handled the heads
`.= .&& .|| .op=`. Add dedicated `K".≔"`/`K".⩴"`/`K".≕"` heads -
matching `K".="` - so these parse to `(.≔ a b)` and convert to
`Expr(:.≔, a, b)` as in the reference parser. Dotted `:=` is not valid
syntax (the reference parser rejects it too), so emit a proper parse
error for it instead of crashing.

Also a few smaller items from review of JuliaLang#62000:
* The new heads also fix `Expr(:.≔, a, b)` conversion into a
  `SyntaxTree`, which previously went through JuliaLowering's compound
  assignment `unknown_head` path. That path now additionally requires a
  trailing `=` in the head name, so operator-named heads such as a
  hand-written `Expr(:⊕, a, b)` produce a lowering error rather than
  being mangled into an `op=` form.
* Add `K"Operator"` to `_nonunique_kind_names`, since the kind name
  doesn't determine the token text.
* Remove duplicated `·` (U+00B7) and `·` (U+0387) entries from the
  `PREC_TIMES` operator table (they are listed - with comments - at the
  end of the list).
* Fix an asymmetry in `Expr` conversion of one-argument `K".op="` vs
  `K"op="` heads.
* Bump the JuliaSyntaxHighlighting pin to drop a stray local-path
  `[sources]` section (left over from testing) from its Project.toml.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants