diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3d57aa..2cf2a6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,8 +10,15 @@ permissions: jobs: check: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + toolchain: ["1.85.0", stable] steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ matrix.toolchain }} + components: rustfmt, clippy - run: make docs && make check - run: git diff --exit-code -- docs diff --git a/Cargo.toml b/Cargo.toml index 9ea86b7..52692d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ name = "quickcoffee" version = "0.1.0" edition = "2024" +rust-version = "1.85" description = "A compact CoffeeScript-inspired bytecode engine" license = "MIT OR Apache-2.0" readme = "README.md" diff --git a/Makefile b/Makefile index ac9b078..171b98e 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,15 @@ -.PHONY: fmt test examples package-metadata package qbench-check clippy api-doc docs doc-check check bench qbench +.PHONY: fmt test release-test examples package-metadata package qbench-check clippy api-doc docs doc-check check bench qbench fmt: cargo fmt --check test: + cargo build --locked --bins cargo test --locked +release-test: + cargo test --locked --release + examples: cargo test --locked --examples @@ -35,7 +39,7 @@ docs: doc-check cargo run --locked --quiet --bin qdocco -- manuals/manual.latin.qc -o docs/manual.latin.html cargo run --locked --quiet --bin qdocco -- manuals/manual.devanagari-sa.qc -o docs/manual.devanagari-sa.html -check: fmt test examples package-metadata package qbench-check clippy api-doc doc-check +check: fmt test release-test examples package-metadata package qbench-check clippy api-doc doc-check bench: cargo bench --locked --bench core diff --git a/PERFORMANCE.md b/PERFORMANCE.md index 435f7eb..fc0d4bc 100644 --- a/PERFORMANCE.md +++ b/PERFORMANCE.md @@ -23,6 +23,8 @@ make qbench cargo run --locked --release --bin qbench -- --json --iterations 100 --repeat 3 ``` +调试单一回归时,先用 `qbench --list` 查看确定性的内建负载名,再用 `qbench --only NAME` 只运行该负载;不指定 `--only` 始终运行完整集合,持续门禁口径不变。 + `qbench` 为每个内建负载输出一行 JSON,分别记录编译、验证和执行的纳秒总耗时,并在计时循环中校验预期最终值。默认 `--repeat 1` 适合快速 CI 回归;需要正式三次 release 中位数时使用 `--repeat 3`,其结果可直接作为下文报告数据。 测量环境:Apple arm64(Darwin 25.5.0,T6000)、`rustc 1.94.0 (4a4ef493e 2026-03-02)`、`cargo bench --bench core`。基准以 release profile 运行;报告日期为 2026-08-22。 diff --git a/README.md b/README.md index c60cb60..7e6ccc2 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ QuickCoffee 是一台以 Rust 编写、受 CoffeeScript 启发的字节码脚本引擎。它保留紧凑、可读的表达式语法,却不兼容 JavaScript:没有原型链、`this`、`eval` 或嵌入 JavaScript。 -当前实现遵循 [RFCs/0000-project-scope.md](RFCs/0000-project-scope.md) 至 [RFCs/0109-qbench-core-suite.md](RFCs/0109-qbench-core-suite.md)。 +当前实现遵循 [RFCs/0000-project-scope.md](RFCs/0000-project-scope.md) 至 [RFCs/0113-numeric-standard-library.md](RFCs/0113-numeric-standard-library.md)。 +构建要求 Rust 1.85 或更新版本(Edition 2024);CI 同时验证 MSRV 与 stable 工具链。 ```coffee square = (x) -> x * x @@ -27,6 +28,8 @@ cargo run -- --dump-bytecode example.qc cargo run -- --fingerprint example.qc cargo run --release --bin qbench -- --json --iterations 100 cargo run --release --bin qbench -- --json --iterations 100 --repeat 3 +cargo run --release --bin qbench -- --list +cargo run --release --bin qbench -- --only map-spread --json --iterations 100 cargo run --locked --quiet --release --bin qbench -- --json --iterations 1 --repeat 3 cargo run --example embed cargo run --bin qdocco -- example.qc -o example.html @@ -43,7 +46,7 @@ cargo run --bin qbench -- --version ## 验收 -`make check` 运行格式检查、全部测试(含外部嵌入 API 集成测试和 1,024 条确定性编译压力语料)、零警告 Clippy 和五份可执行手册校验;`make docs` 从文学编程源重新生成 HTML;`make bench` 运行 release 基准。项目禁止 `unsafe`。 +`make check` 运行格式检查、debug 与 release 两套全部测试(含外部嵌入 API 集成测试和 1,024 条确定性编译压力语料)、零警告 Clippy 和五份可执行手册校验;`make docs` 从文学编程源重新生成 HTML;`make bench` 运行 release 基准。项目禁止 `unsafe`。 手册源在 `manuals/`,每份都是可执行的 Docco 输入。生成 HTML: diff --git a/RFCs/0000-project-scope.md b/RFCs/0000-project-scope.md index b54963c..bdbba12 100644 --- a/RFCs/0000-project-scope.md +++ b/RFCs/0000-project-scope.md @@ -20,4 +20,4 @@ QuickCoffee 是一个 Rust 实现的、受 CoffeeScript 2016 启发的脚本引 本仓库中的测试即 0.1 的语义基线。对语法或运行时的新增特性必须先以 RFC 补充定义,并至少添加:成功测试、错误测试及字节码验证测试。 -当前已实现的后续语义与工具 RFC 延伸至 RFC 0109;其中 RFC 0077 定义 JSON 输出、RFC 0079 定义 TAP 输出、RFC 0080 定义 CLI 字节码指纹、RFC 0081 定义可机器读取的基准输出、RFC 0082 规范化指纹编码、RFC 0083 定义 Markdown 文学编程产物、RFC 0084 定义嵌入上下文 fuel 控制、RFC 0085 定义可执行 Rust 嵌入示例、RFC 0086 定义 crate 发布元数据、RFC 0094 定义 qdocco 最终值门禁、RFC 0095 定义字符串步进迭代、RFC 0096 定义其性能基准、RFC 0097 定义 `do` 参数转发、RFC 0098 定义 RFC 索引门禁、RFC 0099 定义 `!` 否定别名、RFC 0100 定义有符号 `by` 步长、RFC 0101 定义 qdocco 原子输出、RFC 0102 定义 qtest 规范文件去重、RFC 0103 定义 qbench schema 版本、RFC 0104 定义 qdocco 块注释代码保留、RFC 0105 定义 qbench 重复采样中位数、RFC 0106 定义 crate 发布包验收门禁、RFC 0107 定义 release qbench 持续门禁、RFC 0108 定义 qtest 可执行示例语料、RFC 0109 定义 qbench 核心负载全套护栏,均不改变脚本语言值模型的原型无关约束。 +当前已实现的后续语义与工具 RFC 延伸至 RFC 0113;其中 RFC 0077 定义 JSON 输出、RFC 0079 定义 TAP 输出、RFC 0080 定义 CLI 字节码指纹、RFC 0081 定义可机器读取的基准输出、RFC 0082 规范化指纹编码、RFC 0083 定义 Markdown 文学编程产物、RFC 0084 定义嵌入上下文 fuel 控制、RFC 0085 定义可执行 Rust 嵌入示例、RFC 0086 定义 crate 发布元数据、RFC 0094 定义 qdocco 最终值门禁、RFC 0095 定义字符串步进迭代、RFC 0096 定义其性能基准、RFC 0097 定义 `do` 参数转发、RFC 0098 定义 RFC 索引门禁、RFC 0099 定义 `!` 否定别名、RFC 0100 定义有符号 `by` 步长、RFC 0101 定义 qdocco 原子输出、RFC 0102 定义 qtest 规范文件去重、RFC 0103 定义 qbench schema 版本、RFC 0104 定义 qdocco 块注释代码保留、RFC 0105 定义 qbench 重复采样中位数、RFC 0106 定义 crate 发布包验收门禁、RFC 0107 定义 release qbench 持续门禁、RFC 0108 定义 qtest 可执行示例语料、RFC 0109 定义 qbench 核心负载全套护栏、RFC 0110 定义 Rust MSRV 契约、RFC 0111 定义 release profile 完整测试门禁、RFC 0112 定义 qbench 负载枚举与选择、RFC 0113 定义严格数值标准库函数,均不改变脚本语言值模型的原型无关约束。 diff --git a/RFCs/0001-language-core.md b/RFCs/0001-language-core.md index 66c6429..3c7d514 100644 --- a/RFCs/0001-language-core.md +++ b/RFCs/0001-language-core.md @@ -39,7 +39,7 @@ UTF-8 文本,以换行或分号分隔语句。缩进可形成 `if`、`unless` RFC 0095 扩展字符串 `for` 也可使用 `by` 步长;RFC 0100 规定其支持非零有限有符号整数并以负步长倒序;RFC 0097 规定 `do (name) -> ...` 从同名外层变量转发立即调用实参。 -预置函数是普通名称,不是对象原型方法:`print(value...)`、`len(value)`、`type(value)`、`range(start, end)`、`str(value)`、`keys(map)`、`values(map)`、`join(array, separator)`、`split(string, separator)`、`assert(bool, message?)`。它们的行为由 RFC 0003 的宿主接口定义。不存在 `console`、`Object.prototype`、`Array.prototype`。 +预置函数是普通名称,不是对象原型方法:`print(value...)`、`len(value)`、`type(value)`、`range(start, end)`、`str(value)`、`abs(number)`、`sum(array)`、`min(array)`、`max(array)`、`keys(map)`、`values(map)`、`join(array, separator)`、`split(string, separator)`、`assert(bool, message?)`。数值聚合只接受有限数;空数组的 `sum` 为 `0`,`min`/`max` 要求非空。它们的行为由 RFC 0003 与 RFC 0113 的宿主接口定义。不存在 `console`、`Object.prototype`、`Array.prototype`。 ### 后续契约修订 diff --git a/RFCs/0108-qtest-showcase-corpus.md b/RFCs/0108-qtest-showcase-corpus.md index 1d418d6..8b09ec5 100644 --- a/RFCs/0108-qtest-showcase-corpus.md +++ b/RFCs/0108-qtest-showcase-corpus.md @@ -16,7 +16,7 @@ 3. 使用已经采纳的 QuickCoffee 语法,覆盖至少一个可观察的核心语义; 4. 可由 `qtest tests/scripts` 递归发现并以默认 fuel 完成。 -示例语料至少覆盖数值与闭包、map spread/解构、Unicode 字符串索引、筛选 comprehension,以及循环控制。语料只验证语义结果,不比较实现细节或运行时间;性能门禁仍由 RFC 0045、RFC 0081、RFC 0096 与 RFC 0107 负责。 +示例语料至少覆盖数值与闭包、map spread/解构、Unicode 字符串索引、筛选 comprehension、循环控制,以及无原型标准库的组合调用。标准库样例必须覆盖 `range`、`len`、`type`、`str`、`keys`、`values`、`join`、`split` 与成功的 `assert`。语料只验证语义结果,不比较实现细节或运行时间;性能门禁仍由 RFC 0045、RFC 0081、RFC 0096 与 RFC 0107 负责。 ## 验收 diff --git a/RFCs/0110-rust-msrv-contract.md b/RFCs/0110-rust-msrv-contract.md new file mode 100644 index 0000000..34db63e --- /dev/null +++ b/RFCs/0110-rust-msrv-contract.md @@ -0,0 +1,14 @@ +# RFC 0110:Rust 最低版本契约 + +- 状态:已采纳 +- 依赖:RFC 0000、RFC 0086、RFC 0087 + +## 契约 + +QuickCoffee 使用 Edition 2024,并将 Rust `1.85.0` 声明为 crate 的最低支持版本(MSRV)。`Cargo.toml` 的 `rust-version` 字段是发布元数据的一部分;新增依赖或语言特性不得无意中提高该版本。 + +## 验收 + +CI 必须对 Rust `1.85.0` 和当前 stable 各运行一次 `make docs && make check`。两套工具链都必须通过格式、全部测试、示例、crate 打包、release qbench、Clippy、rustdoc 及五份手册门禁。集成测试还必须确认 manifest 暴露 `rust-version = "1.85"`,避免文档与发布元数据分离。 + +MSRV 是编译器兼容性下限,不承诺旧平台的运行时性能;性能数据仍按 RFC 0096 与 RFC 0109 的 release 基准口径解释。 diff --git a/RFCs/0111-release-test-gate.md b/RFCs/0111-release-test-gate.md new file mode 100644 index 0000000..3905399 --- /dev/null +++ b/RFCs/0111-release-test-gate.md @@ -0,0 +1,14 @@ +# RFC 0111:release profile 完整测试门禁 + +- 状态:已采纳 +- 依赖:RFC 0045、RFC 0081、RFC 0107、RFC 0110 + +## 约束 + +`make check` 必须在 debug 与 release 两种 profile 执行完整 `cargo test --locked`。release 测试覆盖库单元测试、所有 CLI 集成测试、公开嵌入 API、五份文学手册、RFC 索引和鲁棒性语料;它不能只依赖 qbench 的少量计时负载。`cargo test --locked --examples` 仍以 debug profile 单独检查示例编译。 + +release qbench 继续负责优化 VM 的全套 34 个语义负载与 compile/verify/execute 计时;本 RFC 的目标是让非 benchmark 的 CLI、嵌入和错误路径也经过优化构建验证。门禁不设置机器相关的时间阈值。 + +## 验收 + +`make check` 必须包含 `cargo test --locked --release`,并在 Rust 1.85 与 stable 工具链上均成功。任何 profile 的测试失败都阻止 PR 进入 `CLEAN` 状态。 diff --git a/RFCs/0112-qbench-workload-selection.md b/RFCs/0112-qbench-workload-selection.md new file mode 100644 index 0000000..a2108bf --- /dev/null +++ b/RFCs/0112-qbench-workload-selection.md @@ -0,0 +1,19 @@ +# RFC 0112:qbench 负载枚举与选择 + +- 状态:已采纳 +- 依赖:RFC 0103、RFC 0105、RFC 0109、RFC 0111 + +## 动机 + +`qbench` 默认运行完整核心负载集,以便持续门禁;调试单个回归时,重复等待全部负载会降低反馈速度。CLI 需要提供不改变默认行为的确定性枚举与单负载选择。 + +## 契约 + +1. `qbench --list` 按内建顺序逐行输出每个负载名,不输出计时记录或诊断。 +2. `qbench --only NAME` 只运行精确匹配的一个负载;JSON 与文本输出格式、语义护栏、计时字段和 schema 不变。 +3. 未知名称必须以退出码 2 失败,并提示使用 `--list`;`--list` 与 `--only` 同时出现也以退出码 2 失败。 +4. 不指定 `--only` 时,默认完整负载集合与 RFC 0109 相同。 + +## 验收 + +`tests/cli_tools.rs` 必须验证完整枚举、单负载 JSON、未知名称和冲突参数;测试门禁先构建当前 CLI 二进制,避免 MSRV 环境回退到过期 target 产物;`make check` 继续执行完整 release qbench 门禁。 diff --git a/RFCs/0113-numeric-standard-library.md b/RFCs/0113-numeric-standard-library.md new file mode 100644 index 0000000..bd9f85f --- /dev/null +++ b/RFCs/0113-numeric-standard-library.md @@ -0,0 +1,20 @@ +# RFC 0113:严格数值标准库函数 + +- 状态:已采纳 +- 依赖:RFC 0001、RFC 0043、RFC 0108、RFC 0111 + +## 动机 + +QuickCoffee 的标准库是普通函数,而非 JavaScript 原型方法。已有容器与字符串函数足以演示数据流,但数值脚本仍需宿主自行注册最常见的绝对值和聚合操作。内建这些函数可让 qtest 语料、嵌入宿主和性能负载共享同一严格语义。 + +## 契约 + +- `abs(number)` 接受恰好一个有限数,返回其绝对值。 +- `sum(array)` 接受恰好一个数组;数组元素必须都是有限数,空数组和为 `0`。 +- `min(array)` 与 `max(array)` 接受恰好一个非空数组;元素必须都是有限数,分别返回最小值或最大值。 +- 参数数量、容器类型、元素类型或有限性不满足时,函数返回结构化运行时错误;不发生隐式转换,也不读取原型或成员方法。 +- 返回值仍是普通 `Number`,不改变字节码验证、fuel 或宿主注册函数规则。 + +## 验收 + +`tests/scripts/stdlib.qc` 验证成功路径;核心测试验证空数组、错误参数和非有限宿主数值的错误边界;五份文学手册列出函数。`make check` 必须继续通过 debug/release 全量测试及 qdocco 门禁。 diff --git a/docs/manual.classical-zh.html b/docs/manual.classical-zh.html index e45fafb..b23cfc1 100644 --- a/docs/manual.classical-zh.html +++ b/docs/manual.classical-zh.html @@ -35,7 +35,7 @@

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

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

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

-

其内府皆常函,如 print、len、type、range、str、keys、values、join、split、assert。

+

其内府皆常函,如 print、len、type、range、str、abs、sum、min、max、keys、values、join、split、assert;数聚函唯受有限数之列。

函式取词法之境;末常参可书 y = 2,参缺或传 nil 则于函中取其值;末有余参,则书 tail...。

常名之参可省其括:left, right -> left + right;常值、余参与解构仍须括之。

return expression 惟函中可用,反其值而终此函;徒 return 得 nil,且清环行 finally。

diff --git a/docs/manual.classical-zh.md b/docs/manual.classical-zh.md index cce4f91..d96ff9f 100644 --- a/docs/manual.classical-zh.md +++ b/docs/manual.classical-zh.md @@ -14,7 +14,7 @@ QuickCoffee 者,Rust 所为字节码机也,非 JavaScript 之运行时。其 严等与数之比较,相连可书 `1 < middle() < 3`;中项惟求一遍,前较既否,后项不求。 -欲试之,曰:`qcoffee -e "print(range(1, 4))"`。`--check FILE` 者,析、编、验其文而不行;`--fuel N` 者,限所行指令之数也;数尽则止而报误。内府有 `print`、`len`、`type`、`range`、`str`、`keys`、`values`、`join`、`split`、`assert`;`range(a, b)` 取自 a 至 b 前。 +欲试之,曰:`qcoffee -e "print(range(1, 4))"`。`--check FILE` 者,析、编、验其文而不行;`--fuel N` 者,限所行指令之数也;数尽则止而报误。内府有 `print`、`len`、`type`、`range`、`str`、`abs`、`sum`、`min`、`max`、`keys`、`values`、`join`、`split`、`assert`;数聚函唯受有限数之列;`range(a, b)` 取自 a 至 b 前。 `qcoffee -` 自标准输入读其文,便于管道;`qcoffee --dump-bytecode -` 则析其指令而不行之。 `qcoffee --stats` 则书所试指令与余燃料于标准错误,程序之标准输出不改;每次惟一源码输入,执行模式相冲则报用法之误。 diff --git a/docs/manual.devanagari-sa.html b/docs/manual.devanagari-sa.html index edcc014..03b3978 100644 --- a/docs/manual.devanagari-sa.html +++ b/docs/manual.devanagari-sa.html @@ -36,7 +36,7 @@

yes/on true, no/off false; is/isnt strict-साम्यम् स्तः।

! strict-Bool not-पर्यायः अस्ति; != strict-असाम्यमेव तिष्ठति।

chained-comparison मध्ये मध्य-मूल्य एकवारं, पूर्व-false चेत् short-circuit भवति।

-

सामान्य-library साधारण-function रूपेण print, len, type, range, str, keys, values, join, split, assert ददाति।

+

सामान्य-library साधारण-function रूपेण print, len, type, range, str, abs, sum, min, max, keys, values, join, split, assert ददाति; संख्या-संग्रहाः केवलं finite-संख्याः गृह्णन्ति।

कार्यं lexical-environment गृह्णाति; y = 2 omitting अथवा nil दत्ते कार्यस्य अन्तरे default भवति; अन्तिमः rest-parameter tail... इति लिख्यते।

bare-name lambda left, right -> left + right भवति; default, rest, pattern तु parentheses गृह्णन्ति।

return expression केवलं वर्तमान-कार्यं समाप्तं करोति; केवलः return nil फलति, loop शुद्धीकरोति, finally च चलयति।

diff --git a/docs/manual.devanagari.sa.md b/docs/manual.devanagari.sa.md index 2dc2466..65d5dc9 100644 --- a/docs/manual.devanagari.sa.md +++ b/docs/manual.devanagari.sa.md @@ -14,7 +14,7 @@ CoffeeScript-नामानि type न परिवर्तयन्ति: ` strict अथवा numeric-comparison श्रृङ्खला भवति: `1 < middle() < 3` मध्ये middle एकवारं मूल्यते, पूर्वं false चेत् परं न मूल्यते। -`qcoffee -e "print(range(1, 4))"` प्रयुञ्जीत। `qcoffee --check FILE` स्रोतं compile-verify करोति, न चालयति; `--fuel N` निर्देश-संख्यां सीमयति। `print`, `len`, `type`, `range`, `str`, `keys`, `values`, `join`, `split`, `assert` मानक-सहायकाः; `range(a, b)` मध्ये `b` न गृह्यते। +`qcoffee -e "print(range(1, 4))"` प्रयुञ्जीत। `qcoffee --check FILE` स्रोतं compile-verify करोति, न चालयति; `--fuel N` निर्देश-संख्यां सीमयति। `print`, `len`, `type`, `range`, `str`, `abs`, `sum`, `min`, `max`, `keys`, `values`, `join`, `split`, `assert` मानक-सहायकाः; संख्या-संग्रहाः केवलं finite-संख्याः गृह्णन्ति; `range(a, b)` मध्ये `b` न गृह्यते। `qcoffee -` मानक-input तः स्रोतः पठति; `qcoffee --dump-bytecode -` तमेव स्रोतं चालनं विना विच्छिनत्ति। `qcoffee --stats` instruction-संख्या तथा अवशिष्ट-fuel standard error मध्ये लिखति, stdout अपरिवर्तितं स्थापयति; एकमेव source ग्राह्यः, विरोधि execution-mode तु usage-दोषं जनयति। diff --git a/docs/manual.en.html b/docs/manual.en.html index 1631674..cb419cf 100644 --- a/docs/manual.en.html +++ b/docs/manual.en.html @@ -32,7 +32,7 @@

yes/on and no/off are Boolean aliases; is/isnt preserve strict equality.

! is a strict Bool alias for not; != remains strict inequality.

Chained strict or numeric comparisons keep the middle value once and short-circuit.

-

The standard library is ordinary functions: print, len, type, range, str, keys, values, join, split, and assert.

+

The standard library is ordinary functions: print, len, type, range, str, abs, sum, min, max, keys, values, join, split, and assert. Numeric aggregators accept strict finite-number arrays.

switch/when selects one strict-equality branch without fallthrough.

try/catch/finally handles QuickCoffee runtime errors without JavaScript Error objects.

Integer ranges use [1..3] for an inclusive end and [1...3] for an exclusive end.

diff --git a/docs/manual.en.md b/docs/manual.en.md index 965f217..e19b5e9 100644 --- a/docs/manual.en.md +++ b/docs/manual.en.md @@ -14,7 +14,7 @@ CoffeeScript-style spellings are available without changing runtime types: `yes` Adjacent strict or numeric comparisons may chain: `1 < middle() < 3` evaluates `middle()` once and stops before later operands when an earlier comparison is false. -Run `qcoffee -e "print(range(1, 4))"`, `qcoffee --fuel 10000 program.qc`, `qcoffee --check program.qc`, or `qcoffee --dump-bytecode program.qc`. `--check` parses, compiles, and verifies without executing. Fuel limits executed instructions; exhaustion is a safe error. The small standard library contains `print`, `len`, `type`, end-exclusive `range(a, b)`, `str`, `keys`, `values`, `join`, `split`, and `assert`. +Run `qcoffee -e "print(range(1, 4))"`, `qcoffee --fuel 10000 program.qc`, `qcoffee --check program.qc`, or `qcoffee --dump-bytecode program.qc`. `--check` parses, compiles, and verifies without executing. Fuel limits executed instructions; exhaustion is a safe error. The small standard library contains `print`, `len`, `type`, end-exclusive `range(a, b)`, `str`, `abs`, `sum`, `min`, `max`, `keys`, `values`, `join`, `split`, and `assert`; numeric aggregators accept strict finite-number arrays. `qcoffee -` reads source from standard input, which is convenient in pipelines; `qcoffee --dump-bytecode -` disassembles that same input instead of executing it. `qcoffee --stats` writes instruction and remaining-fuel counters to stderr while preserving program stdout; qcoffee accepts one source input and rejects conflicting execution modes. diff --git a/docs/manual.latin.html b/docs/manual.latin.html index 73cf013..47c45cf 100644 --- a/docs/manual.latin.html +++ b/docs/manual.latin.html @@ -36,7 +36,7 @@

yes/on sunt true, no/off false; is/isnt aequalitatem strictam servant.

! est negatio Bool stricta sicut not; != inaequalitas stricta manet.

Comparationes conectae medium semel servant atque priore falso breviant.

-

Bibliotheca communis functiones ordinarias habet: print, len, type, range, str, keys, values, join, split, assert.

+

Bibliotheca communis functiones ordinarias habet: print, len, type, range, str, abs, sum, min, max, keys, values, join, split, assert; aggregationes numericae solum series numerorum finitorum accipiunt.

Functiones ambitum lexicalem capiunt; y = 2 omissus vel nil intra functionem adhibetur; rest ultimus scribitur tail....

Nomina nuda parentheses omittere possunt: sinister, dexter -> sinister + dexter; default, rest, pattern eas servant.

return expressio functionem praesentem finit; nudum return nil dat, iterata purgat, et finally circumstantia peragit.

diff --git a/docs/manual.latin.md b/docs/manual.latin.md index 8493d84..8ca21df 100644 --- a/docs/manual.latin.md +++ b/docs/manual.latin.md @@ -14,7 +14,7 @@ Voces CoffeeScript sine mutatione generum valent: `yes`/`on` sunt `true`, `no`/` Comparationes strictae vel numericae conecti possunt: `1 < middle() < 3` medium semel aestimat et, priore falso, posteriora non aestimat. -Utere `qcoffee -e "print(range(1, 4))"`; `qcoffee --check FILE` fontem compilat atque verificat sine cursu; `--fuel N` numerum instructionum finit. Bibliotheca parva habet `print`, `len`, `type`, `range(a, b)`, `str`, `keys`, `values`, `join`, `split`, et `assert`; finis in `range` exclusus est. +Utere `qcoffee -e "print(range(1, 4))"`; `qcoffee --check FILE` fontem compilat atque verificat sine cursu; `--fuel N` numerum instructionum finit. Bibliotheca parva habet `print`, `len`, `type`, `range(a, b)`, `str`, `abs`, `sum`, `min`, `max`, `keys`, `values`, `join`, `split`, et `assert`; aggregationes numericae solum series numerorum finitorum accipiunt; finis in `range` exclusus est. `qcoffee -` fontem ex initio normali legit; `qcoffee --dump-bytecode -` illum fontem sine cursu explicat. `qcoffee --stats` numeros instructionum et alimenti reliqui ad errorem ordinarium scribit, dum exitus programmatis intactus manet; unus tantum fons admittitur et modi contrarii errorem usus reddunt. diff --git a/docs/manual.zh-CN.html b/docs/manual.zh-CN.html index 18b244c..3ad467a 100644 --- a/docs/manual.zh-CN.html +++ b/docs/manual.zh-CN.html @@ -33,7 +33,7 @@

yes/on 与 no/off 是布尔别名;is/isnt 保持严格相等。

! 是严格 Bool 的 not 别名;!= 仍为严格不等。

严格或数值比较可成链,保留中间值且前段失败会短路。

-

标准库皆为普通函数:print、len、type、range、str、keys、values、join、split 与 assert。

+

标准库皆为普通函数:print、len、type、range、str、abs、sum、min、max、keys、values、join、split 与 assert;数值聚合只收严格有限数数组。

映射字面量可从左至右展开:{...defaults, theme: 'dark'};后写键覆盖先写键。

映射解构末尾可用 ...metadata 捕获未列键,所得映射不可变。

数组与 Unicode 字符串支持负索引,-1 取末项。

diff --git a/docs/manual.zh-CN.md b/docs/manual.zh-CN.md index 2fd3e8e..9eacbfa 100644 --- a/docs/manual.zh-CN.md +++ b/docs/manual.zh-CN.md @@ -32,7 +32,7 @@ qcoffee --dump-bytecode program.qc `--` 之后的参数以普通字符串数组 `argv` 提供:`qcoffee program.qc -- first second` 中 `len(argv)` 为 `2`。引擎不会暴露宿主进程或环境对象。 -`--fuel` 是每次执行的指令上限,耗尽会安全失败。标准库包括 `print`、`len`、`type`、`range`、`str`、`keys`、`values`、`join`、`split` 与 `assert`;`range(a, b)` 生成 `[a, b)`。 +`--fuel` 是每次执行的指令上限,耗尽会安全失败。标准库包括 `print`、`len`、`type`、`range`、`str`、`abs`、`sum`、`min`、`max`、`keys`、`values`、`join`、`split` 与 `assert`;数值聚合只接受严格有限数数组,`range(a, b)` 生成 `[a, b)`。 ## 语法示例 diff --git a/docs/syntax.en.md b/docs/syntax.en.md index 6b7bdc5..175de78 100644 --- a/docs/syntax.en.md +++ b/docs/syntax.en.md @@ -4,7 +4,7 @@ Embedding callers may use `Program::fingerprint()` as a deterministic bytecode c Cargo package metadata links embedding users to the repository, README, license, and docs.rs API. -The bundled `qtest --json` runner emits one stable JSON result per file for CI and host integration; `qtest --tap` emits deterministic TAP 13 records. `qcoffee --fingerprint FILE` prints a stable 16-digit hexadecimal key for verified bytecode without executing it; the key uses canonical encoding rather than Rust debug text. `qbench --json` emits guarded compile/verify/execute timing records, and `qdocco --markdown` renders literate notes, fenced source, and final values for review. Embedders can adjust a reused context with `Context::set_fuel` and inspect `Context::fuel`; `cargo run --example embed` is a compiled host integration example. Execution statistics remain on stderr with `--stats`. +The bundled `qtest --json` runner emits one stable JSON result per file for CI and host integration; `qtest --tap` emits deterministic TAP 13 records. `qcoffee --fingerprint FILE` prints a stable 16-digit hexadecimal key for verified bytecode without executing it; the key uses canonical encoding rather than Rust debug text. `qbench --json` emits guarded compile/verify/execute timing records; `qbench --list` enumerates workloads and `qbench --only NAME` runs one selected workload, while the default remains the complete suite. `qdocco --markdown` renders literate notes, fenced source, and final values for review. Embedders can adjust a reused context with `Context::set_fuel` and inspect `Context::fuel`; `cargo run --example embed` is a compiled host integration example. Execution statistics remain on stderr with `--stats`. Integer ranges are ascending or descending: `[2..4]` is `[2, 3, 4]`, `[4..2]` is `[4, 3, 2]`, and exclusive forms omit the end (`[4...2]` is `[4, 3]`). Bounds must be finite integers and oversized ranges are rejected. @@ -18,6 +18,8 @@ Destructuring array and map members may use dynamic defaults (`[first = 1, secon This is the English index of RFC 0001. An omitted CoffeeScript 2016 feature is deliberately unsupported, not silently compatible. +The standard library is ordinary functions: `print`, `len`, `type`, `range`, `str`, `abs`, `sum`, `min`, `max`, `keys`, `values`, `join`, `split`, and `assert`. Numeric aggregators accept one array of finite numbers; `sum([])` is `0`, while `min([])` and `max([])` are errors. + Ordinary single- and double-quoted strings decode `\\0`, `\\b`, `\\f`, `\\n`, `\\r`, `\\t`, `\\v`, quote/backslash escapes, two-digit `\\xNN`, four-digit `\\uNNNN`, and one-to-six-digit `\\u{...}` Unicode escapes. They may span physical lines; a normal newline becomes one space and indentation inside the string is ignored. A single unescaped backslash at the line end removes both the backslash and the newline. Double-quoted multiline strings retain `#{expression}` interpolation, while single-quoted strings remain literal. Triple-quoted heredocs preserve newlines: `"""…"""` interpolates and `'''…'''` is literal; an unclosed delimiter or invalid escape is a lexical error. `for` uses strict recursive patterns for its bindings: `for [left, right] in pairs then left + right` and `for own _, value of record then value` are supported. A pattern mismatch is a runtime error and never partially updates that iteration's bindings. diff --git a/docs/syntax.zh-CN.md b/docs/syntax.zh-CN.md index 69548bc..a8803e9 100644 --- a/docs/syntax.zh-CN.md +++ b/docs/syntax.zh-CN.md @@ -2,10 +2,12 @@ 嵌入宿主可用 `Program::fingerprint()` 作为确定性字节码缓存键;该指纹不改变验证与执行语义。 -内建 `qtest --json` 每个文件输出一行稳定 JSON,供 CI 与宿主系统使用;`qtest --tap` 输出确定性的 TAP 13 记录;`qcoffee --fingerprint FILE` 在不执行脚本时输出已验证字节码的稳定 16 位十六进制键,指纹使用规范化编码而非 Rust 调试文本;`qbench --json` 输出带语义护栏的编译、验证、执行计时记录;`qdocco --markdown` 生成说明、围栏源码和最终值供审阅;嵌入方可用 `Context::set_fuel` 调整复用上下文的预算并用 `Context::fuel` 读取,`cargo run --example embed` 提供可编译宿主示例;`--stats` 的执行统计仍写入标准错误。 +内建 `qtest --json` 每个文件输出一行稳定 JSON,供 CI 与宿主系统使用;`qtest --tap` 输出确定性的 TAP 13 记录;`qcoffee --fingerprint FILE` 在不执行脚本时输出已验证字节码的稳定 16 位十六进制键,指纹使用规范化编码而非 Rust 调试文本;`qbench --json` 输出带语义护栏的编译、验证、执行计时记录,`qbench --list` 枚举负载而 `qbench --only NAME` 可只运行一个负载;`qdocco --markdown` 生成说明、围栏源码和最终值供审阅;嵌入方可用 `Context::set_fuel` 调整复用上下文的预算并用 `Context::fuel` 读取,`cargo run --example embed` 提供可编译宿主示例;`--stats` 的执行统计仍写入标准错误。 这是 RFC 0001 的中文索引;未列出的 CoffeeScript 2016 特性不是“隐式兼容”,而是明确不支持。Cargo 包元数据提供仓库、README、许可证和 docs.rs API 链接。 +标准库是普通函数:`print`、`len`、`type`、`range`、`str`、`abs`、`sum`、`min`、`max`、`keys`、`values`、`join`、`split` 与 `assert`。数值聚合只收一个有限数数组;`sum([])` 为 `0`,`min([])` 与 `max([])` 报错。 + | 类别 | 支持 | 不支持(本版) | |---|---|---| | 字面量 | 十进制、十六进制 `0xff`、二进制 `0b1010`、八进制 `0o755` 与科学计数法数字、字符串、双引号 `#{expr}` 插值、保留换行的 `"""…"""` 插值 heredoc 与 `'''…'''` 字面 heredoc、`true`/`yes`/`on`、`false`/`no`/`off`、`nil`、数组与 `[head, items...]` 展开、整数区间 `[1..3]`(含上界)/`[1...3]`(不含上界)、映射、`{name}` 简写与映射展开 `{...base, key: value}` | 正则、JS 插值、`undefined` | diff --git a/manuals/manual.classical-zh.qc b/manuals/manual.classical-zh.qc index 0c2be39..81ca9f2 100644 --- a/manuals/manual.classical-zh.qc +++ b/manuals/manual.classical-zh.qc @@ -35,7 +35,7 @@ ## Cargo 包志指仓、docs.rs API、README 与许可证,使嵌者易寻其用。 ## Context::last_execution() 示所试指令与余燃料,而不露 VM 之帧。 ## -- 后之参,以常字符串数组 argv 见于文中。 -## 其内府皆常函,如 print、len、type、range、str、keys、values、join、split、assert。 +## 其内府皆常函,如 print、len、type、range、str、abs、sum、min、max、keys、values、join、split、assert;数聚函唯受有限数之列。 ## 函式取词法之境;末常参可书 y = 2,参缺或传 nil 则于函中取其值;末有余参,则书 tail...。 ## 常名之参可省其括:left, right -> left + right;常值、余参与解构仍须括之。 ## return expression 惟函中可用,反其值而终此函;徒 return 得 nil,且清环行 finally。 diff --git a/manuals/manual.devanagari-sa.qc b/manuals/manual.devanagari-sa.qc index 3f3de48..c8a1fdd 100644 --- a/manuals/manual.devanagari-sa.qc +++ b/manuals/manual.devanagari-sa.qc @@ -36,7 +36,7 @@ ## yes/on true, no/off false; is/isnt strict-साम्यम् स्तः। ## ! strict-Bool not-पर्यायः अस्ति; != strict-असाम्यमेव तिष्ठति। ## chained-comparison मध्ये मध्य-मूल्य एकवारं, पूर्व-false चेत् short-circuit भवति। -## सामान्य-library साधारण-function रूपेण print, len, type, range, str, keys, values, join, split, assert ददाति। +## सामान्य-library साधारण-function रूपेण print, len, type, range, str, abs, sum, min, max, keys, values, join, split, assert ददाति; संख्या-संग्रहाः केवलं finite-संख्याः गृह्णन्ति। ## कार्यं lexical-environment गृह्णाति; y = 2 omitting अथवा nil दत्ते कार्यस्य अन्तरे default भवति; अन्तिमः rest-parameter tail... इति लिख्यते। ## bare-name lambda left, right -> left + right भवति; default, rest, pattern तु parentheses गृह्णन्ति। ## return expression केवलं वर्तमान-कार्यं समाप्तं करोति; केवलः return nil फलति, loop शुद्धीकरोति, finally च चलयति। diff --git a/manuals/manual.en.qc b/manuals/manual.en.qc index 96b55ea..2168acd 100644 --- a/manuals/manual.en.qc +++ b/manuals/manual.en.qc @@ -32,7 +32,7 @@ ## yes/on and no/off are Boolean aliases; is/isnt preserve strict equality. ## ! is a strict Bool alias for not; != remains strict inequality. ## Chained strict or numeric comparisons keep the middle value once and short-circuit. -## The standard library is ordinary functions: print, len, type, range, str, keys, values, join, split, and assert. +## The standard library is ordinary functions: print, len, type, range, str, abs, sum, min, max, keys, values, join, split, and assert. Numeric aggregators accept strict finite-number arrays. ## switch/when selects one strict-equality branch without fallthrough. ## try/catch/finally handles QuickCoffee runtime errors without JavaScript Error objects. ## Integer ranges use [1..3] for an inclusive end and [1...3] for an exclusive end. diff --git a/manuals/manual.latin.qc b/manuals/manual.latin.qc index e0dbdd0..2db586c 100644 --- a/manuals/manual.latin.qc +++ b/manuals/manual.latin.qc @@ -36,7 +36,7 @@ ## yes/on sunt true, no/off false; is/isnt aequalitatem strictam servant. ## ! est negatio Bool stricta sicut not; != inaequalitas stricta manet. ## Comparationes conectae medium semel servant atque priore falso breviant. -## Bibliotheca communis functiones ordinarias habet: print, len, type, range, str, keys, values, join, split, assert. +## Bibliotheca communis functiones ordinarias habet: print, len, type, range, str, abs, sum, min, max, keys, values, join, split, assert; aggregationes numericae solum series numerorum finitorum accipiunt. ## Functiones ambitum lexicalem capiunt; y = 2 omissus vel nil intra functionem adhibetur; rest ultimus scribitur tail.... ## Nomina nuda parentheses omittere possunt: sinister, dexter -> sinister + dexter; default, rest, pattern eas servant. ## return expressio functionem praesentem finit; nudum return nil dat, iterata purgat, et finally circumstantia peragit. diff --git a/manuals/manual.zh-CN.qc b/manuals/manual.zh-CN.qc index 1f944d1..fa535e5 100644 --- a/manuals/manual.zh-CN.qc +++ b/manuals/manual.zh-CN.qc @@ -33,7 +33,7 @@ ## yes/on 与 no/off 是布尔别名;is/isnt 保持严格相等。 ## ! 是严格 Bool 的 not 别名;!= 仍为严格不等。 ## 严格或数值比较可成链,保留中间值且前段失败会短路。 -## 标准库皆为普通函数:print、len、type、range、str、keys、values、join、split 与 assert。 +## 标准库皆为普通函数:print、len、type、range、str、abs、sum、min、max、keys、values、join、split 与 assert;数值聚合只收严格有限数数组。 ## 映射字面量可从左至右展开:{...defaults, theme: 'dark'};后写键覆盖先写键。 ## 映射解构末尾可用 ...metadata 捕获未列键,所得映射不可变。 ## 数组与 Unicode 字符串支持负索引,-1 取末项。 diff --git a/src/bin/qbench.rs b/src/bin/qbench.rs index 588e9cf..eeae37c 100644 --- a/src/bin/qbench.rs +++ b/src/bin/qbench.rs @@ -185,7 +185,9 @@ const WORKLOADS: &[Workload] = &[ ]; fn usage() { - eprintln!("Usage: qbench [--iterations N] [--repeat N] [--json]\n qbench --version"); + eprintln!( + "Usage: qbench [--iterations N] [--repeat N] [--only NAME] [--json]\n qbench --list\n qbench --version" + ); } fn json_escape(value: &str) -> String { @@ -201,6 +203,8 @@ fn main() -> ExitCode { let mut iterations = 100; let mut repeat = 1; let mut json = false; + let mut only = None; + let mut list = false; let mut args = env::args().skip(1); while let Some(arg) = args.next() { match arg.as_str() { @@ -212,7 +216,15 @@ fn main() -> ExitCode { usage(); return ExitCode::SUCCESS; } + "--list" => list = true, "--json" => json = true, + "--only" => match args.next() { + Some(value) if !value.is_empty() => only = Some(value), + _ => { + eprintln!("--only requires a workload name"); + return ExitCode::from(2); + } + }, "--iterations" => match args.next().and_then(|value| value.parse().ok()) { Some(value) if value > 0 => iterations = value, _ => { @@ -233,8 +245,28 @@ fn main() -> ExitCode { } } } + if list { + if only.is_some() { + eprintln!("--list cannot be combined with --only"); + return ExitCode::from(2); + } + for workload in WORKLOADS { + println!("{}", workload.name); + } + return ExitCode::SUCCESS; + } + let workloads: Vec<&Workload> = match only.as_deref() { + Some(name) => match WORKLOADS.iter().find(|workload| workload.name == name) { + Some(workload) => vec![workload], + None => { + eprintln!("unknown workload '{name}'; use --list to see available workloads"); + return ExitCode::from(2); + } + }, + None => WORKLOADS.iter().collect(), + }; let engine = Engine::new(); - for workload in WORKLOADS { + for workload in workloads { let mut compile_samples = Vec::with_capacity(repeat); let mut verify_samples = Vec::with_capacity(repeat); let mut execute_samples = Vec::with_capacity(repeat); diff --git a/src/bin/qdocco.rs b/src/bin/qdocco.rs index aa3b2dd..5246566 100644 --- a/src/bin/qdocco.rs +++ b/src/bin/qdocco.rs @@ -2,7 +2,9 @@ use quickcoffee::{Context, Engine, Value}; use std::{ - env, fs, + env, + fmt::Write as _, + fs, io::{self, Write}, path::{Path, PathBuf}, process::ExitCode, @@ -83,10 +85,10 @@ fn split_source(source: &str) -> (String, String) { } fn render(source: &str, result: &str) -> String { let (prose_text, code) = split_source(source); - let prose = prose_text - .lines() - .map(|line| format!("

{}

\n", escape(line))) - .collect::(); + let prose = prose_text.lines().fold(String::new(), |mut prose, line| { + writeln!(prose, "

{}

", escape(line)).expect("writing to a string cannot fail"); + prose + }); format!( "QuickCoffee document

Notes

{prose}

Code

{}
", escape(&code), diff --git a/src/bytecode.rs b/src/bytecode.rs index f63d281..0395ffe 100644 --- a/src/bytecode.rs +++ b/src/bytecode.rs @@ -4,6 +4,7 @@ use crate::{ }; use std::{ collections::{BTreeMap, VecDeque}, + fmt::Write, rc::Rc, }; @@ -122,11 +123,11 @@ pub enum Instruction { impl Chunk { /// Returns a human-readable instruction listing with stable program counters. pub fn disassemble(&self) -> String { - self.code - .iter() - .enumerate() - .map(|(i, op)| format!("{i:04} {op:?}\n")) - .collect() + let mut output = String::new(); + for (i, op) in self.code.iter().enumerate() { + writeln!(&mut output, "{i:04} {op:?}").expect("writing to a String cannot fail"); + } + output } /// Returns a deterministic content fingerprint for cache keys and diagnostics. /// @@ -973,7 +974,7 @@ struct ReturnCleanup { finalizer: Option, } impl Compiler { - fn compile_pattern(&mut self, pattern: &AstPattern) -> Result { + fn compile_pattern(pattern: &AstPattern) -> Result { Ok(match pattern { AstPattern::Ignore => Pattern::Ignore, AstPattern::Bind(name) => Pattern::Bind(name.clone()), @@ -981,19 +982,19 @@ impl Compiler { AstPattern::Array(items) => Pattern::Array( items .iter() - .map(|p| self.compile_pattern(p)) + .map(Self::compile_pattern) .collect::>()?, ), AstPattern::Map(fields) => Pattern::Map( fields .iter() - .map(|(key, p)| Ok((key.clone(), self.compile_pattern(p)?))) + .map(|(key, p)| Ok((key.clone(), Self::compile_pattern(p)?))) .collect::>()?, ), AstPattern::MapRest(fields, rest) => Pattern::MapRest { fields: fields .iter() - .map(|(key, p)| Ok((key.clone(), self.compile_pattern(p)?))) + .map(|(key, p)| Ok((key.clone(), Self::compile_pattern(p)?))) .collect::>()?, rest: rest.clone(), }, @@ -1002,7 +1003,7 @@ impl Compiler { compiler.expr(expr)?; compiler.emit(Instruction::Return); Pattern::Default { - pattern: Box::new(self.compile_pattern(inner)?), + pattern: Box::new(Self::compile_pattern(inner)?), default: Rc::new(compiler.chunk), } } @@ -1055,7 +1056,7 @@ impl Compiler { } Stmt::Destructure(pattern, e) => { self.expr(e)?; - let compiled = self.compile_pattern(pattern)?; + let compiled = Self::compile_pattern(pattern)?; self.emit(Instruction::Destructure(compiled)); } Stmt::Expr(e) => self.expr(e)?, @@ -1085,7 +1086,7 @@ impl Compiler { let start = self.chunk.code.len(); let compiled_patterns = patterns .iter() - .map(|p| self.compile_pattern(p)) + .map(Self::compile_pattern) .collect::>()?; let exit = self.emit(Instruction::IterNext { patterns: compiled_patterns, @@ -1175,7 +1176,7 @@ impl Compiler { } Expr::Destructure(pattern, value) => { self.expr(value)?; - let compiled = self.compile_pattern(pattern)?; + let compiled = Self::compile_pattern(pattern)?; self.emit(Instruction::Destructure(compiled)); } Expr::Array(items) => { @@ -1389,7 +1390,7 @@ impl Compiler { let start = self.chunk.code.len(); let compiled_patterns = patterns .iter() - .map(|p| self.compile_pattern(p)) + .map(Self::compile_pattern) .collect::>()?; let exit = self.emit(Instruction::IterNext { patterns: compiled_patterns, @@ -1678,7 +1679,7 @@ impl Compiler { let idx = self.chunk.constants.len(); let compiled_params = params .iter() - .map(|param| self.compile_pattern(¶m.pattern)) + .map(|param| Self::compile_pattern(¶m.pattern)) .collect::>()?; self.chunk.constants.push(Constant::Function { params: compiled_params, diff --git a/src/lexer.rs b/src/lexer.rs index 68150ff..1a601b7 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -161,18 +161,22 @@ pub(crate) fn lex_spanned(source: &str) -> Result<(Vec, Vec), Erro } if groups.is_empty() && !continued { let current = *indents.last().expect("indent stack"); - if prefix > current { - indents.push(prefix); - out.push(Token::Indent); - } else if prefix < current { - while prefix < *indents.last().expect("indent stack") { - indents.pop(); - out.push(Token::Dedent); + match prefix.cmp(¤t) { + std::cmp::Ordering::Greater => { + indents.push(prefix); + out.push(Token::Indent); } - if prefix != *indents.last().expect("indent stack") { - return Err(Error::parse("inconsistent indentation").at_line(line_number)); + std::cmp::Ordering::Less => { + while prefix < *indents.last().expect("indent stack") { + indents.pop(); + out.push(Token::Dedent); + } + if prefix != *indents.last().expect("indent stack") { + return Err(Error::parse("inconsistent indentation").at_line(line_number)); + } + out.push(Token::Semi); } - out.push(Token::Semi); + std::cmp::Ordering::Equal => {} } } lex_line(content, line_number, &mut groups, &mut out)?; @@ -257,7 +261,7 @@ fn trim_single_trailing_backslash(line: &mut String) -> bool { } backslashes += 1; } - if backslashes.is_multiple_of(2) { + if backslashes % 2 == 0 { line.truncate(trimmed_len); false } else { diff --git a/src/vm.rs b/src/vm.rs index 52fa71a..8a21abe 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -532,6 +532,34 @@ impl Context { } Ok(Value::String(Rc::from(xs[0].to_string()))) }); + self.add_native("abs", |xs| { + if xs.len() != 1 { + return Err(Error::runtime("abs expects one number")); + } + let value = number(xs[0].clone())?; + if !value.is_finite() { + return Err(Error::runtime("abs expects a finite number")); + } + Ok(Value::Number(value.abs())) + }); + self.add_native("sum", |xs| { + let values = numeric_array(xs, "sum")?; + Ok(Value::Number(values.into_iter().sum())) + }); + self.add_native("min", |xs| { + let values = numeric_array(xs, "min")?; + let Some(value) = values.into_iter().reduce(f64::min) else { + return Err(Error::runtime("min expects a non-empty array")); + }; + Ok(Value::Number(value)) + }); + self.add_native("max", |xs| { + let values = numeric_array(xs, "max")?; + let Some(value) = values.into_iter().reduce(f64::max) else { + return Err(Error::runtime("max expects a non-empty array")); + }; + Ok(Value::Number(value)) + }); self.add_native("keys", |xs| { if xs.len() != 1 { return Err(Error::runtime("keys expects one argument")); @@ -1149,13 +1177,14 @@ impl Vm { return Ok(value); } } - Ok(Step::Call { callee, args }) => { - if let Err(error) = call(self, &mut frames, callee, args) - && !handle_error(&mut frames, &error) - { - return Err(error); + Ok(Step::Call { callee, args }) => match call(self, &mut frames, callee, args) { + Ok(()) => {} + Err(error) => { + if !handle_error(&mut frames, &error) { + return Err(error); + } } - } + }, Err(error) => { if !handle_error(&mut frames, &error) { return Err(error); @@ -1272,6 +1301,27 @@ fn numbers(xs: &[Value]) -> Result<(f64, f64), Error> { } Ok((number(xs[0].clone())?, number(xs[1].clone())?)) } +fn numeric_array(xs: &[Value], name: &str) -> Result, Error> { + if xs.len() != 1 { + return Err(Error::runtime(format!("{name} expects one array"))); + } + let Value::Array(values) = &xs[0] else { + return Err(Error::runtime(format!("{name} expects an array"))); + }; + values + .iter() + .map(|value| { + let value = number(value.clone())?; + if value.is_finite() { + Ok(value) + } else { + Err(Error::runtime(format!( + "{name} expects finite numeric elements" + ))) + } + }) + .collect() +} fn numeric_range(start: f64, end: f64, inclusive: bool) -> Result { if !start.is_finite() || !end.is_finite() diff --git a/tests/cli_tools.rs b/tests/cli_tools.rs index dcdd0e6..0d8b5df 100644 --- a/tests/cli_tools.rs +++ b/tests/cli_tools.rs @@ -1,10 +1,28 @@ use std::{ fs, io::Write, + path::PathBuf, process::{Command, Stdio}, }; fn bin(name: &str) -> String { - std::env::var(format!("CARGO_BIN_EXE_{name}")).expect("Cargo supplies bin path") + if let Ok(path) = std::env::var(format!("CARGO_BIN_EXE_{name}")) { + return path; + } + let test_binary = std::env::current_exe().expect("test binary path is available"); + let target_debug = test_binary + .parent() + .and_then(|deps| deps.parent()) + .expect("test binary lives below target/debug/deps"); + let mut candidate = PathBuf::from(target_debug); + candidate.push(name); + if cfg!(windows) { + candidate.set_extension("exe"); + } + assert!( + candidate.is_file(), + "Cargo binary path is unavailable: {candidate:?}" + ); + candidate.to_string_lossy().into_owned() } #[test] fn qdocco_renders_escaped_source_and_checks() { @@ -155,17 +173,15 @@ fn qtest_reports_success_and_failure() { assert!(directory.status.success()); let directory_stdout = String::from_utf8_lossy(&directory.stdout); for fixture in [ - "arithmetic.qc", - "collections.qc", - "comprehension.qc", - "control-flow.qc", - "function.qc", - ] - .map(|name| std::path::Path::new("tests/scripts").join(name)) - { - let fixture = fixture.display().to_string(); + "tests/scripts/arithmetic.qc", + "tests/scripts/collections.qc", + "tests/scripts/comprehension.qc", + "tests/scripts/control-flow.qc", + "tests/scripts/function.qc", + "tests/scripts/stdlib.qc", + ] { assert!( - directory_stdout.contains(&fixture), + directory_stdout.contains(fixture), "qtest skipped {fixture}" ); } @@ -592,6 +608,34 @@ fn qbench_json_is_guarded_and_machine_readable() { .output() .unwrap(); assert_eq!(invalid_repeat.status.code(), Some(2)); + let listed = Command::new(bin("qbench")).arg("--list").output().unwrap(); + assert!(listed.status.success()); + assert_eq!( + String::from_utf8_lossy(&listed.stdout) + .lines() + .collect::>(), + expected_names + ); + assert!(listed.stderr.is_empty()); + let selected = Command::new(bin("qbench")) + .args(["--only", "map-spread", "--json", "--iterations", "1"]) + .output() + .unwrap(); + assert!(selected.status.success()); + let selected_stdout = String::from_utf8_lossy(&selected.stdout); + assert_eq!(selected_stdout.lines().count(), 1); + assert!(selected_stdout.contains("\"name\":\"map-spread\"")); + let unknown = Command::new(bin("qbench")) + .args(["--only", "missing-workload"]) + .output() + .unwrap(); + assert_eq!(unknown.status.code(), Some(2)); + assert!(String::from_utf8_lossy(&unknown.stderr).contains("use --list")); + let list_conflict = Command::new(bin("qbench")) + .args(["--list", "--only", "map-spread"]) + .output() + .unwrap(); + assert_eq!(list_conflict.status.code(), Some(2)); } #[test] fn qcoffee_interactive_session_preserves_context_and_recovers_from_errors() { diff --git a/tests/manuals.rs b/tests/manuals.rs index e430171..fba6459 100644 --- a/tests/manuals.rs +++ b/tests/manuals.rs @@ -1,5 +1,5 @@ use quickcoffee::{Context, Value}; -use std::process::Command; +use std::{path::PathBuf, process::Command}; const MANUALS: &[&str] = &[ include_str!("../manuals/manual.zh-CN.qc"), @@ -18,7 +18,7 @@ fn every_literate_manual_is_an_executable_passing_example() { #[test] fn qdocco_checks_every_manual_source() { - let qdocco = std::env::var("CARGO_BIN_EXE_qdocco").expect("Cargo supplies qdocco path"); + let qdocco = binary("qdocco"); for locale in ["zh-CN", "classical-zh", "en", "latin", "devanagari-sa"] { assert!( Command::new(&qdocco) @@ -29,3 +29,24 @@ fn qdocco_checks_every_manual_source() { ); } } + +fn binary(name: &str) -> String { + if let Ok(path) = std::env::var(format!("CARGO_BIN_EXE_{name}")) { + return path; + } + let test_binary = std::env::current_exe().expect("test binary path is available"); + let target_debug = test_binary + .parent() + .and_then(|deps| deps.parent()) + .expect("test binary lives below target/debug/deps"); + let mut candidate = PathBuf::from(target_debug); + candidate.push(name); + if cfg!(windows) { + candidate.set_extension("exe"); + } + assert!( + candidate.is_file(), + "Cargo binary path is unavailable: {candidate:?}" + ); + candidate.to_string_lossy().into_owned() +} diff --git a/tests/rfc_core.rs b/tests/rfc_core.rs index 734dec6..a3cccb4 100644 --- a/tests/rfc_core.rs +++ b/tests/rfc_core.rs @@ -1341,6 +1341,32 @@ fn redesigned_standard_library_is_function_based_not_prototype_based() { assert!(Context::new().eval("assert(false, 'expected')").is_err()); } #[test] +fn numeric_standard_library_is_strict_and_total() { + assert_eq!(eval("abs(-3)").as_number(), Some(3.)); + assert_eq!(eval("sum([])").as_number(), Some(0.)); + assert_eq!( + eval("min([3, 1, 2]) + max([3, 1, 2])").as_number(), + Some(4.) + ); + for source in [ + "abs()", + "abs('3')", + "sum(1)", + "sum([1, '2'])", + "min([])", + "max([true])", + ] { + assert!( + Context::new().eval(source).is_err(), + "expected {source} to fail" + ); + } + let mut host = Context::new(); + host.set_global("nan", Value::Number(f64::NAN)); + assert!(host.eval("abs(nan)").is_err()); + assert!(host.eval("sum([nan])").is_err()); +} +#[test] fn array_destructuring_is_strict_and_has_an_explicit_ignore_name() { assert_eq!( eval("left, right = [20, 22]\nleft + right").as_number(), diff --git a/tests/rfc_index.rs b/tests/rfc_index.rs index 3898fd4..f790fb0 100644 --- a/tests/rfc_index.rs +++ b/tests/rfc_index.rs @@ -49,3 +49,17 @@ fn rfc_numbers_and_index_references_are_consistent() { "scope RFC must mention the latest RFC" ); } + +#[test] +fn package_manifest_declares_the_documented_msrv() { + let manifest = fs::read_to_string("Cargo.toml").expect("Cargo manifest exists"); + let rust_version = manifest.lines().find_map(|line| { + let line = line.split('#').next()?.trim(); + let (key, value) = line.split_once('=')?; + (key.trim() == "rust-version").then(|| value.trim().trim_matches('"')) + }); + assert!( + rust_version == Some("1.85"), + "Cargo.toml must declare the RFC 0110 MSRV" + ); +} diff --git a/tests/scripts/stdlib.qc b/tests/scripts/stdlib.qc new file mode 100644 index 0000000..418fc25 --- /dev/null +++ b/tests/scripts/stdlib.qc @@ -0,0 +1,4 @@ +# test: the prototype-free standard library stays ordinary and composable +record = {a: 1, b: 2} +numbers = range(1, 4) +len(numbers) == 3 and type(record) == 'map' and str(numbers[2]) == '3' and join(keys(record), ',') == 'a,b' and join(values(record), ',') == '1,2' and split('a,b', ',')[1] == 'b' and abs(-3) == 3 and sum(numbers) == 6 and min(numbers) == 1 and max(numbers) == 3 and assert(true) == nil