// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// The weight class of a token. Weights encode semantic importance for the
/// alignment scorer: identifiers and literals carry full signal, punctuation
/// less, comment content little, and comment boilerplate / whitespace none to
/// almost none.
pub(all) enum TokKind {
Word // identifiers, keywords, numbers
Str // string / char / bytes / regex literals (one token each)
Punct // operators, brackets, separators
Comment // content tokens inside a comment (tokenized, never opaque)
Filler // comment boilerplate: `//` markers and comment whitespace
Space // inter-token whitespace (reconstructed from lexer position gaps)
Marker // the `///|` block separator (full weight: structural, not prose)
} derive(Eq)
///|
/// Keep `==` / `!=` available as dot-callable methods on `TokKind` now that
/// implicit promotion of `impl Eq` methods is deprecated.
pub extend TokKind with Eq::{not_equal, equal}
///|
/// One diffable token: its weight class and its exact source text.
/// Concatenating the `text` of `tokenize_line`'s result reproduces the line.
pub struct Tok {
kind : TokKind
text : String
}
///|
pub fn Tok::kind(self : Tok) -> TokKind {
self.kind
}
///|
pub fn Tok::text(self : Tok) -> String {
self.text
}
///|
/// The alignment weight of a token class (integer; all scoring is integer so
/// results are bit-identical on every backend).
pub fn weight(k : TokKind) -> Int {
match k {
Word | Str | Marker => 20
Punct => 6
Comment => 2
Space => 1
Filler => 0
}
}
///|
/// Tokenize the inside of a comment: words keep a little weight so similar
/// comments pair (and identical comments win ties), while the `//` markers
/// and comment spacing are weightless `Filler` — shared boilerplate must not
/// make unrelated comments look similar.
fn push_comment_tokens(toks : Array[Tok], body : String) -> Unit {
let scanner = @lexbuf.StringScanner::{ data: body.view(), cursor: 0, }
while scanner.cursor < scanner.data.length() {
lexscan scanner with longest {
re"^//+" as t => toks.push({ kind: Filler, text: t.to_owned(), })
re"^[ \t]+" as t => toks.push({ kind: Filler, text: t.to_owned(), })
re"^[A-Za-z0-9_]+" as t =>
toks.push({ kind: Comment, text: t.to_owned(), })
re"^." as t => toks.push({ kind: Comment, text: t.to_string(), })
_ => abort("unreachable")
}
}
}
///|
fn classify(tok : @tokens.Token, text : String) -> TokKind {
match tok {
CHAR(_)
| BYTE(_)
| BYTES(_)
| STRING(_)
| MULTILINE_STRING(_)
| MULTILINE_INTERP(_)
| INTERP(_)
| REGEX_LITERAL(_)
| REGEX_INTERP(_) => Str
// identifier-like tokens are Word regardless of spelling (Unicode
// identifiers must not fall through to Punct), and attributes /
// package references carry identifier-grade signal
LIDENT(_)
| UIDENT(_)
| POST_LABEL(_)
| DOT_LIDENT(_)
| DOT_UIDENT(_)
| PACKAGE_NAME(_)
| ATTRIBUTE(_)
| INT(_)
| FLOAT(_)
| DOUBLE(_) => Word
_ =>
match text {
[c, ..] if (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
c == '_' ||
(c >= '0' && c <= '9') => Word
_ => Punct
}
}
}
///|
/// Tokenize one line of MoonBit source with the real MoonBit lexer
/// (`moonbitlang/lexer`), mapping language tokens to weight classes:
///
/// - identifiers / keywords / numbers -> `Word`
/// - string, char, bytes and regex literals -> `Str` (one token each)
/// - operators and separators -> `Punct`
/// - comments -> content-tokenized `Comment` words + weightless `Filler`
/// - gaps between token spans (whitespace the lexer skipped) -> `Space`,
/// reconstructed from positions so concatenation reproduces the line
///
/// Lexical errors are ignored (diffs are routinely taken of incomplete
/// code); the lexer's best-effort token stream is used as is.
pub fn tokenize_line(line : String) -> Array[Tok] {
let toks : Array[Tok] = []
let r = @lexer.tokens_from_string_with_utf16_location(comment=true, line)
let mut prev_end = 0
for triple in r.tokens {
let (tok, sp, ep) = triple
if tok is (NEWLINE | EOF) {
continue
}
if sp.cnum > prev_end {
toks.push({
kind: Space,
text: line.view(start_offset=prev_end, end_offset=sp.cnum).to_owned(),
})
}
let text = line.view(start_offset=sp.cnum, end_offset=ep.cnum).to_owned()
match tok {
COMMENT(_) =>
if text is ['/', '/', '/', '|', ..] {
toks.push({ kind: Marker, text: "///|", })
push_comment_tokens(toks, text.view(start_offset=4).to_owned())
} else {
push_comment_tokens(toks, text)
}
_ => toks.push({ kind: classify(tok, text), text, })
}
prev_end = ep.cnum
}
if prev_end < line.length() {
toks.push({
kind: Space,
text: line.view(start_offset=prev_end).to_owned(),
})
}
toks
}