Skip to content

Introduce token modes - #127

Open
Lotes wants to merge 83 commits into
mainfrom
lotes/lexer-modes
Open

Introduce token modes#127
Lotes wants to merge 83 commits into
mainfrom
lotes/lexer-modes

Conversation

@Lotes

@Lotes Lotes commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Closes #50

  • added explicit token modes (lexer modes)
  • if none is given, one is create implicitly
  • if not given, it collects all keywords, token declarations and token groups from the grammar
  • token modes can have following members
    • own token declarations
    • token refs to top-level token declaration
    • own token groups
    • keywords
    • keyword selection using a RegExp
  • token commands were also introduced
    • push(name) to push a token mode onto the token mode stack
    • pop to pop the topmost token mode
    • mode(name) to set the topmost token mode
  • I also extended token declarations to be keywords as well
  • a lot of changes happened in the lexer generator
    • I tidied up the code organization a lot, I hope you like it as well!
    • token types are split from group type and token commands
      • it made sense during the journey in order to write a command/type differently in different modes... we can discuss if it still makes sense
    • "keywords" I renamed conceptually to KeywordSelector
  • validations around token commands were created
  • a lot of tests were added by AI, they caught bugs and show the expectations when using token modes

@msujew msujew left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice! Can you resolve the merge conflict so we can check the performance in the GitHub CI? Locally, it seems to look good. Only a 2-3% loss on the statemachine example, which is acceptable.

Comment thread internal/generator/lexer_generator.go Outdated
Comment thread internal/generator/lexer_util_test.go Outdated
Comment thread internal/grammar/validator.go Outdated
Comment thread internal/languages/token_modes/parser_test.go
@Lotes
Lotes force-pushed the lotes/lexer-modes branch from 043764e to 0813295 Compare July 14, 2026 15:25
@Lotes

Lotes commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator Author

@msujew I discovered a problem with my main grammar.

The problem arises when I create a token mode and start to create token declarations within this token mode. As soon as I complete the word "token", the language server seems to end up in a endless loop. It does not react anymore. After completing the declaration and restarting the LS, it works. So the problem is during editing incomplete grammars :-/...

token mode default {
  toke //complete to "token" to see the problem
}

The rule TokenModeMember: TokenDeclUsage | TokenGroupUsage | TokenUsage | KeywordUsage | KeywordSelector is actually having some alternatives with a common (nested) prefix. I hoped that the LL(STAR) is handling this ^^*. Or maybe it is a different problem?

If I would refactor the grammar to have no common prefix in the subrules of TokenModeMember the problem might disappear and the performance would increase.
But it would also make the grammar more difficult I think.

Before I refactor it would be good to discuss whether these members have a good syntax or whether we should alternate it (which could remove the common prefix automatically).

@Lotes

Lotes commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator Author

Ideas...

Some quick fixes or code actions come into my mind:

  1. inline a token rule that points to a keyword
  2. and the opposite: extract all keywords to a common token rule

Or

  1. a hover tooltip over a keyword selector which keywords are included

@msujew

msujew commented Jul 27, 2026

Copy link
Copy Markdown
Member

I discovered a problem with my main grammar.

Does this require some setup? I could not reproduce this:

image

But it would also make the grammar more difficult I think.

I would keep it as it is for now 👍

Comment thread internal/grammar/grammar.fb Outdated
@Lotes
Lotes force-pushed the lotes/lexer-modes branch from 467be1d to 2996cda Compare August 4, 2026 14:56

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.50.

Benchmark suite Current: 1ee2bcd Previous: d803417 Ratio
BenchmarkWorkspaceCycle (typefox.dev/fastbelt/examples/statemachine) - MB/s 11.92 MB/s 5.21 MB/s 2.29

This comment was automatically generated by workflow using github-action-benchmark.

@Lotes

Lotes commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

I discovered a problem with my main grammar.

Does this require some setup? I could not reproduce this:

No, it should fail out of the box. I can also complete this line as you started. But afterwards toke gets underlined and the LS is frozen. I cannot jump to definition or anything else.
I could not find out how to debug this yet.

EDIT:

I found out how to debug. Some kind of endless-loop in entered in this case.

EDIT:

Fixed the endless loop and another issue with the help of @msujew

@Lotes
Lotes marked this pull request as ready for review August 6, 2026 14:10
@Lotes
Lotes requested a review from msujew August 6, 2026 14:11
Comment thread token.go

@Lotes Lotes left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added some findings and also questions from my side.

Comment thread lexer/modes.go Outdated
Comment thread .vscode/launch.json Outdated
Comment thread examples/arithmetics/arithmetics.fb
Comment thread examples/statemachine/statemachine.fb
Comment thread internal/atn/state_names.go Outdated
Comment thread internal/generator/lexer_util.go
Comment thread internal/grammar/grammar.fb Outdated

@msujew msujew left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Really solid contribution. Thanks a lot!

I noticed another few missing validations that we should add as part of this PR:

  • Duplicate keyword/token definitions in the same token mode should result in an error. I.e. you can currently write token mode default { "x" "x" }.
  • Similarly Token references should be unique in the same token mode. Right know, you can write something like token mode default { hidden WS comment WS } without a validation error appearing.
  • When a token is declared, but not referenced in any token mode, the diagnostic appears on the first usage site. I would expect it on the token declaration itself. Also, similar to the keyword issue, this should be an error.
  • A non-default token mode without a pop command should show a diagnostic.

Comment thread .vscode/launch.json Outdated
Comment thread examples/arithmetics/arithmetics.fb
Comment thread internal/atn/state_names.go Outdated
Comment thread internal/generator/lexer_generator.go Outdated
Comment thread internal/languages/token_modes/benchmark_test.go Outdated
Comment thread internal/grammar/validator.go
Comment thread examples/statemachine/statemachine.fb
Comment thread internal/grammar/grammar.fb
Comment thread internal/grammar/grammar.fb
Comment thread internal/languages/token_modes/token_modes.fb Outdated
msujew and others added 15 commits August 13, 2026 13:19
# Conflicts:
#	internal/grammar/types_gen.go

# Conflicts:
#	internal/grammar/types_gen.go
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>

# Conflicts:
#	lexer/lexer.go
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Lotes added 26 commits August 13, 2026 13:25
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>
@Lotes
Lotes force-pushed the lotes/lexer-modes branch from 17d7c0e to 557bdc7 Compare August 13, 2026 11:37
Signed-off-by: Markus Rudolph <markus.rudolph@typefox.io>

@msujew msujew left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looking closer at some of the logic, I found some issues with some specific constellations. Works much better in general now, though!

hidden token WS: /\s+/
comment token ML_COMMENT: /\/\*[\s\S]*?\*\//
hidden token SL_COMMENT: /\/\/[^\n\r]*/
hidden token SL_COMMENT: /\/\/[^\n\r]*/ No newline at end of file

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Formatting: You've removed the EOL here.

Comment on lines +152 to +155
case grammar.TokenGroupUsage:
tokenGroup := member.Group()
tokenIndex := result.TokenIndex.ByTokenGroup[tokenGroup]
pushTokenTypeUsage(tokenIndex, tokenGroup.Modifier(), tokenGroup.Command())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggestion: Token groups in token modes show some buggy behavior. Let's take the following grammar code:

token group X {
  "x"
}

token mode default {
  "x" -> push(Y)
}

token mode Y {
  X -> pop
}

Giving a grammar like this the input xx actually results in an error at the second x, because the X token group does not have start char hints (or is lexible at all). Using X inside a token mode, should actually add its "sub-tokens" to the token mode.

Comment thread parser/error_recovery.go
Comment on lines +71 to +74
//if we are stuck on the same token, consume it and try to move on
if parserState.lastErrorIndex == parserState.Index {
parserState.Index++
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggestion: This should not bump the Index beyond parserState.Length. Otherwise we run into issues at EOF.

}

for _, token := range tokens.TopLevel {
if _, ok := token.Content().(grammar.RegexpTokenElement); ok {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggestion: This drops token usage on tokens like hidden token Test: "test".


TokenDecl: Modifier=TokenModifier? "token" Name=ID ":" Content=TokenElement Command=TokenCommand? ";"?

interface TokenElement {}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

WDYT about this name? Similar RegexpTokenElement -> RegexpTokenContent etc.

Suggested change
interface TokenElement {}
interface TokenContent {}

}

func (s *scopeProviderImpl) ScopeRuleCallRule(ctx context.Context, reference *core.Reference[AbstractRule]) core.Scope {
root, _ := reference.Owner().Document().Root.(Grammar)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggestion: Don't discard the ok of the cast. Can theoretically lead to panics here.

Comment on lines +321 to +325
func mergeImports(target *map[string]bool, source map[string]bool) {
for imp := range source {
(*target)[imp] = true
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggestion: Use maps.Copy for this. Also *map is not necessary. map is always a pointer type.

severity,
fmt.Sprintf("The token '%s' is never referenced in a parser rule, so the lexer can never produce it.", tokenRef.Name()),
member,
core.WithTextRange(member.TextRange()),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggestion: NewDiagnostic automatically takes the TextRange() value to initialize the range. No need to use WithTextRange again.

if !seen.Has(terminal.Name()) {
accept(core.NewDiagnostic(
severity,
fmt.Sprintf("The token '%s' is never referenced in a parser rule, so the lexer can never produce it.", terminal.Name()),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we move this string somewhere? It's repeated 6 times in this method.

}

func generateNestedString() (string, error) {
content := ""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggestion: Use a strings.Builder instead of a plain string here. Concatinating a new string 10000 times is quite expensive.

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.

[Lexer] Support multiple lexer modes

2 participants