diff --git a/CHANGELOG.md b/CHANGELOG.md index b3600cf..d463bdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 无。 +## [0.2.0] - 2026-07-12 + +### Added — INFRA flagship demos, tutorials & performance evidence(INFRA 旗舰 demo、教程与性能证据) + +- feat(examples): second demo batch — `actor_worker_pool` (supervised + restart / routing / backpressure), `build_pipeline` (wave scheduling / + incremental rebuild / auto-bisect), `serialization_studio` (typed wire + + JSON round-trips / breaking-change detection), `dst_explorer` + (deterministic replay / DPOR / linearizability), `config_diff_ops` + (TOML/INI + diff3 + semver)(第二批 5 个 INFRA 端到端 demo) +- feat(examples): third demo batch — `hash_integrity`(SHA-2/3、BLAKE2b、 + HMAC 防篡改、HKDF/PBKDF2、流式 == 一次性、xxHash 分片)、 + `compress_workbench`(DEFLATE/zlib/gzip/zstd/LZ4 压缩率对比、字典压缩、 + 损坏归档拒绝)、`time_scheduler`(RFC 3339/2822、POSIX TZ 夏令时、 + 时间轮、工作窃取调度)、`resilience_gateway`(退避重试、熔断器、 + 令牌桶/滑动窗口、隔板、AIMD、对冲请求)、`cli_devtool`(类型化校验、 + 拼写建议、help/补全生成)、`observability_kit`(HDR 直方图、DDSketch、 + span 追踪)、`text_editor_core`(rope/piece table、Myers diff、 + LRU/布隆/roaring)、`parser_playground`(表达式求值、JSON 错误恢复、 + 增量解析)、`pbt_fuzz_lab`(属性缩小、往返律、种子化图 fuzz)—— + 20 directions now ship runnable end-to-end demos +- feat(bench): `benches/infra_hash_bench` (crypto digests vs fast hashes, + streaming == one-shot smoke) and `benches/infra_compress_bench` + (deflate/zstd/lz4 round-trip throughput, lossless smoke); native results + archived in `benches/results/infra-hash-compress-native-2026-07-12.md` + (新增哈希/压缩原生基准与结果归档) +- docs(tutorials): hands-on handbook for every direction — + `docs/tutorials/README.md` (EN) and `docs/zh/tutorials.md` (中文), + key APIs + runnable snippets + demo mapping +- docs(readme): bilingual demo overview tables now list all 20 runnable + example workflows(README 中英 demo 总览表补齐 20 条) + ## [0.1.0] - 2026-07-12 ### Added — Real OSM Playground & ecosystem(真实 OSM Playground 与生态) @@ -246,5 +278,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Project adopts the "successor function" API philosophy from Rust's `pathfinding` crate, with independent algorithm implementations. -[Unreleased]: https://github.com/Suquster/moonbit-pathfinding/compare/v0.0.1...HEAD +[Unreleased]: https://github.com/Suquster/moonbit-pathfinding/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/Suquster/moonbit-pathfinding/compare/v0.1.0...v0.2.0 +[0.1.0]: https://github.com/Suquster/moonbit-pathfinding/compare/v0.0.1...v0.1.0 [0.0.1]: https://github.com/Suquster/moonbit-pathfinding/releases/tag/v0.0.1 diff --git a/README.md b/README.md index cfcbaae..db1da66 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](./LICENSE) [![CI](https://github.com/Suquster/moonbit-pathfinding/actions/workflows/ci.yml/badge.svg)](https://github.com/Suquster/moonbit-pathfinding/actions/workflows/ci.yml) -[![Version](https://img.shields.io/badge/version-v0.1.0-blue)](./CHANGELOG.md) +[![Version](https://img.shields.io/badge/version-v0.2.0-blue)](./CHANGELOG.md) [![mooncakes.io](https://img.shields.io/badge/mooncakes.io-Suquster%2Fmoonbit--pathfinding-orange)](https://mooncakes.io/docs/Suquster/moonbit-pathfinding) [![Playground](https://img.shields.io/badge/Playground-live-brightgreen)](https://suquster.github.io/moonbit-pathfinding/) [![Executable contracts](https://img.shields.io/badge/proof_predicates-runtime_checked-yellow)](#formal-verification) @@ -34,7 +34,7 @@ CH → Hub Labels → PHAST) plus 20 infra directions, published on [mooncakes.io](https://mooncakes.io/docs/Suquster/moonbit-pathfinding) with a [live in-browser playground](https://suquster.github.io/moonbit-pathfinding/). -2. **An engineering benchmark for the ecosystem** — 3331 tests across four +2. **An engineering benchmark for the ecosystem** — 3339 tests across four backends (wasm-gc / native / js / wasm), executable proof predicates, executable README (`moon test README.mbt.md`), DST + differential PBT, zero-warning `--deny-warn` CI gates, and a published @@ -197,6 +197,15 @@ stories instead of isolated snippets — pathfinding and INFRA directions alike: | Serialization studio | `moon run examples/serialization_studio` | serialization | .proto parse/validate, typed wire + JSON round-trips, canonical bytes, breaking-change detection, codegen | | DST explorer | `moon run examples/dst_explorer` | dst | Seeded deterministic replays, partition/crash fault injection, DPOR exploration, shrinking, linearizability checking | | Config & diff ops | `moon run examples/config_diff_ops` | infra_config + infra_diff | TOML/INI parsing, unified diffs, patch apply/revert, diff3 merge with conflicts, semver gates | +| Hash integrity | `moon run examples/hash_integrity` | infra_hash | SHA-2/SHA-3/BLAKE2b digests (sha256 matches `sha256sum`), HMAC tamper detection, HKDF/PBKDF2 key derivation, streaming == one-shot, xxHash sharding | +| Compress workbench | `moon run examples/compress_workbench` | infra_compress | DEFLATE/zlib/gzip/zstd/LZ4 ratio shoot-out, lossless round-trips, dictionary compression, corrupted-archive rejection | +| Time scheduler | `moon run examples/time_scheduler` | infra_time + infra_timer | RFC 3339/2822 + strftime, civil arithmetic, POSIX TZ DST rules, duration round-trip, timer wheel, work-stealing scheduler with real steals | +| Resilience gateway | `moon run examples/resilience_gateway` | infra_resilience | Capped backoff + retry, circuit-breaker state machine, token bucket vs sliding window, bulkhead, AIMD, hedged requests | +| CLI devtool | `moon run examples/cli_devtool` | infra_cli | Subcommand parsing with defaults, typed validation with choices, typo suggestions, bundled shorts, generated help + bash completion | +| Observability kit | `moon run examples/observability_kit` | infra_metrics | HDR histogram tail percentiles, mergeable DDSketch quantiles, span tracer with total vs self time | +| Text editor core | `moon run examples/text_editor_core` | infra_text + infra_ds | Rope + piece-table edits converge, graphemes/display width, Myers diff, LRU eviction, bloom filter, roaring-bitmap intersection | +| Parser playground | `moon run examples/parser_playground` | parser_combinator | Precedence-correct expression eval, JSON with rendered error positions, error recovery diagnostics, incremental chunked parsing | +| PBT & fuzz lab | `moon run examples/pbt_fuzz_lab` | infra_pbt + infra_fuzz | Property checking, shrinking to boundary counterexample (500), distribution stats, round-trip laws, seeded graph fuzzing + structural shrinking | Verify all example outputs with checked markers: @@ -209,6 +218,10 @@ Latest evidence: and [`docs/examples/latest-examples-run.json`](./docs/examples/latest-examples-run.json). +Hands-on tutorials for every direction (key APIs + minimal snippets + the +demo that exercises them): [`docs/tutorials/README.md`](./docs/tutorials/README.md) +(中文版: [`docs/zh/tutorials.md`](./docs/zh/tutorials.md)). + --- ## Release Readiness diff --git a/README.zh-CN.md b/README.zh-CN.md index adc702b..a129ac3 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -4,7 +4,7 @@ [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](./LICENSE) [![CI](https://github.com/Suquster/moonbit-pathfinding/actions/workflows/ci.yml/badge.svg)](https://github.com/Suquster/moonbit-pathfinding/actions/workflows/ci.yml) -[![Version](https://img.shields.io/badge/version-v0.1.0-blue)](./CHANGELOG.md) +[![Version](https://img.shields.io/badge/version-v0.2.0-blue)](./CHANGELOG.md) [![mooncakes.io](https://img.shields.io/badge/mooncakes.io-Suquster%2Fmoonbit--pathfinding-orange)](https://mooncakes.io/docs/Suquster/moonbit-pathfinding) [![Playground](https://img.shields.io/badge/Playground-live-brightgreen)](https://suquster.github.io/moonbit-pathfinding/) [![Executable contracts](https://img.shields.io/badge/proof_predicates-runtime_checked-yellow)](#formal-verification) @@ -24,7 +24,7 @@ (BFS → A\* → JPS → ALT → CH → Hub Labels → PHAST)+ 20 个 INFRA 方向, 已发布 [mooncakes.io](https://mooncakes.io/docs/Suquster/moonbit-pathfinding), 并提供[打开即玩的浏览器 playground](https://suquster.github.io/moonbit-pathfinding/)。 -2. **生态工程标杆** — 3331 个测试跨四后端(wasm-gc / native / js / wasm)、 +2. **生态工程标杆** — 3339 个测试跨四后端(wasm-gc / native / js / wasm)、 可执行 proof predicates、可执行 README(`moon test README.mbt.md`)、 DST + 差分 PBT、零警告 `--deny-warn` CI 门禁,以及已发布的 [对标 Rust `pathfinding` crate 正面对比](./benches/results/latest-rust-comparison.md) @@ -138,6 +138,15 @@ path = [0, 1, 2, 3] | 序列化工作台 | `moon run examples/serialization_studio` | serialization | .proto 解析校验、类型化二进制/JSON 往返、规范字节、破坏性变更检测、代码生成 | | DST 探索器 | `moon run examples/dst_explorer` | dst | 种子确定性重放、分区/崩溃注错、DPOR 探索、缩小最小复现、线性一致性检查 | | 配置与差分运维 | `moon run examples/config_diff_ops` | infra_config + infra_diff | TOML/INI 解析、统一 diff、补丁应用/回退、diff3 三方合并冲突、semver 门禁 | +| 哈希完整性 | `moon run examples/hash_integrity` | infra_hash | SHA-2/SHA-3/BLAKE2b 摘要(sha256 与 `sha256sum` 一致)、HMAC 防篡改、HKDF/PBKDF2 派生、流式 == 一次性、xxHash 分片 | +| 压缩工作台 | `moon run examples/compress_workbench` | infra_compress | DEFLATE/zlib/gzip/zstd/LZ4 压缩率对比、无损往返、字典压缩、损坏归档拒绝 | +| 时间调度器 | `moon run examples/time_scheduler` | infra_time + infra_timer | RFC 3339/2822 + strftime、公历运算、POSIX TZ 夏令时、时长往返、时间轮、工作窃取调度(真实 steal 计数) | +| 韧性网关 | `moon run examples/resilience_gateway` | infra_resilience | 封顶退避重试、熔断器状态机、令牌桶 vs 滑动窗口、隔板、AIMD、对冲请求 | +| CLI 开发工具 | `moon run examples/cli_devtool` | infra_cli | 子命令解析与默认值、带 choices 的类型化校验、拼写建议、短参展开、生成 help + bash 补全 | +| 观测套件 | `moon run examples/observability_kit` | infra_metrics | HDR 直方图尾部分位数、可合并 DDSketch 分位数、span 追踪 total vs self 时间 | +| 文本编辑器内核 | `moon run examples/text_editor_core` | infra_text + infra_ds | Rope 与 piece table 编辑收敛、字素/显示宽度、Myers 差分、LRU 淘汰、布隆过滤器、roaring 位图求交 | +| 解析器演练场 | `moon run examples/parser_playground` | parser_combinator | 优先级正确的表达式求值、JSON 精确错误位置渲染、错误恢复诊断、增量分块解析 | +| PBT 与 fuzz 实验室 | `moon run examples/pbt_fuzz_lab` | infra_pbt + infra_fuzz | 属性检查、缩小到边界反例(500)、分布统计、往返律、种子化图 fuzz 与结构缩小 | 一键验证示例输出 marker: @@ -150,6 +159,10 @@ pwsh -NoLogo -NoProfile -ExecutionPolicy Bypass -File scripts\examples_guard.ps1 与 [`docs/examples/latest-examples-run.json`](./docs/examples/latest-examples-run.json)。 +各方向上手教程(关键 API + 最小片段 + 对应 demo): +[`docs/zh/tutorials.md`](./docs/zh/tutorials.md)(英文完整版 +[`docs/tutorials/README.md`](./docs/tutorials/README.md))。 + --- ## 发布就绪 · Release Readiness diff --git a/benches/infra_compress_bench/infra_compress_bench.mbt b/benches/infra_compress_bench/infra_compress_bench.mbt new file mode 100644 index 0000000..e5b27d4 --- /dev/null +++ b/benches/infra_compress_bench/infra_compress_bench.mbt @@ -0,0 +1,48 @@ +// infra_compress_bench.mbt —— 压缩方向基准:DEFLATE / zstd(entropy) / +// LZ4 在重复性日志负载上的压缩 + 解压 round-trip 吞吐对比, +// smoke 校验三者均无损。 + +///| +fn cb_log_payload(lines : Int) -> Array[Int] { + let sb = StringBuilder::new() + for i in 0.. Array[Int] { + let out : Array[Int] = [] + let mut s = seed + for _ in 0.. +``` + +The pathfinding core is covered by the executable docs in +[`docs/zh/algorithms/`](../zh/algorithms/README.md); this handbook focuses on +the INFRA directions that ship alongside it. + +## 1. Pathfinding (BFS / Dijkstra / A*) + +Demos: `examples/maze_solver`, `examples/network_routing`, +`examples/eight_puzzle`. + +```moonbit +let result = @pathfinding.dijkstra( + start, + fn(n) { neighbors_of(n) }, // (N) -> Array[(N, W)] + fn(n) { n == goal }, +) +``` + +Nodes can be any `Eq + Hash` type, weights any `Weight + Compare` type. +See the per-algorithm executable docs for BFS/A*/JPS/CH/ALT and more. + +## 2. Mini compiler (`src/mini_compiler`) + +Demo: `examples/mini_compiler_pipeline` — lexer → parser → HM type +inference → optimizer → bytecode VM with tail-call optimization, +interpreter differential testing, and JS emission from one source program. + +## 3. Regex engine (`src/regex_engine`) + +Demo: `examples/regex_toolkit` — named captures, `replace_all` redaction, +`split`, and linear-time matching (ReDoS-resistant NFA simulation). + +## 4. Structured logging (`src/logging`) + +Demo: `examples/log_pipeline` — trace spans with W3C `traceparent` +propagation, JSON/logfmt/pretty renderers, PII redaction, env-filter. + +## 5. Actors (`src/actor`) + +Demo: `examples/actor_worker_pool`. + +```moonbit +let sys = @actor.ActorSystem::new() +// supervised pool: faults trigger restart per SupervisorSpec +// routing: round-robin / random / consistent-hash +// bounded mailboxes give natural backpressure +``` + +## 6. Build tool (`src/build_tool`) + +Demo: `examples/build_pipeline` — rule parsing, parallel wave scheduling, +minimal incremental rebuilds (dirty closure), cached execution, auto-bisect +for first-bad-commit search. + +## 7. Serialization (`src/serialization`) + +Demo: `examples/serialization_studio` — `.proto` parse/validate, typed +wire + JSON round-trips, canonical bytes, breaking-change detection. + +## 8. Deterministic simulation testing (`src/dst`) + +Demo: `examples/dst_explorer` — seeded deterministic replays, +partition/crash fault injection, DPOR exploration, shrinking, +linearizability checking. + +## 9. Config + diff (`src/infra_config`, `src/infra_diff`) + +Demo: `examples/config_diff_ops` — TOML/INI parsing, unified diffs, patch +apply/revert, diff3 three-way merge with conflict detection, SemVer gates. + +## 10. Hashing (`src/infra_hash`) + +Demo: `examples/hash_integrity`. + +```moonbit +@hash.sha256_hex(artifact) // matches sha256sum +let tag = @hash.hmac_sha256(key, message) // authentication tag +let okm = @hash.hkdf(ikm, salt, info, 32) // key derivation +let dk = @hash.pbkdf2_hmac_sha256(pw, salt, 1000, 32) +let h = @hash.Sha256Hasher::new() // streaming == one-shot +h.update(chunk1); h.update(chunk2); h.finalize() +let shard = (@hash.xxhash64(bytes, 42) % 3UL).to_int() +``` + +Rule of thumb (see `benches/results/infra-hash-compress-native-2026-07-12.md`): +xxHash64 is ~7.5× faster than SHA-256 at 16 KiB — use non-crypto hashes for +sharding, crypto digests for integrity. + +## 11. Compression (`src/infra_compress`) + +Demo: `examples/compress_workbench`. + +```moonbit +let c = @compress.deflate(data) // smallest here (46‰) +@compress.inflate(c) == Some(data) // lossless +let zs = @compress.zstd_compress_entropy(data, true) +let zd = @compress.zlib_compress_with_dict(small, dict) // shared dict wins on tiny payloads +@compress.gzip_decompress(corrupted) is None // CRC rejects tampering +``` + +LZ4 is ~7× faster round-trip than DEFLATE at 36 KB; zstd's entropy path +scales best as payloads grow. + +## 12. Time + timers (`src/infra_time`, `src/infra_timer`) + +Demo: `examples/time_scheduler`. + +```moonbit +let dt = @time.parse_iso8601("2026-07-12T18:30:00.250Z").unwrap() +@time.format_rfc2822(dt, 0) +let rule = @time.parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2").unwrap() +let (offset, name) = @time.tz_offset_at(rule, dt) // seconds, "EDT" +let d = @time.parse_duration("2h30m").unwrap() // Go-style durations +let wheel = @timer.TimerWheel::new() // hierarchical wheel +let ws = @timer.WorkStealingScheduler::new(4) // real steal counts +``` + +## 13. Resilience (`src/infra_resilience`) + +Demo: `examples/resilience_gateway`. + +```moonbit +let (ok, attempts) = @res.retry_run(policy, fn(i) { call(i) }) +let cb = @res.CircuitBreaker::create(3, 1000) // Closed→Open→HalfOpen→Closed +let tb = @res.TokenBucket::create(3, 1) +let bh = @res.Bulkhead::create(2, 1) // slots + queue +let aimd = @res.AimdLimiter::create(10, 1, 100) // initial, min, max +let fire = @res.hedge_schedule(policy) // hedged requests +``` + +## 14. CLI (`src/infra_cli`) + +Demo: `examples/cli_devtool` — subcommands, typed validation with choices, +`suggest_option` typo hints, bundled short flags, `help_text`, and +`completion_bash` generation. + +## 15. Metrics (`src/infra_metrics`) + +Demo: `examples/observability_kit`. + +```moonbit +let h = @metrics.HdrHistogram::new(3) +h.record(latency_ms) +h.value_at_percentile(99.9) +let s1 = @metrics.DDSketch::new(0.01) // mergeable across shards +s1.merge_with(s2) +let tracer = @metrics.SpanTracer::new() // total vs self time per span +``` + +## 16. Text + data structures (`src/infra_text`, `src/infra_ds`) + +Demo: `examples/text_editor_core`. + +```moonbit +let rope = @text.Rope::from_string(src) +rope.delete(12, 16) // half-open [lo, hi) +rope.insert(12, "println(\"hi\")") +rope.line_col_at(offset) +@text.grapheme_count("héllo 世界") // 8; display_width counts CJK as 2 +let ops = @text.myers_diff(before, after) +let lru = @ds.LruCache::create(2) +let bloom = @ds.BloomFilter::create(1024, 3) +let bitmap = @ds.RoaringBitmap::new() +``` + +Note `PieceTable::delete(off, count)` takes a count while +`Rope::delete(lo, hi)` takes a half-open range. + +## 17. Parser combinators (`src/parser_combinator`) + +Demo: `examples/parser_playground`. + +```moonbit +@pc.parse_and_eval("2 ^ 3 ^ 2") // 512 — right-assoc power, correct precedence +let json = @pc.parse_json(src) // errors carry rendered caret positions +let (value, errors) = @pc.parse_json_recover(bad) // recovery diagnostics +let g = @pc.lift(parser) +let step = g.run_incremental(chunk1) // NeedMore, not a false failure +let result = @pc.drive(step, [chunk2, chunk3], closed=true) +``` + +## 18. PBT + fuzzing (`src/infra_pbt`, `src/infra_fuzz`) + +Demo: `examples/pbt_fuzz_lab`. + +```moonbit +@pbt.holds_for_all(@pbt.gen_int_range(0, 1000), fn(n) { n + n == 2 * n }) +let r = @pbt.check_with_shrink(gen, prop, @pbt.shrink_int) // minimal counterexample +@pbt.round_trip(gen, encode, decode) // encode/decode law +let g = @fuzz.fuzz_graph_gen(5, 42) // seeded, reproducible graphs +@fuzz.shrink_fuzz_graph(failing) // structurally smaller candidates +``` + +## Benchmarks and evidence + +- Algorithm + INFRA native benchmarks live under `benches/`; results under + [`benches/results/`](../../benches/results/). +- Example outputs are guarded by `scripts/examples_guard.ps1`; latest + evidence in [`docs/examples/latest-examples-run.md`](../examples/latest-examples-run.md). +- Full acceptance gate: `bash scripts/acceptance.sh`. diff --git a/docs/zh/tutorials.md b/docs/zh/tutorials.md new file mode 100644 index 0000000..937c55f --- /dev/null +++ b/docs/zh/tutorials.md @@ -0,0 +1,37 @@ +# 上手教程 —— 每个方向五分钟 + +英文完整版见 [`docs/tutorials/README.md`](../tutorials/README.md)。本页按方向给出 +关键 API、最小片段与对应可运行 demo(片段均取自 `examples/` 下真实代码,可编译 +可运行)。任一 demo 用 `moon run examples/<名称>` 运行。 + +寻路核心算法的可执行文档见 [`docs/zh/algorithms/`](./algorithms/README.md)。 + +| 方向 | demo | 关键 API | +|---|---|---| +| 寻路核心 | maze_solver / network_routing / eight_puzzle | `dijkstra(start, neighbors, is_goal)`,节点任意 `Eq + Hash` 类型 | +| 迷你编译器 | mini_compiler_pipeline | 词法→语法→HM 推断→优化→字节码 VM+TCO、解释器差分、JS 发射 | +| 正则引擎 | regex_toolkit | 命名捕获、`replace_all`、`split`、线性时间抗 ReDoS | +| 结构化日志 | log_pipeline | trace span + W3C traceparent、JSON/logfmt/pretty、PII 脱敏 | +| Actor | actor_worker_pool | 监督重启(`SupervisorSpec`)、路由策略、有界邮箱背压 | +| 构建工具 | build_pipeline | 波次调度、脏闭包增量重建、缓存执行、auto-bisect | +| 序列化 | serialization_studio | .proto 解析、二进制/JSON 往返、破坏性变更检测 | +| DST | dst_explorer | 种子确定性重放、DPOR、缩小、线性一致性 | +| 配置+差分 | config_diff_ops | TOML/INI、统一 diff、patch 应用/回退、diff3、semver | +| 哈希 | hash_integrity | `sha256_hex`(与 sha256sum 一致)、`hmac_sha256`、`hkdf`、`pbkdf2_hmac_sha256`、流式 `Sha256Hasher`、`xxhash64` 分片 | +| 压缩 | compress_workbench | `deflate`/`zstd_compress_entropy`/`lz4_compress` 无损往返、`zlib_compress_with_dict` 字典、损坏 CRC 拒绝 | +| 时间+定时器 | time_scheduler | `parse_iso8601`、`parse_posix_tz` 夏令时、`parse_duration("2h30m")`、`TimerWheel`、`WorkStealingScheduler` | +| 韧性原语 | resilience_gateway | `retry_run` 封顶退避、`CircuitBreaker` 状态机、`TokenBucket`/`SlidingWindowLimiter`、`Bulkhead`、`AimdLimiter::create(initial, min, max)`、`hedge_schedule` | +| CLI | cli_devtool | 子命令解析、类型化校验 + choices、`suggest_option` 拼写建议、`help_text`、`completion_bash` | +| 指标 | observability_kit | `HdrHistogram` 尾部分位数、可合并 `DDSketch`、`SpanTracer` total vs self | +| 文本+数据结构 | text_editor_core | `Rope::delete(lo, hi)` 半开区间(`PieceTable::delete(off, count)` 是计数)、`grapheme_count`、`myers_diff`、`LruCache`、`BloomFilter`、`RoaringBitmap` | +| 解析器组合子 | parser_playground | `parse_and_eval`(优先级/结合性正确)、`parse_json_recover` 错误恢复、`run_incremental` + `drive` 增量解析 | +| PBT + fuzz | pbt_fuzz_lab | `holds_for_all`、`check_with_shrink` 最小反例、`round_trip` 往返律、`fuzz_graph_gen` 种子化图、`shrink_fuzz_graph` | + +## 性能证据 + +- 哈希/压缩原生基准(含解读): + [`benches/results/infra-hash-compress-native-2026-07-12.md`](../../benches/results/infra-hash-compress-native-2026-07-12.md) + —— 16 KiB 下 xxHash64 比 SHA-256 快约 7.5×;36 KB 下 LZ4 往返比 DEFLATE 快约 7×。 +- 其余方向基准见 `benches/`(timer/metrics/text/parser/codec/actor/dst 等), + 结果归档于 `benches/results/`。 +- 全量门禁:`bash scripts/acceptance.sh`。 diff --git a/docs/项目申报书.md b/docs/项目申报书.md index 9d01f03..0fced6e 100644 --- a/docs/项目申报书.md +++ b/docs/项目申报书.md @@ -8,7 +8,7 @@ | 参赛赛道 | MoonBit × CCF 开源创新大赛 | | 作品仓库(GitLink) | https://www.gitlink.org.cn/Taoyouce/moonbit-pathfinding | | GitHub 镜像 | https://github.com/Suquster/moonbit-pathfinding | -| 包发布(mooncakes.io) | https://mooncakes.io/docs/Suquster/moonbit-pathfinding (`moon add Suquster/moonbit-pathfinding`,当前版本 v0.1.0) | +| 包发布(mooncakes.io) | https://mooncakes.io/docs/Suquster/moonbit-pathfinding (`moon add Suquster/moonbit-pathfinding`,当前版本 v0.2.0) | | 开源许可 | Apache-2.0 | | 开发语言 | MoonBit | @@ -155,12 +155,15 @@ Rust `pathfinding` crate v4.11.0 公开 API + 预构建邻接表,`cargo --rele ## 七、用户体验 - **低摩擦安装**:`moon add Suquster/moonbit-pathfinding` 一行安装(已发布 - mooncakes.io v0.1.0)。 + mooncakes.io v0.2.0)。 - **交互式 Playground**:网格寻路可视化,由 `src/` 同一算法库编译为 wasm-gc 驱动。 - **可执行文档**:README 示例即测试,复制即可运行。 -- **全方向可运行 demo**:11 个 `moon run examples/...` 端到端工作流,寻路 +- **全方向可运行 demo**:20 个 `moon run examples/...` 端到端工作流,寻路 (迷宫 / 路由 / 八数码)与 INFRA(迷你编译器 / 正则 / 日志 / actor / - 构建工具 / 序列化 / DST / 配置与 diff)各方向同等闭环。 + 构建工具 / 序列化 / DST / 配置与 diff / 哈希 / 压缩 / 时间与定时器 / + 韧性原语 / CLI / 指标观测 / 文本与数据结构 / 解析器组合子 / PBT 与 + fuzz)各方向同等闭环;配套各方向上手教程 + (`docs/tutorials/README.md` 与 `docs/zh/tutorials.md`)。 - **AI-agent 友好**:后继函数式 API + `docs/AI_AGENT_USAGE.md` 用量指南。 --- @@ -192,4 +195,4 @@ FIPS 规范,对拍对象为权威参考实现(CPython、Go stdlib、python-l --- -*本申报书对应作品版本 v0.1.0,仓库与 mooncakes 包均已公开同步。* +*本申报书对应作品版本 v0.2.0,仓库与 mooncakes 包均已公开同步。* diff --git a/examples/cli_devtool/cli_devtool.mbt b/examples/cli_devtool/cli_devtool.mbt new file mode 100644 index 0000000..e62ee81 --- /dev/null +++ b/examples/cli_devtool/cli_devtool.mbt @@ -0,0 +1,93 @@ +// examples/cli_devtool — runnable demonstration of the `src/infra_cli` +// direction as a deploy-tool CLI: declare a command with a subcommand, +// parse args with defaults and bundled shorts, validate typed values with +// choices, suggest fixes for typos, and emit help + bash completion. +// +// Run with: +// moon run examples/cli_devtool + +///| +fn deploy_command() -> @cli.Command { + { + name: "shipit", + about: "deploy services to a cluster", + specs: [ + @cli.flag_spec("verbose", "v", "enable verbose logging"), + @cli.value_spec("region", "r", "target region", Some("us-east-1")), + ], + subcommands: [ + { + name: "deploy", + about: "roll out a service version", + specs: [ + @cli.value_spec("service", "s", "service name", None), + @cli.value_spec("replicas", "n", "replica count", Some("2")), + @cli.value_spec("env", "e", "target environment", Some("staging")), + @cli.flag_spec("dry-run", "d", "plan only, no changes"), + ], + subcommands: [], + }, + ], + } +} + +///| +fn main { + let cmd = deploy_command() + println("=== 1. parse a realistic invocation ===") + let argv = [ + "deploy", "--service", "router", "-n", "4", "--env", "prod", "--dry-run", + ] + match @cli.parse(cmd, argv) { + Ok(parsed) => { + println("command path : \{parsed.command_path.join(" ")}") + let svc = parsed.value_of("service").unwrap_or("?") + println("service : \{svc}") + match parsed.int_of("replicas") { + Ok(n) => println("replicas : \{n} (typed Int64)") + Err(e) => println("replicas err : \{e}") + } + println("dry-run flag : \{parsed.has_flag("dry-run")}") + let region = parsed.value_of("region").unwrap_or("us-east-1") + println("region : \{region} (default applies)") + println("") + println("=== 2. typed validation with choices ===") + let errors = @cli.validate_rules(parsed, [ + @cli.rule("replicas", required=true, vtype=VInt), + @cli.rule("env", required=true, choices=["staging", "prod"]), + ]) + println("rule errors : \{errors.length()} (all rules satisfied)") + } + Err(e) => println("parse error: \{e}") + } + println("") + println("=== 3. helpful failure: unknown flag + typo suggestion ===") + match @cli.parse(cmd, ["deploy", "--servcie", "router"]) { + Ok(_) => println("unexpectedly parsed") + Err(e) => { + println("error : \{e}") + match @cli.suggest_option("servcie", ["service", "replicas", "env"]) { + Some(s) => println("did you mean : --\{s}") + None => println("no suggestion") + } + } + } + println("") + println("=== 4. bundled short flags expand ===") + let expanded = @cli.expand_bundled_shorts(["-vd"], [ + @cli.flag_spec("verbose", "v", ""), + @cli.flag_spec("dry-run", "d", ""), + ]) + println("-vd expands to: \{expanded.join(" ")}") + println("") + println("=== 5. generated help ===") + println(@cli.help_text(cmd)) + println("=== 6. bash completion (first lines) ===") + let script = @cli.completion_bash(cmd) + let lines = script.split("\n").collect() + let upto = if lines.length() < 6 { lines.length() } else { 6 } + for i in 0.. Array[Int] { + let sb = StringBuilder::new() + for i in 0..<40 { + sb.write_string( + "2026-07-12T18:0\{i % 10}:00Z INFO request handled path=/api/v1/route status=200\n", + ) + } + @codec.utf8_bytes_of_string(sb.to_string()) +} + +///| +fn ratio_permille(compressed : Int, original : Int) -> Int { + compressed * 1000 / original +} + +///| +fn main { + let data = build_log_payload() + let n = data.length() + println("=== 1. codec shoot-out on a \{n}-byte log payload ===") + let df = @compress.deflate(data) + let zl = @compress.zlib_compress(data) + let gz = @compress.gzip_compress(data) + let zs = @compress.zstd_compress_entropy(data, true) + let lz = @compress.lz4_compress(data) + println( + "deflate: \{df.length()} bytes (\{ratio_permille(df.length(), n)}‰)", + ) + println( + "zlib : \{zl.length()} bytes (\{ratio_permille(zl.length(), n)}‰)", + ) + println( + "gzip : \{gz.length()} bytes (\{ratio_permille(gz.length(), n)}‰)", + ) + println( + "zstd : \{zs.length()} bytes (\{ratio_permille(zs.length(), n)}‰)", + ) + println( + "lz4 : \{lz.length()} bytes (\{ratio_permille(lz.length(), n)}‰)", + ) + println("") + println("=== 2. lossless round-trips ===") + println("inflate(deflate) == input: \{@compress.inflate(df) == Some(data)}") + println( + "zlib_decompress == input: \{@compress.zlib_decompress(zl) == Some(data)}", + ) + println( + "gzip_decompress == input: \{@compress.gzip_decompress(gz) == Some(data)}", + ) + println( + "zstd_decompress == input: \{@compress.zstd_decompress(zs, n) == Some(data)}", + ) + println( + "lz4_decompress == input: \{@compress.lz4_decompress(lz, n) == Some(data)}", + ) + let frame = @compress.lz4_frame_compress(data) + println( + "lz4 frame decode == input: \{@compress.lz4_frame_decompress(frame, n) == Some(data)}", + ) + println("") + println("=== 3. dictionary compression for small payloads ===") + let dict = @codec.utf8_bytes_of_string( + "INFO request handled path=/api/v1/route status=200", + ) + let small = @codec.utf8_bytes_of_string( + "INFO request handled path=/api/v1/route status=200 rt=3ms", + ) + let plain = @compress.zlib_compress(small) + let with_dict = @compress.zlib_compress_with_dict(small, dict) + println("zlib plain : \{plain.length()} bytes") + println("zlib with dict: \{with_dict.length()} bytes") + println( + "dict round-trip ok: \{@compress.zlib_decompress_with_dict(with_dict, dict) == Some(small)}", + ) + println("") + println("=== 4. corrupted archives are rejected, not mis-decoded ===") + let bad = gz.copy() + bad[bad.length() - 1] = (bad[bad.length() - 1] + 1) % 256 + let bad_verdict = match @compress.gzip_decompress(bad) { + Some(_) => "accepted (unexpected)" + None => "rejected" + } + println("gzip with flipped CRC byte -> \{bad_verdict}") + let truncated = zl.copy() + let _ = truncated.pop() + let trunc_verdict = match @compress.zlib_decompress(truncated) { + Some(_) => "accepted (unexpected)" + None => "rejected" + } + println("zlib truncated by 1 byte -> \{trunc_verdict}") +} diff --git a/examples/compress_workbench/moon.pkg b/examples/compress_workbench/moon.pkg new file mode 100644 index 0000000..a993fd4 --- /dev/null +++ b/examples/compress_workbench/moon.pkg @@ -0,0 +1,17 @@ +// Runnable example: compress_workbench +// +// Compression workbench with the `src/infra_compress` direction: +// DEFLATE / zlib / gzip / zstd / LZ4 round-trips with ratios, dictionary +// compression, and corrupted-input rejection. +// +// Run with: +// moon run examples/compress_workbench + +import { + "Suquster/moonbit-pathfinding/src/infra_compress" @compress, + "Suquster/moonbit-pathfinding/src/infra_codec" @codec, +} + +options( + "is-main": true, +) diff --git a/examples/compress_workbench/pkg.generated.mbti b/examples/compress_workbench/pkg.generated.mbti new file mode 100644 index 0000000..64822a0 --- /dev/null +++ b/examples/compress_workbench/pkg.generated.mbti @@ -0,0 +1,13 @@ +// Generated using `moon info`, DON'T EDIT IT +package "Suquster/moonbit-pathfinding/examples/compress_workbench" + +// Values + +// Errors + +// Types and methods + +// Type aliases + +// Traits + diff --git a/examples/hash_integrity/hash_integrity.mbt b/examples/hash_integrity/hash_integrity.mbt new file mode 100644 index 0000000..48b435a --- /dev/null +++ b/examples/hash_integrity/hash_integrity.mbt @@ -0,0 +1,72 @@ +// examples/hash_integrity — runnable demonstration of the `src/infra_hash` +// direction as a realistic artifact-integrity workflow: digest a release +// artifact with the SHA-2 / SHA-3 / BLAKE2b families, authenticate it with +// HMAC, derive session keys via HKDF, stretch a password via PBKDF2, +// checksum streams incrementally, and pick fast non-crypto hashes for +// hash-table sharding. +// +// Run with: +// moon run examples/hash_integrity + +///| +fn main { + let artifact = "moonbit-pathfinding-v0.1.0.tar" + println("=== 1. release artifact digests (hex) ===") + println("sha256 : \{@hash.sha256_hex(artifact)}") + println("sha512 : \{@hash.sha512_hex(artifact)}") + println("sha3-256: \{@hash.sha3_256_hex(artifact)}") + println("blake2b : \{@hash.blake2b_hex(artifact)}") + println("md5 : \{@hash.md5_hex(artifact)} (legacy, display only)") + println("") + println("=== 2. HMAC authentication of a manifest ===") + let key = @hash.utf8_bytes("release-signing-key") + let manifest = @hash.utf8_bytes("name=pathfinding;version=0.1.0") + let tag = @hash.hmac_sha256(key, manifest) + println("hmac-sha256: \{@hash.digest_hex(tag)}") + let tampered = @hash.utf8_bytes("name=pathfinding;version=9.9.9") + let tag2 = @hash.hmac_sha256(key, tampered) + println("tamper detected (tags differ): \{tag != tag2}") + println("") + println("=== 3. HKDF session keys from a shared secret ===") + let ikm = @hash.utf8_bytes("shared-ecdh-secret") + let salt = @hash.utf8_bytes("handshake-salt") + let info = @hash.utf8_bytes("session-encryption") + match @hash.hkdf(ikm, salt, info, 32) { + Some(okm) => println("hkdf-sha256 (32B): \{@hash.digest_hex(okm)}") + None => println("hkdf failed") + } + println("") + println("=== 4. PBKDF2 password stretching ===") + let pw = @hash.utf8_bytes("correct horse battery staple") + let pw_salt = @hash.utf8_bytes("per-user-salt") + match @hash.pbkdf2_hmac_sha256(pw, pw_salt, 1000, 32) { + Some(dk) => println("pbkdf2 (1000 iters, 32B): \{@hash.digest_hex(dk)}") + None => println("pbkdf2 failed") + } + println("") + println("=== 5. streaming checksum equals one-shot ===") + let whole = @hash.utf8_bytes("chunk-a|chunk-b|chunk-c") + let one_shot = @hash.crc32(whole) + let h = @hash.Crc32Hasher::new() + h.update(@hash.utf8_bytes("chunk-a|")) + h.update(@hash.utf8_bytes("chunk-b|")) + h.update(@hash.utf8_bytes("chunk-c")) + let streamed = h.finalize() + println("crc32 one-shot : \{one_shot}") + println("crc32 streamed : \{streamed} (equal: \{one_shot == streamed})") + let sh = @hash.Sha256Hasher::new() + sh.update(@hash.utf8_bytes("chunk-a|chunk-b|")) + sh.update(@hash.utf8_bytes("chunk-c")) + let sha_stream = sh.finalize() + println("sha256 streamed == one-shot: \{sha_stream == @hash.sha256(whole)}") + println("") + println("=== 6. fast non-crypto hashes for sharding ===") + let keys = ["user-1", "user-2", "user-3", "user-4"] + for k in keys { + let b = @hash.utf8_bytes(k) + let shard = (@hash.xxhash64(b, 42) % 3UL).to_int() + println( + "\{k}: xxh64 shard #\{shard}, fnv1a=\{@hash.fnv1a_32(b)}, murmur3=\{@hash.murmur3_32(b, 0)}", + ) + } +} diff --git a/examples/hash_integrity/moon.pkg b/examples/hash_integrity/moon.pkg new file mode 100644 index 0000000..1977a07 --- /dev/null +++ b/examples/hash_integrity/moon.pkg @@ -0,0 +1,16 @@ +// Runnable example: hash_integrity +// +// Hashing / MAC / KDF toolchain with the `src/infra_hash` direction: +// FIPS digests (SHA-2 / SHA-3 / BLAKE2b / MD5 / SHA-1), HMAC, HKDF, +// PBKDF2, streaming hashers, and fast non-crypto hashes for tables. +// +// Run with: +// moon run examples/hash_integrity + +import { + "Suquster/moonbit-pathfinding/src/infra_hash" @hash, +} + +options( + "is-main": true, +) diff --git a/examples/hash_integrity/pkg.generated.mbti b/examples/hash_integrity/pkg.generated.mbti new file mode 100644 index 0000000..b8001cf --- /dev/null +++ b/examples/hash_integrity/pkg.generated.mbti @@ -0,0 +1,13 @@ +// Generated using `moon info`, DON'T EDIT IT +package "Suquster/moonbit-pathfinding/examples/hash_integrity" + +// Values + +// Errors + +// Types and methods + +// Type aliases + +// Traits + diff --git a/examples/observability_kit/moon.pkg b/examples/observability_kit/moon.pkg new file mode 100644 index 0000000..f3b5f09 --- /dev/null +++ b/examples/observability_kit/moon.pkg @@ -0,0 +1,16 @@ +// Runnable example: observability_kit +// +// Metrics toolchain with the `src/infra_metrics` direction: HDR histogram +// latency percentiles, mergeable DDSketch quantiles with bounded relative +// error, and a span tracer with self/total time aggregation. +// +// Run with: +// moon run examples/observability_kit + +import { + "Suquster/moonbit-pathfinding/src/infra_metrics" @metrics, +} + +options( + "is-main": true, +) diff --git a/examples/observability_kit/observability_kit.mbt b/examples/observability_kit/observability_kit.mbt new file mode 100644 index 0000000..f4d5345 --- /dev/null +++ b/examples/observability_kit/observability_kit.mbt @@ -0,0 +1,71 @@ +// examples/observability_kit — runnable demonstration of the +// `src/infra_metrics` direction as a service-latency dashboard: record +// request latencies into an HDR histogram, read tail percentiles, merge +// per-shard DDSketches with bounded relative error, and profile a request +// with a span tracer (total vs self time). +// +// Run with: +// moon run examples/observability_kit + +///| +fn main { + println("=== 1. HDR histogram latency percentiles ===") + let hdr = @metrics.HdrHistogram::new(3) + // simulate 1000 requests: mostly fast, a slow tail + for i in 0..<1000 { + let latency = if i % 100 == 0 { + (900 + i / 10).to_int64() // 1% slow outliers + } else { + (10 + i % 40).to_int64() // 10-49 ms steady state + } + hdr.record(latency) + } + println("count : \{hdr.count()}, max: \{hdr.max_value()} ms") + for p in [50.0, 90.0, 99.0, 99.9] { + match hdr.value_at_percentile(p) { + Some(v) => println("p\{p} : \{v} ms") + None => println("p\{p} : n/a") + } + } + println("") + println("=== 2. mergeable DDSketch across shards ===") + let shard_a = @metrics.DDSketch::new(0.01) + let shard_b = @metrics.DDSketch::new(0.01) + for i in 0..<500 { + shard_a.add(10.0 + (i % 50).to_double()) + shard_b.add(200.0 + (i % 300).to_double()) + } + shard_a.merge_with(shard_b) + println( + "merged count : \{shard_a.count()} (error bound \{shard_a.error_bound()})", + ) + match shard_a.quantile(0.5) { + Some(q) => println("merged p50 : \{q}") + None => println("merged p50 : n/a") + } + match shard_a.quantile(0.99) { + Some(q) => println("merged p99 : \{q}") + None => println("merged p99 : n/a") + } + println("") + println("=== 3. span tracer: total vs self time ===") + let tracer = @metrics.SpanTracer::new() + let root = tracer.start("handle_request", 0) + let _db = tracer.start("db_query", 10) + let _ = tracer.end(60) // db_query: 50ms + let _render = tracer.start("render", 65) + let _ = tracer.end(90) // render: 25ms + let _ = tracer.end(100) // handle_request: 100ms total + println("finished spans: \{tracer.finished_count()}") + for name in tracer.stat_names() { + match tracer.stat(name) { + Some((calls, total, self_time)) => + println( + "\{name}: calls=\{calls}, total=\{total}ms, self=\{self_time}ms", + ) + None => () + } + } + let children = tracer.children_of(root) + println("root has \{children.length()} child spans") +} diff --git a/examples/observability_kit/pkg.generated.mbti b/examples/observability_kit/pkg.generated.mbti new file mode 100644 index 0000000..3104f31 --- /dev/null +++ b/examples/observability_kit/pkg.generated.mbti @@ -0,0 +1,13 @@ +// Generated using `moon info`, DON'T EDIT IT +package "Suquster/moonbit-pathfinding/examples/observability_kit" + +// Values + +// Errors + +// Types and methods + +// Type aliases + +// Traits + diff --git a/examples/parser_playground/moon.pkg b/examples/parser_playground/moon.pkg new file mode 100644 index 0000000..1bbb3dc --- /dev/null +++ b/examples/parser_playground/moon.pkg @@ -0,0 +1,16 @@ +// Runnable example: parser_playground +// +// Parser-combinator toolchain with the `src/parser_combinator` direction: +// arithmetic expression parsing and evaluation, JSON parsing with error +// rendering, error recovery, and incremental (streaming) parsing. +// +// Run with: +// moon run examples/parser_playground + +import { + "Suquster/moonbit-pathfinding/src/parser_combinator" @pc, +} + +options( + "is-main": true, +) diff --git a/examples/parser_playground/parser_playground.mbt b/examples/parser_playground/parser_playground.mbt new file mode 100644 index 0000000..f3c7faf --- /dev/null +++ b/examples/parser_playground/parser_playground.mbt @@ -0,0 +1,78 @@ +// examples/parser_playground — runnable demonstration of the +// `src/parser_combinator` direction as a language-tooling workflow: parse +// and evaluate arithmetic with correct precedence/associativity, parse +// JSON with precise error positions, recover from malformed JSON while +// collecting diagnostics, and feed a grammar incrementally chunk by chunk. +// +// Run with: +// moon run examples/parser_playground + +///| +fn main { + println("=== 1. arithmetic: parse + eval with precedence ===") + for src in ["1 + 2 * 3", "(1 + 2) * 3", "2 ^ 3 ^ 2", "10 - 4 - 3"] { + match @pc.parse_and_eval(src) { + Ok(v) => println("\{src} = \{v}") + Err(_) => println("\{src} -> parse error") + } + } + println("") + println("=== 2. expression AST round-trip ===") + let parser = @pc.expr_parser() + match parser.parse_string("1 + 2 * 3") { + Ok(ast, _) => + println("printed AST: \{@pc.print_expr(ast)} -> \{@pc.eval_expr(ast)}") + Fail(_, ..) => println("parse failed") + } + println("") + println("=== 3. JSON parsing with precise errors ===") + match + @pc.parse_json( + "{\"name\": \"router\", \"port\": 8080, \"tags\": [\"a\", \"b\"]}", + ) { + Ok(json) => println("parsed json : \{@pc.print_json(json)}") + Err(_) => println("unexpected parse failure") + } + let bad = "{\"name\": \"router\", \"port\": }" + let jp = @pc.json_parser() + match jp.explain_failure(bad) { + Some(msg) => println("bad json : \{msg}") + None => println("bad json unexpectedly parsed") + } + println("") + println("=== 4. error recovery collects diagnostics ===") + let (recovered, errors) = @pc.parse_json_recover("[1, oops, 3]") + match recovered { + Some(json) => println("recovered : \{@pc.print_json(json)}") + None => println("no recovery possible") + } + println("diagnostics : \{errors.length()} error(s) collected") + println("") + println("=== 5. incremental parsing, chunk by chunk ===") + // framed-number protocol: '<' digits '>' — a prefix that fails at the + // data boundary yields NeedMore instead of a false parse failure. + let digits = @pc.many1( + @pc.satisfy(fn(c) { c >= '0' && c <= '9' }, label="digit"), + ) + let framed = @pc.map(@pc.between(@pc.pchar('<'), digits, @pc.pchar('>')), fn( + ds, + ) { + let mut n = 0 + for c in ds { + n = n * 10 + (c.to_int() - '0'.to_int()) + } + n + }) + let grammar = @pc.lift(framed) + let first = grammar.run_incremental("<12") + match first { + NeedMore(_) => + println("after chunk '<12': parser needs more input (no false failure)") + Done(_) => println("after chunk '<12': unexpectedly done") + } + let result = @pc.drive(first, ["34", "5>"], closed=true) + match result.value() { + Some(n) => println("streamed 3 chunks '<12'+'34'+'5>' -> \{n}") + None => println("stream parse failed") + } +} diff --git a/examples/parser_playground/pkg.generated.mbti b/examples/parser_playground/pkg.generated.mbti new file mode 100644 index 0000000..b1e7889 --- /dev/null +++ b/examples/parser_playground/pkg.generated.mbti @@ -0,0 +1,13 @@ +// Generated using `moon info`, DON'T EDIT IT +package "Suquster/moonbit-pathfinding/examples/parser_playground" + +// Values + +// Errors + +// Types and methods + +// Type aliases + +// Traits + diff --git a/examples/pbt_fuzz_lab/moon.pkg b/examples/pbt_fuzz_lab/moon.pkg new file mode 100644 index 0000000..1f0a58d --- /dev/null +++ b/examples/pbt_fuzz_lab/moon.pkg @@ -0,0 +1,18 @@ +// Runnable example: pbt_fuzz_lab +// +// Property-based testing + fuzzing toolchain with the `src/infra_pbt` and +// `src/infra_fuzz` directions: generators, properties with shrinking +// counterexamples, distribution stats, encode/decode round-trip laws, and +// random graph generation with structural shrinking. +// +// Run with: +// moon run examples/pbt_fuzz_lab + +import { + "Suquster/moonbit-pathfinding/src/infra_pbt" @pbt, + "Suquster/moonbit-pathfinding/src/infra_fuzz" @fuzz, +} + +options( + "is-main": true, +) diff --git a/examples/pbt_fuzz_lab/pbt_fuzz_lab.mbt b/examples/pbt_fuzz_lab/pbt_fuzz_lab.mbt new file mode 100644 index 0000000..9eb00f0 --- /dev/null +++ b/examples/pbt_fuzz_lab/pbt_fuzz_lab.mbt @@ -0,0 +1,89 @@ +// examples/pbt_fuzz_lab — runnable demonstration of the `src/infra_pbt` + +// `src/infra_fuzz` directions as a testing-lab workflow: check properties +// over generated inputs, watch a false property shrink to a minimal +// counterexample, inspect generator distribution stats, verify an +// encode/decode round-trip law, and generate + shrink random graphs for +// differential fuzzing. +// +// Run with: +// moon run examples/pbt_fuzz_lab + +///| +fn main { + println("=== 1. a true property holds for all generated inputs ===") + let gen_small = @pbt.gen_int_range(0, 1000) + let holds = @pbt.holds_for_all(gen_small, fn(n) { n + n == 2 * n }) + println("forall n in [0,1000): n + n == 2n -> \{holds}") + println("") + println("=== 2. a false property shrinks to a minimal counterexample ===") + // claim: every number is < 500 — false, and shrinking should drive the + // counterexample down to the boundary 500. + let result = @pbt.check_with_shrink( + gen_small, + fn(n) { n < 500 }, + @pbt.shrink_int, + ) + match (result.passed, result.counterexample) { + (false, Some(cex)) => + println("property n < 500 failed, shrunk counterexample: \{cex}") + (true, _) => println("property unexpectedly passed") + (false, None) => println("failed without counterexample") + } + println("") + println("=== 3. generator distribution stats ===") + let gen_dice = @pbt.frequency([ + (1, @pbt.gen_int_range(0, 10)), + (3, @pbt.gen_int_range(10, 100)), + ]) + let (ok, stats) = @pbt.holds_for_all_stats(gen_dice, fn(_n) { true }, fn(n) { + if n < 10 { + "small" + } else { + "large" + } + }) + println( + "weighted 1:3 generator over \{stats.total()} samples -> small=\{stats.count("small")}, large=\{stats.count("large")} (ok=\{ok})", + ) + println("") + println("=== 4. encode/decode round-trip law ===") + let rt = @pbt.round_trip( + @pbt.gen_int_range(0, 65536), + fn(n) { + let b = Bytes::makei(2, fn(i) { + if i == 0 { + (n / 256 % 256).to_byte() + } else { + (n % 256).to_byte() + } + }) + b + }, + fn(b) { + if b.length() == 2 { + Some(b[0].to_int() * 256 + b[1].to_int()) + } else { + None + } + }, + ) + println("decode(encode(n)) == n for u16 big-endian: \{rt}") + println("") + println("=== 5. random graphs for differential fuzzing ===") + let rng = @pbt.rng_new(42) + let g = @fuzz.fuzz_graph_gen(8, 16).generate(rng) + println("generated graph: \{g.n} nodes, \{g.edges.length()} edges (seed 42)") + let g2 = @fuzz.fuzz_graph_gen(8, 16).generate(@pbt.rng_new(42)) + println("same seed reproduces identical graph: \{g == g2}") + println("") + println("=== 6. structural shrinking of a failing graph ===") + let candidates = @fuzz.shrink_fuzz_graph(g) + println("shrink candidates: \{candidates.length()}") + let mut smaller = 0 + for c in candidates { + if c.edges.length() < g.edges.length() || c.n < g.n { + smaller = smaller + 1 + } + } + println("all candidates strictly smaller: \{smaller == candidates.length()}") +} diff --git a/examples/pbt_fuzz_lab/pkg.generated.mbti b/examples/pbt_fuzz_lab/pkg.generated.mbti new file mode 100644 index 0000000..a07e6c7 --- /dev/null +++ b/examples/pbt_fuzz_lab/pkg.generated.mbti @@ -0,0 +1,13 @@ +// Generated using `moon info`, DON'T EDIT IT +package "Suquster/moonbit-pathfinding/examples/pbt_fuzz_lab" + +// Values + +// Errors + +// Types and methods + +// Type aliases + +// Traits + diff --git a/examples/resilience_gateway/moon.pkg b/examples/resilience_gateway/moon.pkg new file mode 100644 index 0000000..ff9fbc5 --- /dev/null +++ b/examples/resilience_gateway/moon.pkg @@ -0,0 +1,17 @@ +// Runnable example: resilience_gateway +// +// Resilience toolchain with the `src/infra_resilience` direction: +// exponential backoff with jitter, retry budgets, circuit breakers, +// token-bucket and sliding-window rate limiting, bulkhead isolation, +// AIMD adaptive concurrency, and hedged requests. +// +// Run with: +// moon run examples/resilience_gateway + +import { + "Suquster/moonbit-pathfinding/src/infra_resilience" @res, +} + +options( + "is-main": true, +) diff --git a/examples/resilience_gateway/pkg.generated.mbti b/examples/resilience_gateway/pkg.generated.mbti new file mode 100644 index 0000000..9947140 --- /dev/null +++ b/examples/resilience_gateway/pkg.generated.mbti @@ -0,0 +1,13 @@ +// Generated using `moon info`, DON'T EDIT IT +package "Suquster/moonbit-pathfinding/examples/resilience_gateway" + +// Values + +// Errors + +// Types and methods + +// Type aliases + +// Traits + diff --git a/examples/resilience_gateway/resilience_gateway.mbt b/examples/resilience_gateway/resilience_gateway.mbt new file mode 100644 index 0000000..fddb33e --- /dev/null +++ b/examples/resilience_gateway/resilience_gateway.mbt @@ -0,0 +1,94 @@ +// examples/resilience_gateway — runnable demonstration of the +// `src/infra_resilience` direction as an API-gateway workflow: retry a +// flaky upstream with capped exponential backoff, trip and recover a +// circuit breaker, rate-limit clients with token bucket and sliding +// window, isolate tenants with a bulkhead, adapt concurrency with AIMD, +// and cut tail latency with hedged requests. +// +// Run with: +// moon run examples/resilience_gateway + +///| +fn main { + println("=== 1. capped exponential backoff + retry ===") + let policy : @res.BackoffPolicy = { + base_ms: 100, + cap_ms: 2000, + max_retries: 6, + } + let sched = [] + for attempt in 0..<6 { + sched.push(@res.backoff_delay(policy, attempt).to_string()) + } + println("delays (ms) : \{sched.join(", ")}") + println( + "jittered #3 : \{@res.backoff_delay_jittered(policy, 3, 42)} ms (deterministic seed)", + ) + let (ok, attempts) = @res.retry_run(policy, fn(attempt) { attempt >= 2 }) + println("flaky upstream succeeds on 3rd try: ok=\{ok}, attempts=\{attempts}") + println("") + println("=== 2. circuit breaker state machine ===") + let cb = @res.CircuitBreaker::create(3, 1000) + println("initial : \{cb.state_of()}") + cb.record_failure(0) + cb.record_failure(10) + cb.record_failure(20) + println("3 failures : \{cb.state_of()} (allow now: \{cb.allow(30)})") + println("after cooldown: allow=\{cb.allow(1500)} -> \{cb.state_of()}") + cb.record_success() + println("probe success: \{cb.state_of()}") + println("") + println("=== 3. rate limiting: token bucket vs sliding window ===") + let bucket = @res.TokenBucket::create(3, 1) + let bucket_hits = [] + for i in 0..<5 { + bucket_hits.push(bucket.try_acquire(i.to_int64() * 10).to_string()) + } + println("token bucket (cap 3): \{bucket_hits.join(", ")}") + let win = @res.SlidingWindowLimiter::create(1000, 3) + let win_hits = [] + for i in 0..<5 { + win_hits.push(win.try_acquire(i.to_int64() * 100).to_string()) + } + println("sliding window (3/s) : \{win_hits.join(", ")}") + println("after window slides : \{win.try_acquire(1500)}") + println("") + println("=== 4. bulkhead isolation per tenant ===") + let bh = @res.Bulkhead::create(2, 1) + let admissions = [] + for _i in 0..<4 { + let verdict = match bh.try_acquire() { + Admitted => "admitted" + Enqueued => "enqueued" + Rejected => "rejected" + } + admissions.push(verdict) + } + println("4 concurrent calls (2 slots + 1 queue): \{admissions.join(", ")}") + let _ = bh.release() + println( + "after release: in-flight=\{bh.in_flight_count()}, queued=\{bh.queued_count()}, rejected=\{bh.rejected_count()}", + ) + println("") + println("=== 5. AIMD adaptive concurrency ===") + let aimd = @res.AimdLimiter::create(10, 1, 100) + for _i in 0..<5 { + aimd.on_success() + } + println("after 5 successes : limit = \{aimd.current_limit()}") + aimd.on_overload() + println( + "after overload : limit = \{aimd.current_limit()} (multiplicative decrease)", + ) + println("") + println("=== 6. hedged requests cut tail latency ===") + let hedge : @res.HedgePolicy = { hedge_after_ms: 50, max_hedges: 2 } + println( + "hedge fire times : \{@res.hedge_schedule(hedge).map(fn(t) { t.to_string() }).join(", ")} ms", + ) + let latencies = [300, 80, 120] + let (winner, latency, wasted) = @res.hedge_outcome(hedge, latencies) + println( + "upstream latencies \{latencies.map(fn(t) { t.to_string() }).join("/")}ms -> winner #\{winner} at \{latency}ms (wasted \{wasted})", + ) +} diff --git a/examples/text_editor_core/moon.pkg b/examples/text_editor_core/moon.pkg new file mode 100644 index 0000000..54fd58e --- /dev/null +++ b/examples/text_editor_core/moon.pkg @@ -0,0 +1,18 @@ +// Runnable example: text_editor_core +// +// Text + data-structure toolchain with the `src/infra_text` and +// `src/infra_ds` directions: rope editing with line/column mapping, +// piece-table edits, grapheme clusters and display width, Myers character +// diff, plus LRU cache, bloom filter, and roaring bitmap indexes. +// +// Run with: +// moon run examples/text_editor_core + +import { + "Suquster/moonbit-pathfinding/src/infra_text" @text, + "Suquster/moonbit-pathfinding/src/infra_ds" @ds, +} + +options( + "is-main": true, +) diff --git a/examples/text_editor_core/pkg.generated.mbti b/examples/text_editor_core/pkg.generated.mbti new file mode 100644 index 0000000..9f3b458 --- /dev/null +++ b/examples/text_editor_core/pkg.generated.mbti @@ -0,0 +1,13 @@ +// Generated using `moon info`, DON'T EDIT IT +package "Suquster/moonbit-pathfinding/examples/text_editor_core" + +// Values + +// Errors + +// Types and methods + +// Type aliases + +// Traits + diff --git a/examples/text_editor_core/text_editor_core.mbt b/examples/text_editor_core/text_editor_core.mbt new file mode 100644 index 0000000..2eea4b0 --- /dev/null +++ b/examples/text_editor_core/text_editor_core.mbt @@ -0,0 +1,82 @@ +// examples/text_editor_core — runnable demonstration of the +// `src/infra_text` + `src/infra_ds` directions as an editor-backend +// workflow: edit a buffer through a rope with line/column mapping, apply +// the same edits through a piece table, segment graphemes and compute +// terminal display width, produce a Myers character diff, and back the +// editor with LRU / bloom / roaring-bitmap indexes. +// +// Run with: +// moon run examples/text_editor_core + +///| +fn main { + println("=== 1. rope editing with line/column mapping ===") + let rope = @text.Rope::from_string("fn main {\n todo\n}\n") + rope.delete(12, 16) // half-open range [12, 16) removes "todo" + rope.insert(12, "println(\"hi\")") + println("buffer after edit:\n\{rope.to_string()}") + println( + "len \{rope.len()}, lines \{rope.line_count()}, depth \{rope.depth()}", + ) + let (line, col) = rope.line_col_at(14) + println("offset 14 -> line \{line}, col \{col}") + println("line 1 starts at offset \{rope.offset_of_line(1)}") + println("") + println("=== 2. piece table applies the same edit ===") + let pt = @text.PieceTable::from_string("fn main {\n todo\n}\n") + pt.delete(12, 4) + pt.insert(12, "println(\"hi\")") + println("piece table == rope: \{pt.to_string() == rope.to_string()}") + println("pieces: \{pt.piece_count()}, len: \{pt.len()}") + println("") + println("=== 3. graphemes + display width ===") + let s = "héllo 世界" + println("string : \{s}") + println("graphemes : \{@text.grapheme_count(s)}") + println("display width: \{@text.display_width(s)} (CJK counts as 2 cells)") + println("case-fold eq : \{@text.eq_fold("HÉLLO 世界", s)}") + println("") + println("=== 4. Myers character diff ===") + let before = "let x = compute(a, b)" + let after = "let x = compute(a, b, c)" + let ops = @text.myers_diff(before, after) + println("edit cost: \{@text.diff_edit_cost(ops)}") + println("apply(diff) == after : \{@text.diff_apply(ops) == after}") + println("source(diff) == before: \{@text.diff_source(ops) == before}") + println("") + println("=== 5. LRU cache for rendered lines ===") + let cache : @ds.LruCache[Int, String] = @ds.LruCache::create(2) + cache.put(1, "render:line-1") + cache.put(2, "render:line-2") + let _ = cache.get(1) // touch 1 so 2 becomes LRU + cache.put(3, "render:line-3") // evicts 2 + let hit1 = match cache.get(1) { + Some(_) => "hit" + None => "miss" + } + let hit2 = match cache.get(2) { + Some(_) => "hit" + None => "miss (evicted)" + } + println( + "line 1: \{hit1}, line 2: \{hit2}, size \{cache.size()}/\{cache.cap()}", + ) + println("") + println("=== 6. bloom filter + roaring bitmap indexes ===") + let bloom = @ds.BloomFilter::create(1024, 3) + bloom.insert("symbol:main") + bloom.insert("symbol:rope") + println("bloom has 'symbol:main' : \{bloom.contains("symbol:main")}") + println("bloom has 'symbol:nope' : \{bloom.contains("symbol:nope")}") + let dirty_lines = @ds.RoaringBitmap::new() + let _ = dirty_lines.add(1) + let _ = dirty_lines.add(2) + let _ = dirty_lines.add(100000) + let visible = @ds.RoaringBitmap::new() + let _ = visible.add(2) + let _ = visible.add(3) + let redraw = dirty_lines.intersect_with(visible) + println( + "dirty ∩ visible lines: \{redraw.to_array().map(fn(v) { v.to_string() }).join(", ")} (cardinality \{redraw.cardinality()})", + ) +} diff --git a/examples/time_scheduler/moon.pkg b/examples/time_scheduler/moon.pkg new file mode 100644 index 0000000..a155e11 --- /dev/null +++ b/examples/time_scheduler/moon.pkg @@ -0,0 +1,18 @@ +// Runnable example: time_scheduler +// +// Date/time + timer toolchain with the `src/infra_time` and +// `src/infra_timer` directions: RFC 3339 / ISO 8601 parsing and +// formatting, civil-calendar arithmetic, POSIX TZ DST rules, ISO 8601 +// durations, a hierarchical timer wheel, and a work-stealing scheduler. +// +// Run with: +// moon run examples/time_scheduler + +import { + "Suquster/moonbit-pathfinding/src/infra_time" @time, + "Suquster/moonbit-pathfinding/src/infra_timer" @timer, +} + +options( + "is-main": true, +) diff --git a/examples/time_scheduler/pkg.generated.mbti b/examples/time_scheduler/pkg.generated.mbti new file mode 100644 index 0000000..6c1d44f --- /dev/null +++ b/examples/time_scheduler/pkg.generated.mbti @@ -0,0 +1,13 @@ +// Generated using `moon info`, DON'T EDIT IT +package "Suquster/moonbit-pathfinding/examples/time_scheduler" + +// Values + +// Errors + +// Types and methods + +// Type aliases + +// Traits + diff --git a/examples/time_scheduler/time_scheduler.mbt b/examples/time_scheduler/time_scheduler.mbt new file mode 100644 index 0000000..6b7d9bc --- /dev/null +++ b/examples/time_scheduler/time_scheduler.mbt @@ -0,0 +1,110 @@ +// examples/time_scheduler — runnable demonstration of the `src/infra_time` +// + `src/infra_timer` directions as a cron-like maintenance-window +// workflow: parse RFC 3339 timestamps, do civil-calendar arithmetic, apply +// a POSIX TZ rule with DST, parse ISO 8601 durations, schedule timeouts on +// a hierarchical timer wheel, and dispatch jobs on a work-stealing +// scheduler. +// +// Run with: +// moon run examples/time_scheduler + +///| +fn main { + println("=== 1. parse + format RFC 3339 / ISO 8601 ===") + let dt = match @time.parse_iso8601("2026-07-12T18:30:00.250Z") { + Some(d) => d + None => { + println("parse failed") + return + } + } + println("parsed : \{dt}") + println("iso8601 : \{@time.format_iso8601(dt)}") + println("rfc2822 : \{@time.format_rfc2822(dt, 0)}") + println("rfc3339 +08 : \{@time.format_rfc3339_with_offset(dt, 8 * 60)}") + println("iso week : \{@time.format_iso_week_date(dt)}") + let pretty = @time.strftime(dt, "%a %b %e, %Y %H:%M:%S") + println("strftime : \{pretty}") + println("") + println("=== 2. civil-calendar arithmetic ===") + let next_month = dt.add_months(1) + println("+1 month : \{@time.format_iso_date(next_month)}") + let leap = @time.is_leap_year(2028) + println( + "2028 leap : \{leap}, Feb 2028 has \{@time.days_in_month(2028, 2)} days", + ) + let deadline = dt.add_days(45) + println( + "+45 days : \{@time.format_iso_date(deadline)} (weekday \{deadline.weekday()})", + ) + println("day of year : \{dt.day_of_year()}") + println("") + println("=== 3. POSIX TZ rule with DST (US Eastern) ===") + match @time.parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2") { + Some(rule) => { + let (offset, name) = @time.tz_offset_at(rule, dt) + println( + "2026-07-12 UTC -> \{name} (offset \{offset / 3600}h), DST: \{@time.is_dst(rule, dt)}", + ) + println( + "local time : \{@time.format_rfc3339_with_offset(dt, offset / 60)}", + ) + let winter = match @time.parse_iso8601("2026-01-12T18:30:00Z") { + Some(d) => d + None => dt + } + let (woff, wname) = @time.tz_offset_at(rule, winter) + println( + "2026-01-12 UTC -> \{wname} (offset \{woff / 3600}h), DST: \{@time.is_dst(rule, winter)}", + ) + } + None => println("tz parse failed") + } + println("") + println("=== 4. human-readable durations ===") + match @time.parse_duration("2h30m") { + Some(d) => { + println( + "2h30m : \{d.total_minutes()} minutes (round-trip: \{d.to_string()})", + ) + let ends = dt.add_duration(d) + println("window ends : \{@time.format_iso8601(ends)}") + } + None => println("duration parse failed") + } + println("") + println("=== 5. hierarchical timer wheel ===") + let wheel = @timer.TimerWheel::new() + let t_short = wheel.schedule(3) + let t_mid = wheel.schedule(10) + let t_cancel = wheel.schedule(5) + println("scheduled 3 timers (ticks 3, 10, 5), pending = \{wheel.pending()}") + println("cancel #\{t_cancel}: \{wheel.cancel(t_cancel)}") + let mut fired_short = false + let mut fired_mid = false + for _tick in 0..<12 { + let fired = wheel.advance() + for id in fired { + if id == t_short { + fired_short = true + } + if id == t_mid { + fired_mid = true + } + } + } + println( + "after 12 ticks: short fired = \{fired_short}, mid fired = \{fired_mid}, pending = \{wheel.pending()}", + ) + println("") + println("=== 6. work-stealing scheduler ===") + let ws = @timer.WorkStealingScheduler::new(4) + // pile all work on worker 0 so idle workers must steal + for i in 0..<16 { + let _ = ws.submit(0, if i % 5 == 0 { 2 } else { 0 }) + } + let executed = ws.run() + println( + "workers: \{ws.workers()}, executed \{executed.length()} tasks, steals: \{ws.steal_count()}", + ) +} diff --git a/moon.mod b/moon.mod index 5efec12..d537490 100644 --- a/moon.mod +++ b/moon.mod @@ -1,6 +1,6 @@ name = "Suquster/moonbit-pathfinding" -version = "0.1.0" +version = "0.2.0" readme = "README.md"