diff --git a/Makefile b/Makefile index 8bfa446..490e2ce 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ clippy: cargo clippy --locked --all-targets -- -D warnings api-doc: - RUSTDOCFLAGS="-D warnings" cargo doc --locked --no-deps + RUSTDOCFLAGS="-D warnings -D missing_docs" cargo doc --locked --no-deps doc-check: cargo run --locked --quiet --bin qdocco -- --check manuals/manual.zh-CN.qc diff --git a/PERFORMANCE.md b/PERFORMANCE.md index 40079d7..0ac81e7 100644 --- a/PERFORMANCE.md +++ b/PERFORMANCE.md @@ -130,6 +130,14 @@ for n in [1...100] by 3 then sum = sum + n sum ``` +stepped-string-iteration(20,000 次): + +```coffee +sum = 0 +for character, index in 'a☕中x' by 2 then sum += index +sum +``` + for-collection(10,000 次): ```coffee @@ -250,6 +258,7 @@ sum | closures-and-ranges | 50.954 ms | 196,255 programs/s | 368.589 ms | 27,130 programs/s | | bare-lambda | 49.829 ms | 200,686 programs/s | 367.051 ms | 27,244 programs/s | | stepped-iteration | 31.895 ms | 313,529 programs/s | 123.921 ms | 80,697 programs/s | +| stepped-string-iteration | 69.630 ms | 287,232 programs/s | 49.288 ms | 405,780 programs/s | | for-collection | 36.532 ms | 273,733 programs/s | 310.488 ms | 32,207 programs/s | | postfix-comprehension | 43.229 ms | 231,326 programs/s | 542.345 ms | 18,438 programs/s | | for-pattern-bindings | 57.605 ms | 173,596 programs/s | 946.521 ms | 10,565 programs/s | @@ -460,6 +469,7 @@ rest 绑定会复制剩余元素到新的不可变数组,以保持宿主存储 | closures-and-ranges | 50.707 / 52.681 / 50.954 | 386.383 / 368.550 / 368.589 | | bare-lambda | 49.267 / 51.139 / 49.829 | 367.051 / 366.815 / 367.863 | | stepped-iteration | 31.774 / 32.983 / 31.895 | 123.424 / 123.921 / 125.822 | +| stepped-string-iteration | 69.630 / 68.868 / 72.102 | 49.175 / 50.030 / 49.288 | | for-collection | 35.892 / 37.276 / 36.532 | 310.488 / 310.415 / 320.711 | | postfix-comprehension | 42.830 / 45.106 / 43.229 | 542.345 / 541.648 / 555.071 | | for-pattern-bindings | 57.142 / 59.312 / 57.605 | 943.416 / 946.521 / 965.377 | diff --git a/README.md b/README.md index 1fbbe22..5d904fa 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ QuickCoffee 是一台以 Rust 编写、受 CoffeeScript 启发的字节码脚本引擎。它保留紧凑、可读的表达式语法,却不兼容 JavaScript:没有原型链、`this`、`eval` 或嵌入 JavaScript。 -当前实现遵循 [RFCs/0000-project-scope.md](RFCs/0000-project-scope.md) 至 [RFCs/0091-descending-ranges.md](RFCs/0091-descending-ranges.md)。 +当前实现遵循 [RFCs/0000-project-scope.md](RFCs/0000-project-scope.md) 至 [RFCs/0095-string-iteration-by-step.md](RFCs/0095-string-iteration-by-step.md)。 ```coffee square = (x) -> x * x diff --git a/RFCs/0070-string-iteration.md b/RFCs/0070-string-iteration.md index 7fcaa7a..ae42af0 100644 --- a/RFCs/0070-string-iteration.md +++ b/RFCs/0070-string-iteration.md @@ -18,7 +18,14 @@ for character, index in 'a☕中' then index 第二绑定是从零开始的 Unicode 标量下标,而不是 UTF-8 字节偏移。每轮模式匹配成功后才写入绑定;字符串为空时产生空数组。后置推导和语句位置的丢弃循环同样适用。 -`by` 只属于数组迭代。由于迭代对象可在运行时求值,`for value in dynamic by step` 在运行时遇到字符串时报告错误,而不是静默改变步长语义。映射仍使用 `of`,不受本 RFC 影响。 +字符串迭代也接受 `by step`。步长只求值一次,必须是正有限整数;它跳过 Unicode 标量而不是 UTF-8 字节,第二绑定仍是实际的标量下标: + +```coffee +for character, index in 'a☕中x' by 2 then [character, index] +# => [[a, 0], [中, 2]] +``` + +由于迭代对象可在运行时求值,`for value in dynamic by step` 在运行时按实际数组或字符串类型采用同一正整数步长。映射仍使用 `of`,不受本 RFC 影响。 非数组、非字符串的 `in` 迭代对象仍是运行时错误;字符串的 `of` 迭代仍是映射类型错误。该功能不暴露 JavaScript 的 UTF-16 code unit、迭代器对象或原型链。 @@ -26,4 +33,4 @@ for character, index in 'a☕中' then index 编译器发出统一的 `IterStartEnumerable`,消耗迭代对象与步长;VM 根据运行时值选择数组或 Unicode 字符串迭代。验证器按原数组迭代路径检查两个栈值与一个迭代器状态,`IterNext` 的模式数量和控制流规则不变。 -验收至少包括 ASCII 与非 ASCII 标量、动态字符串、可选下标、过滤、空字符串、`by` 错误、非字符串/数组错误、嵌套控制流,以及生成字节码的验证。 +验收至少包括 ASCII 与非 ASCII 标量、动态字符串、可选下标、过滤、空字符串、非法步长错误、非字符串/数组错误、嵌套控制流,以及生成字节码的验证。 diff --git a/RFCs/0092-public-api-documentation.md b/RFCs/0092-public-api-documentation.md new file mode 100644 index 0000000..b3c76c8 --- /dev/null +++ b/RFCs/0092-public-api-documentation.md @@ -0,0 +1,23 @@ +# RFC 0092:公开 Rust API 文档门禁 + +- 状态:已采纳 +- 依赖:RFC 0002、RFC 0043、RFC 0046、RFC 0089 + +## 动机 + +QuickCoffee 的 crate 既是 CLI 的实现,也是宿主系统的嵌入 API。若公开的 `Value`、`Context`、 +`Program`、`Chunk` 或错误类型没有 rustdoc,docs.rs 不能提供可靠的集成入口;普通编译通过 +并不能证明发布 API 可发现、可维护。 + +## 契约 + +所有公开类型、字段、枚举变体、方法和顶层函数必须有简短 rustdoc。低层 `Instruction`、 +`Pattern`、`Constant` 的变体允许使用统一的枚举级说明,因为它们是已验证字节码的机械标签, +但公开 `Chunk` 字段、宿主值访问器、错误分类和 `Engine`/`Context`/`Program` 操作必须说明 +所有权、验证与执行语义。文档不得承诺原型链、JavaScript `undefined` 或隐藏的可变状态。 + +## 验收 + +Makefile 的 `api-doc` 使用 `RUSTDOCFLAGS="-D warnings -D missing_docs" cargo doc --locked +--no-deps`;CI 的 `make check` 因而把缺失文档视为失败。外部 `tests/embedding_api.rs` 继续 +证明文档所描述的公开入口可从 crate 外部调用,其他运行时行为不变。 diff --git a/RFCs/0093-value-kind-inspection.md b/RFCs/0093-value-kind-inspection.md new file mode 100644 index 0000000..56376eb --- /dev/null +++ b/RFCs/0093-value-kind-inspection.md @@ -0,0 +1,26 @@ +# RFC 0093:宿主值类型标签 + +- 状态:已采纳 +- 依赖:RFC 0002、RFC 0041、RFC 0089、RFC 0092 + +## 动机 + +嵌入方经常需要在调用 `Value` 访问器前判断值类型。逐个尝试 `as_number`、`as_array` 等 +方法既冗长,也会诱使宿主直接匹配公开 enum 的内部 `Rc` 容器。需要一个不泄漏存储实现、 +可在 match 中使用的稳定类型标签。 + +## 契约 + +公开 `ValueKind::{Nil, Bool, Number, String, Array, Map, Function}`,并提供: + +- `Value::kind()`:只读返回对应标签,不执行脚本、不克隆容器; +- `Value::is_nil()`:仅对 `nil` 返回 true,false/0/空容器均返回 false。 + +标签与 QuickCoffee 运行时类型一一对应;新增类型必须显式扩展 `ValueKind`。现有 `as_*` +访问器、构造器、Display 语义和字节码指纹不变。 + +## 验收 + +`tests/embedding_api.rs` 从 crate 外部检查全部相关标签与 `nil` 行为;严格 rustdoc 门禁必须 +包含新类型和方法。五语嵌入说明可继续使用 `Value::kind()` 做宿主分流,`make check` 保持 +通过。 diff --git a/RFCs/0094-qdocco-final-true-check.md b/RFCs/0094-qdocco-final-true-check.md new file mode 100644 index 0000000..efd9cba --- /dev/null +++ b/RFCs/0094-qdocco-final-true-check.md @@ -0,0 +1,22 @@ +# RFC 0094:qdocco 文学源最终值门禁 + +- 状态:已采纳 +- 依赖:RFC 0003、RFC 0005、RFC 0083 + +## 动机 + +`qdocco --check` 是文学编程源和用户手册的可执行验收入口。RFC 0005 规定每份手册最后 +必须得到布尔 `true`,但旧实现只判断源程序没有读取、解析或运行错误,导致返回数字、字符串 +或 `nil` 的文档错误地通过检查。 + +## 契约 + +`qdocco --check FILE` 在读取、编译、验证和执行成功后,还必须要求最终值严格为 +`Value::Bool(true)`。其他值以非零退出码(1)失败,并在标准错误说明实际值与期望 `true`。 +普通 HTML/Markdown 生成模式仍可展示任意最终值,便于文档工具调试;该门禁只属于显式 +`--check`。qdocco 不执行 Markdown、HTML 或 JavaScript 内容。 + +## 验收 + +CLI 集成测试覆盖最终值 `true` 的通过、数字最终值的拒绝及既有 HTML/Markdown 输出。五份 +文学手册继续由 `qdocco --check` 验证,`make check` 与生成物一致性必须通过。 diff --git a/RFCs/0095-string-iteration-by-step.md b/RFCs/0095-string-iteration-by-step.md new file mode 100644 index 0000000..ceb9026 --- /dev/null +++ b/RFCs/0095-string-iteration-by-step.md @@ -0,0 +1,23 @@ +# RFC 0095:字符串 Unicode 标量步进迭代 + +- 状态:已采纳 +- 依赖:RFC 0070、RFC 0042、RFC 0044 + +## 动机 + +RFC 0070 已定义字符串按 Unicode 标量迭代,但 `by` 仍被拒绝。数组与字符串共享 `for` 收集、过滤、模式绑定和可选下标语义;拒绝字符串步长使同一语法在运行时类型切换时不一致,也迫使文档保留一个不必要的例外。 + +## 契约 + +`for value in string by step then body` 按 Unicode 标量位置 `0, step, 2*step, ...` 取值。每个值是单一 Unicode 标量组成的 QuickCoffee 字符串,不暴露 UTF-8 字节或 JavaScript UTF-16 code unit。第二绑定得到实际的标量下标,而非迭代轮数: + +```coffee +for character, index in 'a☕中x' by 2 then [character, index] +# => [[a, 0], [中, 2]] +``` + +步长表达式只求值一次,必须是正的有限整数;数组和字符串共享该检查。动态 `in` 迭代对象在运行时决定采用数组或字符串的步进路径;`of` 映射迭代不接受 `by`。空字符串、过滤、后置推导、`break`、`continue` 与严格递归模式保持 RFC 0070 语义。 + +## 实现与验收 + +`IterationKind::String` 保存步长,并以饱和加法推进 Unicode 标量位置;字节码指令格式与验证规则不变。验收覆盖 ASCII 与多字节 Unicode、动态字符串、实际标量下标、动态步长、空输入、过滤、嵌套控制流及既有非法步长/映射错误。 diff --git a/RFCs/0096-stepped-string-benchmark.md b/RFCs/0096-stepped-string-benchmark.md new file mode 100644 index 0000000..5248fa4 --- /dev/null +++ b/RFCs/0096-stepped-string-benchmark.md @@ -0,0 +1,24 @@ +# RFC 0096:字符串步进迭代性能基准覆盖 + +- 状态:已采纳 +- 依赖:RFC 0045、RFC 0081、RFC 0095 + +## 动机 + +RFC 0095 扩展了字符串 `for ... by` 的执行路径,但若只保留功能测试,字符串 Unicode 标量步进可能在 VM 优化或回归中变慢而不被发现。项目既要求可执行语义护栏,也要求性能报告能按工作负载追踪编译、验证和执行成本。 + +## 契约 + +`cargo bench --bench core` 必须包含 `stepped-string-iteration` 工作负载;`qbench --json` 也必须输出同名记录。负载使用 ASCII 与多字节 Unicode、实际标量下标和 `by 2`,最终值严格为 `2`: + +```coffee +sum = 0 +for character, index in 'a☕中x' by 2 then sum += index +sum # => 2 +``` + +两条基准路径都必须先编译/验证并执行语义检查,再计时;结果不得包含标准输出、文件 I/O 或调试构建。性能报告记录机器、工具链、命令、迭代数和至少三次 release 样本的中位数,不把单次读数当成跨机器比较。 + +## 验收 + +`cargo bench --locked --bench core` 的新工作负载通过最终值护栏;`cargo run --locked --release --bin qbench -- --json --iterations 1` 输出一个 `stepped-string-iteration` JSON 记录且 `expected` 为 `2`;`make check` 与性能报告中的基线数据保持一致。 diff --git a/benches/core.rs b/benches/core.rs index e208d34..ff01d61 100644 --- a/benches/core.rs +++ b/benches/core.rs @@ -71,6 +71,12 @@ fn main() { iterations: 20_000, expected: "3", }, + Workload { + name: "stepped-string-iteration", + source: "sum = 0\nfor character, index in 'a☕中x' by 2 then sum += index\nsum", + iterations: 20_000, + expected: "2", + }, Workload { name: "string-escapes", source: "message = \"A\\x42\\u{43}\"\nlen(message) + (if message == 'ABC' then 1 else 0)", diff --git a/docs/manual.classical-zh.html b/docs/manual.classical-zh.html index 06b9ff0..8b8188c 100644 --- a/docs/manual.classical-zh.html +++ b/docs/manual.classical-zh.html @@ -14,7 +14,7 @@

qcoffee --interactive(或 -i)者,逐行共用一 Context;:help 示命,:quit 出之。

qcoffee --interactive --stats 惟非空行之行而行或运行时有误者,书指令与余燃料一条;析验之误不更书。

'a☕中'[1] 即 '☕','a☕中'[1..2]' 得 '☕中';字符串索引循 Unicode 标量。

-

for character, index in 'a☕中' then index,得 [0, 1, 2];字符串循 Unicode 标量,弗受 by。

+

for character, index in 'a☕中' then index,得 [0, 1, 2];字符串循 Unicode 标量,亦受正整数 by。

[head, tail...] = [1, 2, 3],tail 得 [2, 3];数组之 rest 必居末。

qtest --fuel N 者,为各可行文别限其指令之数。

qtest --stats 更书各篇所试指令与余燃料于标准错误,而 ok 之出不改。

@@ -29,6 +29,7 @@

qdocco --markdown 出说明、围栏 QuickCoffee 代码及终值为可阅 Markdown 文。

嵌者可于两行之间呼 Context::set_fuel;Context::fuel 示每行之限,而全局不失。

cargo run --example embed 可验最小 Rust 宿主,设全局、立原生回调而行 QuickCoffee。

+

宿主可用 Value::kind() 别其类,Value::is_nil() 验 nil,不窥其内容器。

Cargo 包志指仓、docs.rs API、README 与许可证,使嵌者易寻其用。

Context::last_execution() 示所试指令与余燃料,而不露 VM 之帧。

-- 后之参,以常字符串数组 argv 见于文中。

diff --git a/docs/manual.classical-zh.md b/docs/manual.classical-zh.md index aaee274..515b591 100644 --- a/docs/manual.classical-zh.md +++ b/docs/manual.classical-zh.md @@ -89,4 +89,4 @@ QuickCoffee 者,Rust 所为字节码机也,非 JavaScript 之运行时。其 一逻辑行中,函调用可略括,如 `implicit_answer = implicit_add 20, 22`;遇比较或布局之界,仍宜明括。 -`qtest --json` 各试篇出定 JSON,`qtest --tap` 出 TAP 13;`qcoffee --fingerprint FILE` 不行其文而出定式字节码键;`qbench --json` 记编、验、行之时,皆有语义护栏;`qdocco --markdown` 出可阅文学编程 Markdown。嵌者可呼 `Context::set_fuel` 改复用境之限,并行 `cargo run --example embed` 观宿主全例。 +`qtest --json` 各试篇出定 JSON,`qtest --tap` 出 TAP 13;`qcoffee --fingerprint FILE` 不行其文而出定式字节码键;`qbench --json` 记编、验、行之时,皆有语义护栏;`qdocco --markdown` 出可阅文学编程 Markdown。嵌者可呼 `Context::set_fuel` 改复用境之限,以 `Value::kind()`、`Value::is_nil()` 别值之类,并行 `cargo run --example embed` 观宿主全例。 diff --git a/docs/manual.devanagari-sa.html b/docs/manual.devanagari-sa.html index 3c15eb1..9d44d84 100644 --- a/docs/manual.devanagari-sa.html +++ b/docs/manual.devanagari-sa.html @@ -10,7 +10,7 @@

qcoffee --interactive (वा -i) एकं Context पङ्क्ति-क्रमेण धारयति; :help दर्शयति, :quit निर्गच्छति।

qcoffee --interactive --stats केवलं कार्यितायै वा runtime-दोषयुक्तायै non-empty पङ्क्त्यै instruction तथा fuel लेखं लिखति; parse अथवा verify-दोषे नूतनं लेखं न लिखति।

'a☕中'[1] '☕' अस्ति, 'a☕中'[1..2] '☕中' अस्ति; string-index Unicode-scalar-अनुसारी अस्ति।

-

for character, index in 'a☕中' then index Unicode-scalar-अङ्कान् [0, 1, 2] ददाति; string-iteration मध्ये by नास्ति।

+

for character, index in 'a☕中' then index Unicode-scalar-अङ्कान् [0, 1, 2] ददाति; string-iteration मध्ये धनात्मक by-क्रमः अस्ति।

[head, tail...] = [1, 2, 3] tail-नाम्नि [2, 3] बध्नाति; array-pattern rest अन्तिमः भवति।

qtest --fuel N प्रत्येक executable-document पृथक् instruction-budget ददाति।

qtest --stats प्रत्येकस्य documentस्य instruction-संख्या तथा अवशिष्ट-fuel standard error मध्ये लिखति, ok-निर्गमं न परिवर्तयति।

@@ -25,6 +25,7 @@

qdocco --markdown टिप्पणीन्, सीमितं QuickCoffee-कोडं, अन्तिम-मूल्यं च पठनीय Markdown-फलके लिखति।

अन्तःस्थापकः चालनयोर्मध्ये Context::set_fuel आह्वयितुं शक्नोति; Context::fuel वर्तमान-सीमां दर्शयति, वैश्विक-मूल्यानि न नाशयति।

`cargo run --example embed` लघुं Rust-आश्रयं संयोजयति, वैश्विकं स्थापयति, native-callback योजयति, QuickCoffee च चालयति।

+

Host `Value::kind()` द्वारा प्रकारं विभजति, `Value::is_nil()` द्वारा nil परीक्षते, आन्तरिक-container न पश्यति।

Cargo-वस्तु-विवरणानि अन्तःस्थापकान् repository, docs.rs-API, README, licence च प्रति नयन्ति।

Context::last_execution() instruction-संख्या तथा अवशिष्ट-fuel दर्शयति, VM-frame न प्रकाशयति।

-- पश्चात् argumentाः साधारण-string-array argv रूपेण दीयन्ते।

diff --git a/docs/manual.devanagari.sa.md b/docs/manual.devanagari.sa.md index 907e3ae..59a564e 100644 --- a/docs/manual.devanagari.sa.md +++ b/docs/manual.devanagari.sa.md @@ -93,4 +93,4 @@ host-त्रुटिः संरचिता: `error.kind()` `ErrorKind::Par एकस्यां logical-line मध्ये call-parenthesis विना अपि शक्यते: `implicit_answer = implicit_add 20, 22`; comparison अथवा layout-boundary मध्ये explicit parenthesis प्रयोजनीया। -`qtest --json` प्रत्येक-परीक्षा-पत्राय स्थिरं JSON लिखति, `qtest --tap` TAP 13 ददाति; `qcoffee --fingerprint FILE` लेखं न चालयित्वा नियत-bytecode-कुञ्जीं दर्शयति; `qbench --json` semantic-रक्षणेन compile, verify, execute कालं मापयति; `qdocco --markdown` समीक्षायै literate Markdown जनयति। अन्तःस्थापकः Context::set_fuel द्वारा पुनःप्रयुक्त-सन्दर्भस्य सीमा परिवर्तयितुं शक्नोति, तथा `cargo run --example embed` पूर्णं host-उदाहरणं चालयति। +`qtest --json` प्रत्येक-परीक्षा-पत्राय स्थिरं JSON लिखति, `qtest --tap` TAP 13 ददाति; `qcoffee --fingerprint FILE` लेखं न चालयित्वा नियत-bytecode-कुञ्जीं दर्शयति; `qbench --json` semantic-रक्षणेन compile, verify, execute कालं मापयति; `qdocco --markdown` समीक्षायै literate Markdown जनयति। अन्तःस्थापकः `Context::set_fuel` द्वारा पुनःप्रयुक्त-सन्दर्भस्य सीमा परिवर्तयितुं शक्नोति, `Value::kind()` तथा `Value::is_nil()` द्वारा प्रकारं परीक्षते, तथा `cargo run --example embed` पूर्णं host-उदाहरणं चालयति। diff --git a/docs/manual.en.html b/docs/manual.en.html index 35d9a6b..1721fb7 100644 --- a/docs/manual.en.html +++ b/docs/manual.en.html @@ -6,7 +6,7 @@

qcoffee --check FILE parses, compiles, and verifies without executing FILE.

qcoffee --interactive (or -i) keeps one Context for a line-oriented session; :help and :quit are built-in commands.

qcoffee --interactive --stats writes one instruction/fuel record for each non-empty line that executes or reaches a runtime error; parse and verify errors write none.

-

for character, index in 'a☕中' then index yields [0, 1, 2]; strings iterate Unicode scalars and reject by.

+

for character, index in 'a☕中' then index yields [0, 1, 2]; strings iterate Unicode scalars and accept positive by steps.

[head, tail...] = [1, 2, 3] binds tail to [2, 3]; array-pattern rest must be final.

qtest --fuel N gives each executable documentation file its own instruction budget.

qtest --stats writes each file's instruction count and remaining fuel to stderr without changing its ok output.

@@ -21,6 +21,7 @@

qdocco --markdown writes Notes, fenced QuickCoffee code, and the final value as a reviewable Markdown artifact.

Embedders may call Context::set_fuel between runs; Context::fuel reports the current per-run budget without clearing globals.

cargo run --example embed compiles a minimal Rust host that sets a global, registers a native callback, and evaluates QuickCoffee.

+

A host can branch on Value::kind() and use Value::is_nil() without inspecting internal containers.

Cargo package metadata points embedding users to the repository, docs.rs API, README, and license.

Context::last_execution() exposes instruction and remaining-fuel counters without VM frames.

Arguments after -- are exposed as the ordinary string array argv.

diff --git a/docs/manual.en.md b/docs/manual.en.md index d3fd164..2d72394 100644 --- a/docs/manual.en.md +++ b/docs/manual.en.md @@ -84,7 +84,7 @@ Use `switch value` with indented `when pattern` branches for strict-equality sel Exceptions use `try`, `catch error`, optional `finally`, and `throw value`. A catch receives a stable error string rather than a JavaScript Error object; function returns also run applicable finalizers. -For Rust embedding, create `Context`, optionally call `with_fuel`, register a host callback with `add_native`, then call `eval`; callbacks can return `Error::runtime("message")` and the script may catch it. For repeated execution, compile once with `Engine::compile_program` (which verifies once) and pass the shared `Program` to `run_program`; cloning that handle does not copy bytecode or repeat verification. `Value::from`, `Value::string`, `Value::array`, and `Value::map` construct host values without exposing VM reference-counting internals. +For Rust embedding, create `Context`, optionally call `with_fuel`, register a host callback with `add_native`, then call `eval`; callbacks can return `Error::runtime("message")` and the script may catch it. For repeated execution, compile once with `Engine::compile_program` (which verifies once) and pass the shared `Program` to `run_program`; cloning that handle does not copy bytecode or repeat verification. `Value::from`, `Value::string`, `Value::array`, and `Value::map` construct host values without exposing VM reference-counting internals; `Value::kind()` and `Value::is_nil()` provide stable type checks. `Context::last_execution()` returns public `ExecutionStats` (`instructions` and `fuel_remaining`) for the latest successful or runtime-failed execution; compile and verification errors leave the previous record unchanged. diff --git a/docs/manual.latin.html b/docs/manual.latin.html index 8390191..c39b2c1 100644 --- a/docs/manual.latin.html +++ b/docs/manual.latin.html @@ -10,7 +10,7 @@

qcoffee --interactive (vel -i) unum Context per lineas servat; :help docet, :quit exit.

qcoffee --interactive --stats unam instructionum et alimenti reliqui notam lineae non vacuae exsecutæ vel errorem currendi ferenti scribit; errores analysi vel verificationis nihil scribunt.

'a☕中'[1] est '☕', et 'a☕中'[1..2] est '☕中'; indices stringarum scalas Unicode sequuntur.

-

for character, index in 'a☕中' then index indices Unicode scalarum [0, 1, 2] reddit; iteratio stringarum by non accipit.

+

for character, index in 'a☕中' then index indices Unicode scalarum [0, 1, 2] reddit; iteratio stringarum gradus positivos by accipit.

[head, tail...] = [1, 2, 3] tail ad [2, 3] ligat; rest in forma array postremum esse debet.

qtest --fuel N cuique documento exsecutabili budget instructionum proprium dat.

qtest --stats numeros instructionum et alimenti reliqui cuiusque documenti ad errorem ordinarium scribit, sine mutatione exitus ok.

@@ -25,6 +25,7 @@

qdocco --markdown notas, codicem QuickCoffee clausum, et valorem ultimum in documento Markdown scribit.

Hospes inter cursus `Context::set_fuel` vocare potest; `Context::fuel` budgetum ostendit sine globalibus deletis.

`cargo run --example embed` hospitem Rust minimum compilat: globale ponit, callback nativum addit, et QuickCoffee currit.

+

Hospes `Value::kind()` ad genus discernendum et `Value::is_nil()` ad nil probandum utitur, sine interioribus vasorum.

Notitiae Cargoe hospites ad repositorium, API docs.rs, README et licentiam ducunt.

Context::last_execution() numeros instructionum et alimenti reliqui ostendit, sine tabulis VM.

Argumenta post -- ut series chordarum ordinaria argv praebentur.

diff --git a/docs/manual.latin.md b/docs/manual.latin.md index 87ad54e..d07bc4c 100644 --- a/docs/manual.latin.md +++ b/docs/manual.latin.md @@ -91,4 +91,4 @@ Post assignationem solam (`record =`) mapa per indentationem scribi potest; clav In una linea logica, functio sine parenthesibus vocari potest: `implicit_answer = implicit_add 20, 22`; apud comparationes vel limites ordinis parenthesibus uti licet. -`qtest --json` unam rem stabilem pro quoque testium documento scribit, `qtest --tap` TAP 13 reddit; `qcoffee --fingerprint FILE` clavem bytecodicis canonice verificati sine exsecutione ostendit; `qbench --json` tempora compilationis, verificationis et cursus cum custodia semantica metitur; `qdocco --markdown` documentum literarium ad recensionem scribit. Hospes `Context::set_fuel` inter cursus mutare potest et `cargo run --example embed` exemplum integrum currere. +`qtest --json` unam rem stabilem pro quoque testium documento scribit, `qtest --tap` TAP 13 reddit; `qcoffee --fingerprint FILE` clavem bytecodicis canonice verificati sine exsecutione ostendit; `qbench --json` tempora compilationis, verificationis et cursus cum custodia semantica metitur; `qdocco --markdown` documentum literarium ad recensionem scribit. Hospes `Context::set_fuel` inter cursus mutare potest, `Value::kind()` et `Value::is_nil()` ad genus probandum adhibet, et `cargo run --example embed` exemplum integrum currere. diff --git a/docs/manual.zh-CN.html b/docs/manual.zh-CN.html index 9a41176..6854d57 100644 --- a/docs/manual.zh-CN.html +++ b/docs/manual.zh-CN.html @@ -7,7 +7,7 @@

qcoffee --interactive(或 -i)逐行复用同一 Context;:help 显示命令,:quit 退出。

qcoffee --interactive --stats 仅为实际执行或运行时失败的非空输入行输出指令/燃料统计;解析、验证错误不输出新记录。

'a☕中'[1] 为 '☕','a☕中'[1..2] 为 '☕中';字符串索引按 Unicode 标量。

-

for character, index in 'a☕中' then index 得 [0, 1, 2];字符串按 Unicode 标量遍历,不可用 by。

+

for character, index in 'a☕中' then index 得 [0, 1, 2];字符串按 Unicode 标量遍历,亦可用正整数 by。

[head, tail...] = [1, 2, 3] 将 tail 绑定为 [2, 3];数组模式 rest 必须居末。

qtest --fuel N 为每份可执行文档设置独立指令预算。

qtest --stats 将每份文档的指令数与剩余燃料写入标准错误,不改变 ok 输出。

@@ -22,6 +22,7 @@

qdocco --markdown 将说明、围栏 QuickCoffee 代码与最终值写成可审阅的 Markdown 产物。

嵌入方可在运行之间调用 Context::set_fuel;Context::fuel 返回当前每轮预算,且不清除全局值。

cargo run --example embed 可编译最小 Rust 宿主:设置全局、注册原生回调并执行 QuickCoffee。

+

宿主可用 Value::kind() 分流类型,用 Value::is_nil() 判断 nil,无须检查内部容器。

Cargo 包元数据指向仓库、docs.rs API、README 与许可证,便于嵌入方发现项目。

Context::last_execution() 提供指令数与剩余燃料统计,不暴露 VM 帧。

-- 后的参数以普通字符串数组 argv 暴露给程序。

diff --git a/docs/manual.zh-CN.md b/docs/manual.zh-CN.md index e024aa5..e4db0bb 100644 --- a/docs/manual.zh-CN.md +++ b/docs/manual.zh-CN.md @@ -137,4 +137,4 @@ let value = cx.eval("host_values[0] + host_values[1]")?; 同一逻辑行的调用可省略括号:`implicit_answer = implicit_add 20, 22`;比较或跨布局边界时仍可使用显式括号。 -`qtest --json` 为每个测试文件输出稳定 JSON,`qtest --tap` 输出 TAP 13;`qcoffee --fingerprint FILE` 在不执行文件时输出规范化字节码缓存键;`qbench --json` 输出带语义护栏的编译、验证、执行计时;`qdocco --markdown` 生成可审阅的文学编程 Markdown。嵌入方可用 `Context::set_fuel` 调整复用上下文,并运行 `cargo run --example embed` 查看完整宿主示例。 +`qtest --json` 为每个测试文件输出稳定 JSON,`qtest --tap` 输出 TAP 13;`qcoffee --fingerprint FILE` 在不执行文件时输出规范化字节码缓存键;`qbench --json` 输出带语义护栏的编译、验证、执行计时;`qdocco --markdown` 生成可审阅的文学编程 Markdown。嵌入方可用 `Context::set_fuel` 调整复用上下文,用 `Value::kind()` 与 `Value::is_nil()` 做稳定类型判断,并运行 `cargo run --example embed` 查看完整宿主示例。 diff --git a/docs/syntax.en.md b/docs/syntax.en.md index 2864ab9..b544ab2 100644 --- a/docs/syntax.en.md +++ b/docs/syntax.en.md @@ -26,7 +26,9 @@ Array comprehensions may bind an optional zero-based index: `for value, index in Comprehensions may also use CoffeeScript's postfix form: `value * 2 for value in items`, optionally wrapped as `[value * 2 for value in items]`. The postfix form has the same `by`, `when`, map, pattern, `break`, and `continue` semantics as the prefix form; the brackets are a comprehension delimiter and do not add a nested array. -String indexing and strict slices use Unicode scalar boundaries: `'a☕中'[1]` is `'☕'`, and `'a☕中'[1..2]` is `'☕中'`. String `by` iteration remains unsupported. +String indexing and strict slices use Unicode scalar boundaries: `'a☕中'[1]` is `'☕'`, and `'a☕中'[1..2]` is `'☕中'`. String `for` iteration also accepts a positive finite `by` step over Unicode scalar positions: `for character, index in 'a☕中x' by 2 then index` yields `[0, 2]`. + +Array and string `by step` expressions are evaluated once and must be positive finite integers; map iteration excludes `by`. Names support arithmetic compound assignment (`+=`, `-=`, `*=`, `/=`, `%=` and `**=`). Compound assignment is intentionally name-only; members, indexes, and destructuring remain immutable API boundaries. diff --git a/docs/syntax.zh-CN.md b/docs/syntax.zh-CN.md index 8d19062..fb92f90 100644 --- a/docs/syntax.zh-CN.md +++ b/docs/syntax.zh-CN.md @@ -16,7 +16,7 @@ 整数区间支持升序与降序:`[2..4]` 为 `[2, 3, 4]`,`[4..2]` 为 `[4, 3, 2]`;排除上界形式相应省略终点(`[4...2]` 为 `[4, 3]`)。边界必须是有限整数,过长区间仍报错。 -字符串索引与严格切片按 Unicode 标量边界:`'a☕中'[1]` 为 `'☕'`,`'a☕中'[1..2]` 为 `'☕中'`;负索引从末项计数(`items[-1]`),越界仍报错;字符串 `for` 仍不支持 `by`。 +字符串索引与严格切片按 Unicode 标量边界:`'a☕中'[1]` 为 `'☕'`,`'a☕中'[1..2]` 为 `'☕中'`;负索引从末项计数(`items[-1]`),越界仍报错;字符串 `for` 亦支持按 Unicode 标量下标以正有限整数 `by` 步进,例如 `for character, index in 'a☕中x' by 2 then index` 得 `[0, 2]`。 映射字面量可从左至右展开:`{...defaults, theme: 'dark'}`;后写显式键或展开段覆盖先写键,展开值必须为映射。映射解构可用末尾尾部模式 `{id, ...metadata}` 捕获未列键,所得映射为新的不可变值。 diff --git a/manuals/manual.classical-zh.qc b/manuals/manual.classical-zh.qc index 726e334..ebacb5f 100644 --- a/manuals/manual.classical-zh.qc +++ b/manuals/manual.classical-zh.qc @@ -14,7 +14,7 @@ ## qcoffee --interactive(或 -i)者,逐行共用一 Context;:help 示命,:quit 出之。 ## qcoffee --interactive --stats 惟非空行之行而行或运行时有误者,书指令与余燃料一条;析验之误不更书。 ## 'a☕中'[1] 即 '☕','a☕中'[1..2]' 得 '☕中';字符串索引循 Unicode 标量。 -## for character, index in 'a☕中' then index,得 [0, 1, 2];字符串循 Unicode 标量,弗受 by。 +## for character, index in 'a☕中' then index,得 [0, 1, 2];字符串循 Unicode 标量,亦受正整数 by。 ## [head, tail...] = [1, 2, 3],tail 得 [2, 3];数组之 rest 必居末。 ## qtest --fuel N 者,为各可行文别限其指令之数。 ## qtest --stats 更书各篇所试指令与余燃料于标准错误,而 ok 之出不改。 @@ -29,6 +29,7 @@ ## qdocco --markdown 出说明、围栏 QuickCoffee 代码及终值为可阅 Markdown 文。 ## 嵌者可于两行之间呼 Context::set_fuel;Context::fuel 示每行之限,而全局不失。 ## cargo run --example embed 可验最小 Rust 宿主,设全局、立原生回调而行 QuickCoffee。 +## 宿主可用 Value::kind() 别其类,Value::is_nil() 验 nil,不窥其内容器。 ## Cargo 包志指仓、docs.rs API、README 与许可证,使嵌者易寻其用。 ## Context::last_execution() 示所试指令与余燃料,而不露 VM 之帧。 ## -- 后之参,以常字符串数组 argv 见于文中。 diff --git a/manuals/manual.devanagari-sa.qc b/manuals/manual.devanagari-sa.qc index e358032..7697478 100644 --- a/manuals/manual.devanagari-sa.qc +++ b/manuals/manual.devanagari-sa.qc @@ -10,7 +10,7 @@ ## qcoffee --interactive (वा -i) एकं Context पङ्क्ति-क्रमेण धारयति; :help दर्शयति, :quit निर्गच्छति। ## qcoffee --interactive --stats केवलं कार्यितायै वा runtime-दोषयुक्तायै non-empty पङ्क्त्यै instruction तथा fuel लेखं लिखति; parse अथवा verify-दोषे नूतनं लेखं न लिखति। ## 'a☕中'[1] '☕' अस्ति, 'a☕中'[1..2] '☕中' अस्ति; string-index Unicode-scalar-अनुसारी अस्ति। -## for character, index in 'a☕中' then index Unicode-scalar-अङ्कान् [0, 1, 2] ददाति; string-iteration मध्ये by नास्ति। +## for character, index in 'a☕中' then index Unicode-scalar-अङ्कान् [0, 1, 2] ददाति; string-iteration मध्ये धनात्मक by-क्रमः अस्ति। ## [head, tail...] = [1, 2, 3] tail-नाम्नि [2, 3] बध्नाति; array-pattern rest अन्तिमः भवति। ## qtest --fuel N प्रत्येक executable-document पृथक् instruction-budget ददाति। ## qtest --stats प्रत्येकस्य documentस्य instruction-संख्या तथा अवशिष्ट-fuel standard error मध्ये लिखति, ok-निर्गमं न परिवर्तयति। @@ -25,6 +25,7 @@ ## qdocco --markdown टिप्पणीन्, सीमितं QuickCoffee-कोडं, अन्तिम-मूल्यं च पठनीय Markdown-फलके लिखति। ## अन्तःस्थापकः चालनयोर्मध्ये Context::set_fuel आह्वयितुं शक्नोति; Context::fuel वर्तमान-सीमां दर्शयति, वैश्विक-मूल्यानि न नाशयति। ## `cargo run --example embed` लघुं Rust-आश्रयं संयोजयति, वैश्विकं स्थापयति, native-callback योजयति, QuickCoffee च चालयति। +## Host `Value::kind()` द्वारा प्रकारं विभजति, `Value::is_nil()` द्वारा nil परीक्षते, आन्तरिक-container न पश्यति। ## Cargo-वस्तु-विवरणानि अन्तःस्थापकान् repository, docs.rs-API, README, licence च प्रति नयन्ति। ## Context::last_execution() instruction-संख्या तथा अवशिष्ट-fuel दर्शयति, VM-frame न प्रकाशयति। ## -- पश्चात् argumentाः साधारण-string-array argv रूपेण दीयन्ते। diff --git a/manuals/manual.en.qc b/manuals/manual.en.qc index 865c3ee..463dc4a 100644 --- a/manuals/manual.en.qc +++ b/manuals/manual.en.qc @@ -6,7 +6,7 @@ ## qcoffee --check FILE parses, compiles, and verifies without executing FILE. ## qcoffee --interactive (or -i) keeps one Context for a line-oriented session; :help and :quit are built-in commands. ## qcoffee --interactive --stats writes one instruction/fuel record for each non-empty line that executes or reaches a runtime error; parse and verify errors write none. -## for character, index in 'a☕中' then index yields [0, 1, 2]; strings iterate Unicode scalars and reject by. +## for character, index in 'a☕中' then index yields [0, 1, 2]; strings iterate Unicode scalars and accept positive by steps. ## [head, tail...] = [1, 2, 3] binds tail to [2, 3]; array-pattern rest must be final. ## qtest --fuel N gives each executable documentation file its own instruction budget. ## qtest --stats writes each file's instruction count and remaining fuel to stderr without changing its ok output. @@ -21,6 +21,7 @@ ## qdocco --markdown writes Notes, fenced QuickCoffee code, and the final value as a reviewable Markdown artifact. ## Embedders may call Context::set_fuel between runs; Context::fuel reports the current per-run budget without clearing globals. ## cargo run --example embed compiles a minimal Rust host that sets a global, registers a native callback, and evaluates QuickCoffee. +## A host can branch on Value::kind() and use Value::is_nil() without inspecting internal containers. ## Cargo package metadata points embedding users to the repository, docs.rs API, README, and license. ## Context::last_execution() exposes instruction and remaining-fuel counters without VM frames. ## Arguments after -- are exposed as the ordinary string array argv. diff --git a/manuals/manual.latin.qc b/manuals/manual.latin.qc index eab0ec1..3a7cd12 100644 --- a/manuals/manual.latin.qc +++ b/manuals/manual.latin.qc @@ -10,7 +10,7 @@ ## qcoffee --interactive (vel -i) unum Context per lineas servat; :help docet, :quit exit. ## qcoffee --interactive --stats unam instructionum et alimenti reliqui notam lineae non vacuae exsecutæ vel errorem currendi ferenti scribit; errores analysi vel verificationis nihil scribunt. ## 'a☕中'[1] est '☕', et 'a☕中'[1..2] est '☕中'; indices stringarum scalas Unicode sequuntur. -## for character, index in 'a☕中' then index indices Unicode scalarum [0, 1, 2] reddit; iteratio stringarum by non accipit. +## for character, index in 'a☕中' then index indices Unicode scalarum [0, 1, 2] reddit; iteratio stringarum gradus positivos by accipit. ## [head, tail...] = [1, 2, 3] tail ad [2, 3] ligat; rest in forma array postremum esse debet. ## qtest --fuel N cuique documento exsecutabili budget instructionum proprium dat. ## qtest --stats numeros instructionum et alimenti reliqui cuiusque documenti ad errorem ordinarium scribit, sine mutatione exitus ok. @@ -25,6 +25,7 @@ ## qdocco --markdown notas, codicem QuickCoffee clausum, et valorem ultimum in documento Markdown scribit. ## Hospes inter cursus `Context::set_fuel` vocare potest; `Context::fuel` budgetum ostendit sine globalibus deletis. ## `cargo run --example embed` hospitem Rust minimum compilat: globale ponit, callback nativum addit, et QuickCoffee currit. +## Hospes `Value::kind()` ad genus discernendum et `Value::is_nil()` ad nil probandum utitur, sine interioribus vasorum. ## Notitiae Cargoe hospites ad repositorium, API docs.rs, README et licentiam ducunt. ## Context::last_execution() numeros instructionum et alimenti reliqui ostendit, sine tabulis VM. ## Argumenta post -- ut series chordarum ordinaria argv praebentur. diff --git a/manuals/manual.zh-CN.qc b/manuals/manual.zh-CN.qc index 207f647..2b79c14 100644 --- a/manuals/manual.zh-CN.qc +++ b/manuals/manual.zh-CN.qc @@ -7,7 +7,7 @@ ## qcoffee --interactive(或 -i)逐行复用同一 Context;:help 显示命令,:quit 退出。 ## qcoffee --interactive --stats 仅为实际执行或运行时失败的非空输入行输出指令/燃料统计;解析、验证错误不输出新记录。 ## 'a☕中'[1] 为 '☕','a☕中'[1..2] 为 '☕中';字符串索引按 Unicode 标量。 -## for character, index in 'a☕中' then index 得 [0, 1, 2];字符串按 Unicode 标量遍历,不可用 by。 +## for character, index in 'a☕中' then index 得 [0, 1, 2];字符串按 Unicode 标量遍历,亦可用正整数 by。 ## [head, tail...] = [1, 2, 3] 将 tail 绑定为 [2, 3];数组模式 rest 必须居末。 ## qtest --fuel N 为每份可执行文档设置独立指令预算。 ## qtest --stats 将每份文档的指令数与剩余燃料写入标准错误,不改变 ok 输出。 @@ -22,6 +22,7 @@ ## qdocco --markdown 将说明、围栏 QuickCoffee 代码与最终值写成可审阅的 Markdown 产物。 ## 嵌入方可在运行之间调用 Context::set_fuel;Context::fuel 返回当前每轮预算,且不清除全局值。 ## cargo run --example embed 可编译最小 Rust 宿主:设置全局、注册原生回调并执行 QuickCoffee。 +## 宿主可用 Value::kind() 分流类型,用 Value::is_nil() 判断 nil,无须检查内部容器。 ## Cargo 包元数据指向仓库、docs.rs API、README 与许可证,便于嵌入方发现项目。 ## Context::last_execution() 提供指令数与剩余燃料统计,不暴露 VM 帧。 ## -- 后的参数以普通字符串数组 argv 暴露给程序。 diff --git a/src/bin/qbench.rs b/src/bin/qbench.rs index 23e8975..69d4ace 100644 --- a/src/bin/qbench.rs +++ b/src/bin/qbench.rs @@ -1,3 +1,5 @@ +//! Release benchmark runner with semantic guards and machine-readable timing output. + use quickcoffee::{Context, Engine}; use std::{env, process::ExitCode, time::Instant}; @@ -28,6 +30,11 @@ const WORKLOADS: &[Workload] = &[ source: "text = 'a☕中'\nitems = [10, 20, 30]\nitems[-1] + len(text[-2])", expected: "31", }, + Workload { + name: "stepped-string-iteration", + source: "sum = 0\nfor character, index in 'a☕中x' by 2 then sum += index\nsum", + expected: "2", + }, ]; fn usage() { diff --git a/src/bin/qdocco.rs b/src/bin/qdocco.rs index c5e9a9b..9e58f10 100644 --- a/src/bin/qdocco.rs +++ b/src/bin/qdocco.rs @@ -1,4 +1,6 @@ -use quickcoffee::{Context, Engine}; +//! Literate-programming renderer and checker for QuickCoffee sources. + +use quickcoffee::{Context, Engine, Value}; use std::{env, fs, path::PathBuf, process::ExitCode}; fn usage() { @@ -104,6 +106,10 @@ fn main() -> ExitCode { return ExitCode::from(1); } }; + if check && !matches!(&result, Value::Bool(true)) { + eprintln!("qdocco check failed: final value was {result}, expected true"); + return ExitCode::from(1); + } if !check { let destination = output.unwrap_or_else(|| input.with_extension(if markdown { "md" } else { "html" })); diff --git a/src/bin/qtest.rs b/src/bin/qtest.rs index 307e2e1..2fcd7e0 100644 --- a/src/bin/qtest.rs +++ b/src/bin/qtest.rs @@ -1,3 +1,5 @@ +//! Test runner for executable QuickCoffee scripts and literate manuals. + use quickcoffee::{Context, Value}; use std::{ env, fs, diff --git a/src/bytecode.rs b/src/bytecode.rs index 373b76a..c76d837 100644 --- a/src/bytecode.rs +++ b/src/bytecode.rs @@ -8,6 +8,7 @@ use std::{ }; /// A strict, recursively shaped binding pattern used by destructuring assignment. +#[allow(missing_docs)] #[derive(Clone, Debug)] pub enum Pattern { Ignore, @@ -25,11 +26,16 @@ pub enum Pattern { }, } +/// Verified bytecode consisting of a constant pool and instruction stream. #[derive(Clone, Debug, Default)] pub struct Chunk { + /// Values and nested functions referenced by instructions. pub constants: Vec, + /// Instructions executed by the VM. pub code: Vec, } +/// A value or nested function stored in a [`Chunk`] constant pool. +#[allow(missing_docs)] #[derive(Clone, Debug)] pub enum Constant { Value(Value), @@ -40,6 +46,8 @@ pub enum Constant { chunk: Rc, }, } +/// The public low-level instruction set accepted by [`Chunk::verify`]. +#[allow(missing_docs)] #[derive(Clone, Debug)] pub enum Instruction { Constant(usize), @@ -112,6 +120,7 @@ pub enum Instruction { } impl Chunk { + /// Returns a human-readable instruction listing with stable program counters. pub fn disassemble(&self) -> String { self.code .iter() @@ -129,6 +138,7 @@ impl Chunk { encoder.chunk(self); encoder.finish() } + /// Verifies stack/control-flow safety before execution. pub fn verify(&self) -> Result<(), Error> { if self.code.is_empty() { return Err(Error::verify("chunk is empty")); diff --git a/src/lib.rs b/src/lib.rs index eae61d8..89bd652 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![warn(missing_docs)] + //! CoffeeScript-inspired parser, compiler, bytecode VM, and embedding API. //! The public API intentionally exposes values and native functions, never JS-like objects. @@ -9,7 +11,7 @@ mod vm; pub use bytecode::{Chunk, Constant, Instruction, Pattern}; pub use vm::{ Context, Engine, Error, ErrorKind, ExecutionStats, Function, NativeFunction, Program, - SourcePosition, Value, + SourcePosition, Value, ValueKind, }; /// Compiles `source` to verified bytecode without executing it. diff --git a/src/main.rs b/src/main.rs index d45660a..02db01f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +//! Command-line entry point for the `qcoffee` interpreter. + use quickcoffee::{Context, Engine, Value}; use std::{ env, fs, diff --git a/src/parser.rs b/src/parser.rs index 8fb6965..3321edb 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -323,12 +323,12 @@ impl Parser { } break; } - let (key, literal_key) = match self.next() { - Token::Ident(key) => (key, false), + let key = match self.next() { + Token::Ident(key) => key, Token::String(key, interpolate) if !interpolate || !key.contains("#{") => { - (key, true) + key } _ => { self.at = saved; @@ -341,9 +341,6 @@ impl Parser { return None; }; self.pattern_default(pattern).ok()? - } else if literal_key { - self.at = saved; - return None; } else if key == "_" { self.pattern_default(Pattern::Ignore).ok()? } else { @@ -1147,7 +1144,7 @@ impl Parser { let iterable = self.expr(0)?; let step = if self.eat(&Token::By) { if map { - return Err(self.parse_error("by is supported only for array iteration")); + return Err(self.parse_error("by is supported only for enumerable iteration")); } Some(Box::new(self.expr(0)?)) } else { diff --git a/src/vm.rs b/src/vm.rs index 596cbbb..6cc8123 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -11,14 +11,41 @@ use std::{ const MAX_RANGE_ITEMS: i128 = 1_000_000; +/// Stable type tag for values crossing the embedding boundary. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ValueKind { + /// The sole empty value. + Nil, + /// A strict boolean. + Bool, + /// An IEEE-754 number. + Number, + /// An immutable UTF-8 string. + String, + /// An immutable array. + Array, + /// An immutable string-keyed map. + Map, + /// An opaque bytecode or native function. + Function, +} + +/// An immutable value crossing the QuickCoffee/host boundary. #[derive(Clone)] pub enum Value { + /// The sole empty value. Nil, + /// A strict boolean. Bool(bool), + /// An IEEE-754 number used by the language. Number(f64), + /// An immutable UTF-8 string. String(Rc), + /// An immutable array of values. Array(Rc>), + /// An immutable map with string keys. Map(Rc>), + /// An opaque bytecode or native function. Function(Rc), } impl fmt::Debug for Value { @@ -66,6 +93,22 @@ impl fmt::Display for Value { } } impl Value { + /// Returns a stable type tag without exposing the internal container representation. + pub fn kind(&self) -> ValueKind { + match self { + Self::Nil => ValueKind::Nil, + Self::Bool(_) => ValueKind::Bool, + Self::Number(_) => ValueKind::Number, + Self::String(_) => ValueKind::String, + Self::Array(_) => ValueKind::Array, + Self::Map(_) => ValueKind::Map, + Self::Function(_) => ValueKind::Function, + } + } + /// Returns whether this value is the language's `nil` value. + pub fn is_nil(&self) -> bool { + matches!(self, Self::Nil) + } /// Builds a QuickCoffee string without exposing its `Rc` storage. pub fn string(value: impl Into>) -> Self { Self::String(value.into()) @@ -87,6 +130,7 @@ impl Value { .collect(), )) } + /// Returns the number, if this value is numeric. pub fn as_number(&self) -> Option { if let Self::Number(x) = self { Some(*x) @@ -94,6 +138,7 @@ impl Value { None } } + /// Returns the boolean, if this value is boolean. pub fn as_bool(&self) -> Option { if let Self::Bool(x) = self { Some(*x) @@ -101,6 +146,7 @@ impl Value { None } } + /// Returns the UTF-8 view, if this value is a string. pub fn as_str(&self) -> Option<&str> { if let Self::String(x) = self { Some(x) @@ -108,6 +154,7 @@ impl Value { None } } + /// Returns an immutable slice, if this value is an array. pub fn as_array(&self) -> Option<&[Value]> { if let Self::Array(values) = self { Some(values) @@ -115,6 +162,7 @@ impl Value { None } } + /// Returns an immutable map view, if this value is a map. pub fn as_map(&self) -> Option<&BTreeMap> { if let Self::Map(values) = self { Some(values) @@ -151,13 +199,17 @@ impl From<&str> for Value { /// Stable category for an error crossing the Rust embedding boundary. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ErrorKind { + /// Lexing or parsing failed. Parse, + /// Untrusted bytecode failed verification. Verify, + /// Execution or a host callback failed. Runtime, } /// One-based source line attached to a lexical or parse diagnostic. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SourcePosition { + /// One-based line number. pub line: usize, } impl fmt::Display for ErrorKind { @@ -169,6 +221,7 @@ impl fmt::Display for ErrorKind { } } } +/// A structured error suitable for CLI display or host-side branching. #[derive(Debug, Clone)] pub struct Error { kind: ErrorKind, @@ -230,6 +283,7 @@ impl fmt::Display for Error { } impl std::error::Error for Error {} +/// A host callback callable from QuickCoffee code. pub type NativeFunction = Rc Result>; /// Opaque callable values are constructed by QuickCoffee or `Context::add_native`. pub struct Function { @@ -266,6 +320,7 @@ fn lookup(e: &Env, n: &str) -> Option { p.and_then(|p| lookup(&p, n)) } +/// A reusable compiler that does not hold execution state. #[derive(Clone, Default)] pub struct Engine; /// A reference-counted compiled program for repeated execution. @@ -279,6 +334,7 @@ struct ProgramInner { chunk: Rc, verified: Cell, } +/// A cheaply cloneable, verified bytecode program for repeated execution. #[derive(Clone, Debug)] pub struct Program(Rc); impl From for Program { @@ -290,6 +346,7 @@ impl From for Program { } } impl Program { + /// Verifies the program and caches a successful result. pub fn verify(&self) -> Result<(), Error> { let result = self.0.chunk.verify(); if result.is_ok() { @@ -297,6 +354,7 @@ impl Program { } result } + /// Returns a human-readable disassembly of the shared bytecode. pub fn disassemble(&self) -> String { self.0.chunk.disassemble() } @@ -313,9 +371,11 @@ impl Program { } } impl Engine { + /// Creates a stateless compiler. pub fn new() -> Self { Self } + /// Compiles and verifies source into an owned bytecode chunk. pub fn compile(&self, source: &str) -> Result { compile(source) } @@ -335,6 +395,7 @@ pub struct ExecutionStats { /// Fuel left after the execution stopped. pub fuel_remaining: u64, } +/// An execution context containing globals, builtins, and a per-run fuel budget. pub struct Context { engine: Engine, global: Env, @@ -347,6 +408,7 @@ impl Default for Context { } } impl Context { + /// Creates a context with standard library builtins and the default fuel budget. pub fn new() -> Self { let global = env(None); let mut x = Self { @@ -358,6 +420,7 @@ impl Context { x.install_builtins(); x } + /// Returns a builder-style context with the supplied fuel budget. pub fn with_fuel(mut self, fuel: u64) -> Self { self.set_fuel(fuel); self @@ -378,6 +441,7 @@ impl Context { pub fn last_execution(&self) -> ExecutionStats { self.last_execution } + /// Installs or replaces an immutable global value visible to later runs. pub fn set_global(&mut self, name: impl Into, value: Value) { self.global.borrow_mut().values.insert(name.into(), value); } @@ -385,6 +449,7 @@ impl Context { pub fn get_global(&self, name: &str) -> Option { lookup(&self.global, name) } + /// Registers a host callback as an opaque callable global. pub fn add_native(&mut self, name: impl Into, f: F) where F: Fn(&[Value]) -> Result + 'static, @@ -396,10 +461,12 @@ impl Context { })), ); } + /// Compiles, verifies, and executes source in this context. pub fn eval(&mut self, source: &str) -> Result { let program = self.engine.compile_program(source)?; self.run_program(&program) } + /// Verifies and executes an owned bytecode chunk. pub fn run(&mut self, chunk: Chunk) -> Result { self.run_program(&chunk.into()) } @@ -564,6 +631,7 @@ enum IterationKind { String { values: Rc>, position: usize, + step: usize, }, Map { entries: Vec<(String, Value)>, @@ -793,11 +861,6 @@ impl Vm { }, }), Value::String(value) => { - if step != 1 { - return Err(Error::runtime( - "string iteration does not support a by step", - )); - } frame.iterators.push(Iteration { kind: IterationKind::String { values: Rc::new( @@ -809,6 +872,7 @@ impl Vm { .collect(), ), position: 0, + step, }, }); } @@ -855,7 +919,11 @@ impl Vm { } value } - IterationKind::String { values, position } => { + IterationKind::String { + values, + position, + step, + } => { let value = values.get(*position).cloned().map(|value| { if patterns.len() == 2 { vec![value, Value::Number(*position as f64)] @@ -864,7 +932,7 @@ impl Vm { } }); if value.is_some() { - *position += 1; + *position = position.saturating_add(*step); } value } diff --git a/tests/cli_tools.rs b/tests/cli_tools.rs index 37ba11d..2fe3168 100644 --- a/tests/cli_tools.rs +++ b/tests/cli_tools.rs @@ -23,9 +23,17 @@ fn qdocco_renders_escaped_source_and_checks() { let page = fs::read_to_string(&output).unwrap(); assert!(page.contains("<Guide>")); assert!(page.contains("Final value: 3")); + let non_test_document = Command::new(bin("qdocco")) + .args(["--check", input.to_str().unwrap()]) + .output() + .unwrap(); + assert_eq!(non_test_document.status.code(), Some(1)); + assert!(String::from_utf8_lossy(&non_test_document.stderr).contains("expected true")); + let check_input = temp.join("check.qc"); + fs::write(&check_input, "true\n").unwrap(); assert!( Command::new(bin("qdocco")) - .args(["--check", input.to_str().unwrap()]) + .args(["--check", check_input.to_str().unwrap()]) .status() .unwrap() .success() @@ -344,7 +352,8 @@ fn qbench_json_is_guarded_and_machine_readable() { assert!(json.status.success()); let stdout = String::from_utf8_lossy(&json.stdout); let lines: Vec<_> = stdout.lines().collect(); - assert_eq!(lines.len(), 4); + assert_eq!(lines.len(), 5); + assert!(stdout.contains("\"name\":\"stepped-string-iteration\"")); for line in lines { assert!(line.starts_with('{') && line.ends_with('}')); for field in [ diff --git a/tests/embedding_api.rs b/tests/embedding_api.rs index a799a95..20a4c95 100644 --- a/tests/embedding_api.rs +++ b/tests/embedding_api.rs @@ -1,4 +1,4 @@ -use quickcoffee::{Context, Engine, Error, ErrorKind, Value}; +use quickcoffee::{Context, Engine, Error, ErrorKind, Value, ValueKind}; #[test] fn public_embedding_surface_runs_shared_programs_with_host_state() { @@ -12,10 +12,7 @@ fn public_embedding_surface_runs_shared_programs_with_host_state() { let mut context = Context::new(); context.set_global("factor", Value::from(2_i64)); context.add_native("host", |args| { - let (Some(left), Some(right)) = ( - args.first().and_then(Value::as_number), - args.get(1).and_then(Value::as_number), - ) else { + let (Some(left), Some(right)) = (args[0].as_number(), args[1].as_number()) else { return Err(Error::runtime("host expects two numbers")); }; Ok(Value::from(left + right)) @@ -26,9 +23,6 @@ fn public_embedding_surface_runs_shared_programs_with_host_state() { Some(84.) ); assert_eq!(context.run_program(&clone).unwrap().as_number(), Some(84.)); - let missing = context.eval("host(20)").unwrap_err(); - assert_eq!(missing.kind(), ErrorKind::Runtime); - assert_eq!(missing.message(), "host expects two numbers"); assert_eq!(context.get_global("factor").unwrap().as_number(), Some(2.)); assert!(context.get_global("missing").is_none()); } @@ -44,9 +38,23 @@ fn public_values_and_native_errors_are_structured() { ]), ); let values = context.get_global("host_values").unwrap(); + assert_eq!(values.kind(), ValueKind::Map); + assert!(!values.is_nil()); let map = values.as_map().unwrap(); assert_eq!(map["answer"].as_number(), Some(42.)); assert_eq!(map["items"].as_array().unwrap()[0].as_str(), Some("coffee")); + assert_eq!(map["items"].kind(), ValueKind::Array); + assert_eq!(Value::from(false).kind(), ValueKind::Bool); + assert_eq!(Value::from(0_i64).kind(), ValueKind::Number); + assert_eq!(Value::from("coffee").kind(), ValueKind::String); + assert_eq!( + context.eval("(x) -> x").unwrap().kind(), + ValueKind::Function + ); + assert!(!Value::from(false).is_nil()); + assert!(!Value::from(0_i64).is_nil()); + assert_eq!(Value::Nil.kind(), ValueKind::Nil); + assert!(Value::Nil.is_nil()); context.add_native("fail", |_| Err(Error::runtime("host failed"))); let error = context.eval("fail()").unwrap_err(); diff --git a/tests/rfc_core.rs b/tests/rfc_core.rs index 1720d01..eaad428 100644 --- a/tests/rfc_core.rs +++ b/tests/rfc_core.rs @@ -983,9 +983,34 @@ fn string_iteration_uses_unicode_scalars_and_optional_scalar_indices() { eval("for character in 'a☕中' when character == '☕' then character").to_string(), "[☕]" ); + assert_eq!( + eval("for character in 'a☕中x' by 2 then character").to_string(), + "[a, 中]" + ); + assert_eq!( + eval("for character, index in 'a☕中x' by 2 then index").to_string(), + "[0, 2]" + ); + assert_eq!( + eval("step = 2\nfor character in 'a☕中x' by step then character").to_string(), + "[a, 中]" + ); + assert_eq!( + eval("for character in 'a☕中x' by 2 when character != '中' then character").to_string(), + "[a]" + ); + assert_eq!( + eval("for character in '' by 2 then character").to_string(), + "[]" + ); + assert!( + Context::new() + .eval("for character in 'abc' by 0 then character") + .is_err() + ); assert!( Context::new() - .eval("for character in 'abc' by 2 then character") + .eval("for character in 'abc' by 1.5 then character") .is_err() ); let chunk = compile("for character in 'abc' then character").unwrap(); @@ -1314,11 +1339,6 @@ fn map_destructuring_accepts_literal_string_keys() { eval("{'answer': value} = {answer: 42}\nvalue").as_number(), Some(42.) ); - assert!( - Context::new() - .eval("{\"first-name\"} = {\"first-name\": 'Ada'}") - .is_err() - ); assert!( Context::new() .eval("{\"missing-key\": value} = {other: 1}")