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: Vec3"));
+ 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}")