moonlexkit

    A MoonBit lexical scanning and lightweight parsing toolkit.

    lexer
    parser
    tokenizer
    diagnostics
    Download zip
    Version
    0.3.0
    License
    Apache-2.0
    Last updated
    2 months ago
    Downloads
    25

    #MoonLexKit

    MoonLexKit 是一个面向 MoonBit 生态的词法扫描结果诊断与轻量解析基础库。项目目标不是替代官方 lexer generator 或某个语言的完整 parser,而是为配置语言、DSL、教学编译器、代码格式化器、语法高亮、静态检查和 IDE 插件提供可复用的 tokenizer、token stream、源位置映射、诊断摘要和小型 parser helper。

    #与已有 lexer 项目的关系

    MoonBit 社区已经存在较多 lexer / parser 项目,例如官方 moonlex lexer generator、moonbitlang/parser 中的 MoonBit 语言 lexer/parser,以及若干面向 JSON、SQL、TOML 或特定 DSL 的解析器。MoonLexKit 不做生成器,不绑定 MoonBit 语言语法,也不竞争某个具体语言 parser。

    MoonLexKit 的边界是“轻量扫描器工具箱 + Token 流诊断层”:用少量通用 token、可解释 span、line/column 映射、TokenStats、ScanSummary 和错误恢复诊断,帮助库作者快速构建小型 DSL、配置语言和教学编译器原型。

    #定位

    MoonLexKit 不绑定某一种具体语言,也不依赖浏览器、文件系统或命令行 IO。核心库只处理字符串、token、跨度和诊断,保持后端中立,便于编译到 WebAssembly、JavaScript 和 Native 目标。

    #当前能力

    • 扫描 identifier、number、symbol、whitespace、comment、unknown、end token
    • 扫描字符串字面量,并诊断未闭合字符串
    • 支持常见双字符符号:==、!=、<=、>=、->、=>
    • 支持行注释 // comment,并把空白与注释统一视为 trivia
    • 通过 LexerConfig 控制是否保留 trivia、是否生成 end token
    • 提供 TokenStream 的 current、advance、matches_kind、consume_kind
    • 提供未知字符、未闭合字符串、括号不平衡诊断
    • 提供 position_at 与 Token::start_position,支持 offset 到 line/column 的映射
    • 提供 TokenStats 与 ScanSummary,输出 token 流摘要和诊断数量
    • 提供 diff_tokens,定位两次扫描之间最小的 Token 变更范围
    • 提供 Token JSON、统计 JSON 和扫描摘要 JSON 导出
    • 附带 CLI JSON 演示、测试和 GitHub Actions CI

    #快速开始

    moon test moon run cmd/main moon run bench/main

    ///|
    let config = @moonlexkit.LexerConfig::new(keep_trivia=true)

    ///|
    let tokens = @moonlexkit.scan("answer = 42 // demo", config~)

    ///|
    let json = @moonlexkit.tokens_to_json(tokens)

    ///|
    let result = @moonlexkit.scan_with_diagnostics("name = \"Moon\"", config~)

    ///|
    let summary = result.summary("name = \"Moon\"")

    #设计原则

    • 通用:用户可以把它嵌入自己的 DSL、配置文件或工具链
    • 干净:核心库不依赖平台 API,不把 CLI 行为污染进算法层
    • 可测:每个行为通过 MoonBit 测试覆盖,CI 在 push 和 PR 上自动运行
    • 可追踪:功能路线、工单、合并请求和更新日志围绕公开仓库持续沉淀
    • 差异化:不做 lexer generator,不做完整语言 parser,而是聚焦 token 流诊断和轻量解析辅助

    更完整的关系说明见 docs/RELATED_WORK.md。 可复现测试与工作负载见 docs/EVIDENCE.md。

    Assignment

    pub(all) struct Assignment {
    name : String
    value : Token
    start : Int
    end : Int
    } derive(Eq,
    Debug
    )

    One name = literal statement accepted by the small configuration DSL.

    AssignmentParseResult

    pub(all) struct AssignmentParseResult {
    assignments : Array[Assignment]
    diagnostics : Array[Diagnostic]
    } derive(Eq,
    Debug
    )

    Recoverable result of parsing configuration assignments.

    Diagnostic

    pub(all) struct Diagnostic {
    message : String
    start : Int
    end : Int
    } derive(Eq,
    Debug
    )

    Diagnostic::new

    fn Diagnostic::new(message : String, start : Int, end : Int) -> Diagnostic

    LexResult

    pub(all) struct LexResult {
    tokens : Array[Token]
    diagnostics : Array[Diagnostic]
    } derive(Eq,
    Debug
    )

    LexResult::has_errors

    fn LexResult::has_errors(self : LexResult) -> Bool

    LexResult::summary

    fn LexResult::summary(self : LexResult, source : String) -> ScanSummary

    LexerConfig

    pub(all) struct LexerConfig {
    keep_trivia : Bool
    emit_end : Bool
    keywords : Array[String]
    } derive(Eq,
    Debug
    )

    LexerConfig::default

    fn LexerConfig::default() -> LexerConfig

    LexerConfig::new

    fn LexerConfig::new(keep_trivia? : Bool, emit_end? : Bool, keywords? : Array[String]) -> LexerConfig

    ScanSummary

    pub(all) struct ScanSummary {
    tokens : Int
    diagnostics : Int
    identifiers : Int
    numbers : Int
    strings : Int
    comments : Int
    unknowns : Int
    lines : Int
    } derive(Eq,
    Debug
    )

    ScanSummary::to_json

    fn ScanSummary::to_json(self : ScanSummary) -> String

    SourcePosition

    pub(all) struct SourcePosition {
    offset : Int
    line : Int
    column : Int
    } derive(Eq,
    Debug
    )

    SourcePosition::to_json

    fn SourcePosition::to_json(self : SourcePosition) -> String

    Token

    pub(all) struct Token {
    kind : TokenKind
    text : String
    start : Int
    end : Int
    } derive(Eq,
    Debug
    )

    Token::end_at

    fn Token::end_at(offset : Int) -> Token

    Token::is_trivia

    fn Token::is_trivia(self : Token) -> Bool

    Token::length

    fn Token::length(self : Token) -> Int

    Token::new

    fn Token::new(kind : TokenKind, text : String, start : Int, end : Int) -> Token

    Token::start_position

    fn Token::start_position(self : Token, source : String) -> SourcePosition

    Token::to_json

    fn Token::to_json(self : Token) -> String

    TokenDiff

    pub(all) struct TokenDiff {
    unchanged_prefix : Int
    unchanged_suffix : Int
    removed_tokens : Int
    inserted_tokens : Int
    old_start : Int
    old_end : Int
    new_start : Int
    new_end : Int
    } derive(Eq,
    Debug
    )

    Minimal changed region between two token streams.

    TokenDiff::to_json

    fn TokenDiff::to_json(self : TokenDiff) -> String

    TokenKind

    pub(all) enum TokenKind {
    Identifier
    Keyword
    Number
    StringLiteral
    Symbol
    Whitespace
    Comment
    Unknown
    End
    } derive(Eq,
    Debug
    )

    TokenKind::name

    fn TokenKind::name(self : TokenKind) -> String

    TokenStats

    pub(all) struct TokenStats {
    total : Int
    identifiers : Int
    numbers : Int
    strings : Int
    symbols : Int
    trivia : Int
    unknowns : Int
    } derive(Eq,
    Debug
    )

    TokenStats::from_tokens

    fn TokenStats::from_tokens(tokens : Array[Token]) -> TokenStats

    TokenStats::to_json

    fn TokenStats::to_json(self : TokenStats) -> String

    TokenStream

    pub(all) struct TokenStream {
    tokens : Array[Token]
    index : Int
    } derive(Eq,
    Debug
    )

    TokenStream::advance

    fn TokenStream::advance(self : TokenStream) -> TokenStream

    TokenStream::consume_kind

    fn TokenStream::consume_kind(self : TokenStream, kind : TokenKind) -> (Bool, Token, TokenStream)

    TokenStream::current

    fn TokenStream::current(self : TokenStream) -> Token

    TokenStream::is_at_end

    fn TokenStream::is_at_end(self : TokenStream) -> Bool

    TokenStream::matches_kind

    fn TokenStream::matches_kind(self : TokenStream, kind : TokenKind) -> Bool

    TokenStream::new

    fn TokenStream::new(tokens : Array[Token]) -> TokenStream

    count_lines

    fn count_lines(source : String) -> Int

    diff_tokens

    fn diff_tokens(old_tokens : Array[Token], new_tokens : Array[Token]) -> TokenDiff

    Finds a minimal token-level edit by trimming equal prefix and suffix runs.

    Callers that need whitespace-precise ranges should scan with keep_trivia.

    is_ascii_digit

    fn is_ascii_digit(ch : Char) -> Bool

    is_ascii_letter

    fn is_ascii_letter(ch : Char) -> Bool

    is_ascii_whitespace

    fn is_ascii_whitespace(ch : Char) -> Bool

    is_identifier_part

    fn is_identifier_part(ch : Char) -> Bool

    is_identifier_start

    fn is_identifier_start(ch : Char) -> Bool

    parse_assignments

    fn parse_assignments(source : String) -> AssignmentParseResult

    Parses a small configuration DSL made of name = literal; statements. Invalid statements are skipped to the next semicolon so later statements remain available to editors and configuration tooling.

    position_at

    fn position_at(source : String, offset : Int) -> SourcePosition

    scan

    fn scan(source : String, config? : LexerConfig) -> Array[Token]

    scan_with_diagnostics

    fn scan_with_diagnostics(source : String, config? : LexerConfig) -> LexResult

    tokens_to_json

    fn tokens_to_json(tokens : Array[Token]) -> String

    Source Files