From 9a351437aba8f7f4279decdb6f3331ebd410ab69 Mon Sep 17 00:00:00 2001 From: tiye Date: Sun, 23 Aug 2026 02:40:48 +0800 Subject: [PATCH 1/2] feat: rebase ValueKind API onto public docs --- PERFORMANCE.md | 10 ++++++ README.md | 2 +- RFCs/0070-string-iteration.md | 11 ++++-- RFCs/0093-value-kind-inspection.md | 26 ++++++++++++++ RFCs/0094-qdocco-final-true-check.md | 22 ++++++++++++ RFCs/0095-string-iteration-by-step.md | 23 ++++++++++++ RFCs/0096-stepped-string-benchmark.md | 24 +++++++++++++ benches/core.rs | 6 ++++ docs/manual.classical-zh.html | 3 +- docs/manual.classical-zh.md | 2 +- docs/manual.devanagari-sa.html | 3 +- docs/manual.devanagari.sa.md | 2 +- docs/manual.en.html | 3 +- docs/manual.en.md | 2 +- docs/manual.latin.html | 3 +- docs/manual.latin.md | 2 +- docs/manual.zh-CN.html | 3 +- docs/manual.zh-CN.md | 2 +- docs/syntax.en.md | 4 ++- docs/syntax.zh-CN.md | 2 +- manuals/manual.classical-zh.qc | 3 +- manuals/manual.devanagari-sa.qc | 3 +- manuals/manual.en.qc | 3 +- manuals/manual.latin.qc | 3 +- manuals/manual.zh-CN.qc | 3 +- src/bin/qbench.rs | 5 +++ src/bin/qdocco.rs | 6 +++- src/lib.rs | 2 +- src/parser.rs | 2 +- src/vm.rs | 50 +++++++++++++++++++++++---- tests/cli_tools.rs | 13 +++++-- tests/embedding_api.rs | 7 +++- tests/rfc_core.rs | 27 ++++++++++++++- 33 files changed, 248 insertions(+), 34 deletions(-) create mode 100644 RFCs/0093-value-kind-inspection.md create mode 100644 RFCs/0094-qdocco-final-true-check.md create mode 100644 RFCs/0095-string-iteration-by-step.md create mode 100644 RFCs/0096-stepped-string-benchmark.md 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 3f8ae98..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/0092-public-api-documentation.md](RFCs/0092-public-api-documentation.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/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..24467c7 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 f33de08..69d4ace 100644 --- a/src/bin/qbench.rs +++ b/src/bin/qbench.rs @@ -30,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 8e21e0e..9e58f10 100644 --- a/src/bin/qdocco.rs +++ b/src/bin/qdocco.rs @@ -1,6 +1,6 @@ //! Literate-programming renderer and checker for QuickCoffee sources. -use quickcoffee::{Context, Engine}; +use quickcoffee::{Context, Engine, Value}; use std::{env, fs, path::PathBuf, process::ExitCode}; fn usage() { @@ -106,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/lib.rs b/src/lib.rs index f490db3..89bd652 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,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/parser.rs b/src/parser.rs index 5dff50d..3321edb 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1144,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 21d7380..6cc8123 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -11,6 +11,25 @@ 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 { @@ -74,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()) @@ -596,6 +631,7 @@ enum IterationKind { String { values: Rc>, position: usize, + step: usize, }, Map { entries: Vec<(String, Value)>, @@ -825,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( @@ -841,6 +872,7 @@ impl Vm { .collect(), ), position: 0, + step, }, }); } @@ -887,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)] @@ -896,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 9376959..0416134 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() { @@ -38,9 +38,14 @@ 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::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 7d60de9..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(); From 6abf296c5947a8c3bea265b987b2ed6e540aad8d Mon Sep 17 00:00:00 2001 From: tiye Date: Sun, 23 Aug 2026 11:45:26 +0800 Subject: [PATCH 2/2] test: cover all public value kinds --- docs/manual.devanagari.sa.md | 2 +- tests/embedding_api.rs | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/manual.devanagari.sa.md b/docs/manual.devanagari.sa.md index 24467c7..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 द्वारा पुनःप्रयुक्त-सन्दर्भस्य सीमा परिवर्तयितुं शक्नोति, Value::kind() तथा Value::is_nil() द्वारा प्रकारं परीक्षते, तथा `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/tests/embedding_api.rs b/tests/embedding_api.rs index 0416134..20a4c95 100644 --- a/tests/embedding_api.rs +++ b/tests/embedding_api.rs @@ -44,6 +44,15 @@ fn public_values_and_native_errors_are_structured() { 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());