moonlexkit

A MoonBit lexical scanning and lightweight parsing toolkit.

lexer
parser
tokenizer
diagnostics
moon add Wyh123456559/moonlexkit@0.3.0
Download zip
Version
0.3.0
License
Apache-2.0
Last updated
last month
Downloads
14
README

#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_atToken::start_position,支持 offset 到 line/column 的映射
  • 提供 TokenStatsScanSummary,输出 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