Sign in

    arachne

    Behavior contract testing across MoonBit regex backends with Unicode boundary fixtures.

    regex
    compatibility
    testing
    unicode
    Download zip
    Version
    0.1.1
    License
    Apache-2.0
    Last updated
    10 hours ago
    Downloads
    7

    #Arachne

    **Arachne 是 MoonBit 正则行为契约与兼容性测试工具。**它将共享的模式、输入和预期结果运行在正则引擎适配器上,比较编译接受、匹配文本、跨度和捕获值,并输出文本或 JSON 报告,帮助开发者检查依赖升级与后端替换的行为变化。

    Arachne 还包含一个自有的 ASCII 子集参考引擎:递归下降解析器将模式编译为 Thompson NFA,再以去重状态集执行,作为可审查的第三个对照后端。官方 moonbitlang/regexp 与 walkzzz/re-mbt 仍是独立公开依赖。参考引擎只支持文档列明的语法,不实现捕获值或完整 Unicode,也不替代通用正则库;它与后端的差异会由契约报告呈现。

    #快速开始

    需要 MoonBit(moonc 0.10.14 或更新版本)和 Node.js 22 或更新版本:

    moon update moon check --deny-warn moon test --deny-warn moon build --deny-warn moon run examples/demo moon run examples/demo-json moon run examples/reference-engine node tools/verify-demo.mjs

    文本与 JSON 示例演示迁移报告;参考引擎示例将其与官方引擎运行同一组契约。moon update 获取 moon.mod 中固定版本的依赖;检查及示例命令用于验证当前检出版本。

    #功能

    • 编写匹配、非匹配、捕获值和整段跨度契约。
    • 用 MoonBit 实现有限 ASCII 正则解析、Thompson NFA 编译和状态集匹配,作为独立参考后端。
    • 通过适配器比较自有参考引擎与公开正则后端的实际结果。
    • 获取稳定文本结果与 JSON v1 报告。
    • 运行迁移夹具、示例和回归检查。

    参考引擎支持字面量、.、^ / $、字符类与范围、\d / \w / \s 及其反类、分组、|、* / + / ?。分组只影响优先级,不产出捕获值;匹配采用左侧起点优先、同一起点最长结果。模式上限为 4096 个 ASCII 字符,输入上限为 16384 个 ASCII 字符,分组最多嵌套 64 层。回引用、环视、计数重复、其他字母数字转义和非 ASCII 输入会明确拒绝。

    #示例观察

    当前共享 ASCII 夹具在两套后端上执行;回引用夹具 ^([a-z])\1$ 在官方引擎接受并匹配,在 re-mbt Perl 前端被拒绝编译。该结果表示选定用例上的行为差异,不自动说明任何一方存在缺陷。迁移是否可接受由使用者结合应用需求判断。

    整段跨度使用 MoonBit StringView UTF-16 代码单元。例如 🦊a 中的 a 跨度为 [2, 3)。适配器须遵守仓库所定义的偏移约定。

    #验证与贡献边界

    GitHub Actions CI 在 default、JS、native 目标执行检查、测试、构建和示例验证,并记录工具链版本及运行报告。正则行为报告针对有限夹具,不提供超时、沙箱或性能保证;Unicode 类别数据的来源和限制见第三方说明及项目边界。

    项目由韦安良一人维护。AI(Codex)辅助部分代码与测试编写、排查和文档整理;项目范围与最终取舍由负责人负责。公开依赖维护者不是本项目成员,项目不申报上游贡献。AI 辅助披露和数据限制见申报书。

    #许可

    Arachne 源码采用 Apache-2.0。第三方依赖及 Unicode 数据许可见 THIRD_PARTY_NOTICES.md、LICENSE 和 UNICODE-LICENSE.txt。

    CompatibilityFinding

    pub(all) struct CompatibilityFinding {
    case_id : String
    engine_id : String
    kind : FindingKind
    detail : String
    }

    A cross-engine or expectation difference.

    CompatibilityReport

    pub(all) struct CompatibilityReport {
    baseline_id : String
    case_count : Int
    engine_count : Int
    runs : Array[EngineRun]
    findings : Array[CompatibilityFinding]
    }

    Aggregate result from a compatibility run.

    CompatibilityReport::case_count

    fn CompatibilityReport::case_count(self : CompatibilityReport) -> Int

    Number of cases evaluated by each configured engine.

    CompatibilityReport::engine_count

    fn CompatibilityReport::engine_count(self : CompatibilityReport) -> Int

    Number of configured adapters in this run.

    CompatibilityReport::format_findings

    fn CompatibilityReport::format_findings(self : CompatibilityReport) -> String

    Stable text form of findings, preserving case and adapter order.

    CompatibilityReport::format_json

    fn CompatibilityReport::format_json(self : CompatibilityReport) -> String

    Serialize a compatibility report using the version 1 wire schema.

    CompatibilityReport::is_compatible

    fn CompatibilityReport::is_compatible(self : CompatibilityReport) -> Bool

    True when no engine differs from the baseline and every golden expectation passed.

    CompatibilityReport::json_v1

    Take a v1 snapshot without compiling patterns or calling adapters.

    CompatibilityReport::summary

    fn CompatibilityReport::summary(self : CompatibilityReport) -> String

    Stable one-line summary intended for CI output.

    CompiledMatcher

    pub(all) struct CompiledMatcher {
    observe : (String) -> Result[MatchObservation?, String]
    }

    An opaque compiled pattern supplied by an engine adapter.

    CompiledMatcher::new

    fn CompiledMatcher::new(observe~ : (String) -> Result[MatchObservation?, String]) -> CompiledMatcher

    Build the observation closure returned after a backend compiles a pattern.

    ContractCase

    pub(all) struct ContractCase {
    id : String
    pattern : String
    input : String
    expected : ExpectedOutcome?
    expected_span : (Int, Int)?
    }

    A pattern/input pair in a portable behavior contract.

    ContractCase::expect_match

    fn ContractCase::expect_match(id~ : String, pattern~ : String, input~ : String, text~ : String, captures~ : Array[String?]) -> ContractCase

    Construct a contract case with expected whole-match and capture text.

    ContractCase::expect_match_span

    fn ContractCase::expect_match_span(id~ : String, pattern~ : String, input~ : String, text~ : String, captures~ : Array[String?], start~ : Int, end~ : Int) -> ContractCase

    Construct a contract case that also asserts the normalized whole-match span. Offsets use the adapter contract's zero-based StringView code units.

    ContractCase::expect_no_match

    fn ContractCase::expect_no_match(id~ : String, pattern~ : String, input~ : String) -> ContractCase

    Construct a contract case with an expected non-match.

    ContractCase::new

    fn ContractCase::new(id~ : String, pattern~ : String, input~ : String) -> ContractCase

    Construct a contract case without an expected value.

    ContractError

    pub(all) enum ContractError {
    EmptyBaseline
    NoEngines
    EmptyEngineId
    DuplicateEngine(String)
    BaselineNotFound(String)
    DuplicateCase(String)
    EmptyCaseId
    InvalidExpectedSpan(String)
    } derive(Eq,
    Debug
    )

    Invalid or ambiguous compatibility-run configuration.

    ContractError::equal

    ContractError::not_equal

    fn ContractError::not_equal(x : ContractError, y : ContractError) -> Bool

    EngineAdapter

    pub(all) struct EngineAdapter {
    id : String
    compile : (String) -> Result[CompiledMatcher, String]
    }

    An engine adapter. Implement compile by calling the engine under test; this package never parses patterns or performs matching itself.

    EngineAdapter::new

    fn EngineAdapter::new(id~ : String, compile~ : (String) -> Result[CompiledMatcher, String]) -> EngineAdapter

    Build an adapter around a real regex implementation.

    EngineOutcome

    pub(all) enum EngineOutcome {
    CompileRejected(String)
    ExecutionFailed(String)
    Completed(MatchObservation?)
    } derive(Eq,
    Debug
    )

    Result of running one case against one engine.

    EngineOutcome::equal

    EngineOutcome::not_equal

    fn EngineOutcome::not_equal(x : EngineOutcome, y : EngineOutcome) -> Bool

    EngineOutcomeJsonV1

    pub(all) struct EngineOutcomeJsonV1 {
    status : String
    error : String?
    observation : MatchJsonV1?
    }

    Status is compile_rejected, execution_failed, no_match, or matched. Error and observation are always present on the wire, using null when absent.

    EngineOutcomeJsonV1::to_json

    EngineRun

    pub(all) struct EngineRun {
    case_id : String
    engine_id : String
    outcome : EngineOutcome
    }

    Output rows are stable in case order, then adapter order.

    EngineRunJsonV1

    pub(all) struct EngineRunJsonV1 {
    case_id : String
    engine_id : String
    outcome : EngineOutcomeJsonV1
    } derive(ToJson)

    EngineRunJsonV1::to_json

    ExpectedOutcome

    pub(all) enum ExpectedOutcome {
    ExpectedNoMatch
    ExpectedMatch(text~ : String, captures~ : Array[String?])
    } derive(Eq,
    Debug
    )

    Optional engine-independent expectation for one contract case.

    ExpectedOutcome::equal

    ExpectedOutcome::not_equal

    fn ExpectedOutcome::not_equal(x : ExpectedOutcome, y : ExpectedOutcome) -> Bool

    FindingJsonV1

    pub(all) struct FindingJsonV1 {
    case_id : String
    engine_id : String
    kind : String
    detail : String
    baseline : EngineOutcomeJsonV1?
    actual : EngineOutcomeJsonV1?
    }

    Baseline and actual preserve the observations behind a finding. Null means the corresponding run is absent from a caller-constructed report.

    FindingJsonV1::to_json

    fn FindingJsonV1::to_json(self : FindingJsonV1) -> Json

    FindingKind

    pub(all) enum FindingKind {
    CompileAcceptanceDiff
    ExecutionDiff
    MatchPresenceDiff
    MatchedTextDiff
    MatchSpanDiff
    CaptureDiff
    ExpectationFailed
    } derive(Eq,
    Debug
    )

    Difference categories emitted by a contract run.

    FindingKind::equal

    fn FindingKind::equal(FindingKind, FindingKind) -> Bool

    FindingKind::not_equal

    fn FindingKind::not_equal(x : FindingKind, y : FindingKind) -> Bool

    MatchJsonV1

    pub(all) struct MatchJsonV1 {
    text : String
    start : Int
    end : Int
    captures : Array[String?]
    }

    Captures exclude group zero; null means an unmatched capture. Spans remain zero-based, end-exclusive StringView code-unit offsets.

    MatchJsonV1::to_json

    fn MatchJsonV1::to_json(self : MatchJsonV1) -> Json

    MatchObservation

    pub(all) struct MatchObservation {
    text : String
    start : Int
    end : Int
    captures : Array[String?]
    } derive(Eq,
    Debug
    )

    A single engine observation normalized for cross-backend comparison. Offsets are zero-based MoonBit StringView code-unit offsets.

    MatchObservation::equal

    MatchObservation::not_equal

    fn MatchObservation::not_equal(x : MatchObservation, y : MatchObservation) -> Bool

    ReportJsonV1

    pub(all) struct ReportJsonV1 {
    schema_version : Int
    baseline_id : String
    case_count : Int
    engine_count : Int
    compatible : Bool
    runs : Array[EngineRunJsonV1]
    findings : Array[FindingJsonV1]
    } derive(ToJson)

    Version 1 of the machine-readable report. Independent of debug enum output.

    ReportJsonV1::stringify

    fn ReportJsonV1::stringify(self : ReportJsonV1) -> String

    ReportJsonV1::to_json

    UnicodeCorpusError

    pub(all) enum UnicodeCorpusError {
    UnknownCategory(String)
    InvalidCaseLimit
    NoScalarSamples(String)
    } derive(Eq,
    Debug
    )

    Errors returned while deriving boundary cases from Unicode category data.

    UnicodeCorpusError::equal

    UnicodeCorpusError::not_equal

    moonbit_regexp_adapter

    fn moonbit_regexp_adapter() -> EngineAdapter

    Compare a suite using MoonBit's official moonbitlang/regexp package.

    reference_ascii_adapter

    fn reference_ascii_adapter() -> EngineAdapter

    A deliberately small, first-party ASCII regex reference backend. It is intended for differential fixtures, not as a replacement for a full regex implementation. Groups are structural only; capture values are not exposed.

    rembt_ascii_perl_adapter

    fn rembt_ascii_perl_adapter() -> EngineAdapter

    Adapt the Perl frontend of walkzzz/re-mbt for ASCII-only contracts.

    re-mbt exposes byte-oriented APIs. This adapter intentionally accepts only ASCII patterns and inputs, where byte offsets equal MoonBit StringView code unit offsets. It reports non-ASCII values as adapter-domain errors rather than guessing at an encoding conversion.

    run_contracts

    fn run_contracts(cases : Array[ContractCase], engines : Array[EngineAdapter], baseline_id : String) -> Result[CompatibilityReport, ContractError]

    Run a suite against every supplied adapter and compare each result with a designated baseline. The caller controls adapter order and case order.

    unicode_category_cases

    fn unicode_category_cases(category~ : String, max_cases~ : Int) -> Result[Array[ContractCase], UnicodeCorpusError]

    Build deterministic positive and adjacent-negative cases from the Unicode 17.0 category range boundaries. The limit caps the number of cases emitted. This is a test-corpus oracle, not a Unicode classifier used at runtime.