From 7ccd8c786904c0a8906c66be421aa1cc62a0013c Mon Sep 17 00:00:00 2001 From: tiye Date: Sat, 22 Aug 2026 23:36:32 +0800 Subject: [PATCH 1/5] fix: preserve qdocco block comments as code --- README.md | 2 +- RFCs/0000-project-scope.md | 2 +- RFCs/0104-qdocco-block-comments.md | 8 ++++++++ src/bin/qdocco.rs | 9 +++++++-- tests/cli_tools.rs | 31 ++++++++++++++++++++++++++++++ 5 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 RFCs/0104-qdocco-block-comments.md diff --git a/README.md b/README.md index fad4196..702a2ca 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/0103-qbench-schema-version.md](RFCs/0103-qbench-schema-version.md)。 +当前实现遵循 [RFCs/0000-project-scope.md](RFCs/0000-project-scope.md) 至 [RFCs/0104-qdocco-block-comments.md](RFCs/0104-qdocco-block-comments.md)。 ```coffee square = (x) -> x * x diff --git a/RFCs/0000-project-scope.md b/RFCs/0000-project-scope.md index 66e61d5..ecb68c0 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 0103;其中 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 延伸至 RFC 0104;其中 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 块注释代码保留,均不改变脚本语言值模型的原型无关约束。 diff --git a/RFCs/0104-qdocco-block-comments.md b/RFCs/0104-qdocco-block-comments.md new file mode 100644 index 0000000..88fab7d --- /dev/null +++ b/RFCs/0104-qdocco-block-comments.md @@ -0,0 +1,8 @@ +# RFC 0104:qdocco 保留块注释代码 + +- 状态:已采纳 +- 依赖:RFC 0032、RFC 0083 + +qdocco 文学 prose 行必须以恰好两个 `#` 开始(可有前导空格);以三个或更多 `#` 开始的行属于 QuickCoffee 块注释或其内容,必须保留在生成文档的代码区。这样 `### ... ###` 不会被误渲染成说明文字,也不会从可复制代码中消失。 + +HTML 与 Markdown 输出采用同一分类规则。块注释仍由 QuickCoffee 编译器执行并从运行语义中移除,RFC 0104 只规定文学文档的代码/prose 分区。 diff --git a/src/bin/qdocco.rs b/src/bin/qdocco.rs index fd071c8..9edfd58 100644 --- a/src/bin/qdocco.rs +++ b/src/bin/qdocco.rs @@ -50,11 +50,16 @@ fn escape(input: &str) -> String { .replace('>', ">") .replace('"', """) } +fn prose_text(line: &str) -> Option<&str> { + let trimmed = line.trim_start(); + let text = trimmed.strip_prefix("##")?; + (!text.starts_with('#')).then_some(text) +} fn render(source: &str, result: &str) -> String { let mut prose = String::new(); let mut code = String::new(); for line in source.lines() { - if let Some(text) = line.trim_start().strip_prefix("##") { + if let Some(text) = prose_text(line) { prose.push_str(&format!("

{}

\n", escape(text.trim()))) } else { code.push_str(line); @@ -71,7 +76,7 @@ fn render_markdown(source: &str, result: &str) -> String { let mut prose = String::new(); let mut code = String::new(); for line in source.lines() { - if let Some(text) = line.trim_start().strip_prefix("##") { + if let Some(text) = prose_text(line) { let text = text.trim(); if !text.is_empty() { prose.push_str(text); diff --git a/tests/cli_tools.rs b/tests/cli_tools.rs index 4981b2d..092bad9 100644 --- a/tests/cli_tools.rs +++ b/tests/cli_tools.rs @@ -72,6 +72,37 @@ fn qdocco_renders_escaped_source_and_checks() { ); let fenced_document = fs::read_to_string(&fenced_output).unwrap(); assert!(fenced_document.contains("`````quickcoffee\n# ````\ntrue\n`````")); + let block_input = temp.join("block-comment.qc"); + let block_output = temp.join("block-comment.html"); + fs::write(&block_input, "### hidden code ###\ntrue\n").unwrap(); + assert!( + Command::new(bin("qdocco")) + .args([ + block_input.to_str().unwrap(), + "-o", + block_output.to_str().unwrap() + ]) + .status() + .unwrap() + .success() + ); + let block_document = fs::read_to_string(&block_output).unwrap(); + assert!(block_document.contains("
### hidden code ###\ntrue\n
")); + let block_markdown = temp.join("block-comment.md"); + assert!( + Command::new(bin("qdocco")) + .args([ + "--markdown", + block_input.to_str().unwrap(), + "-o", + block_markdown.to_str().unwrap(), + ]) + .status() + .unwrap() + .success() + ); + let block_markdown_document = fs::read_to_string(&block_markdown).unwrap(); + assert!(block_markdown_document.contains("````quickcoffee\n### hidden code ###\ntrue\n````")); let overwrite = Command::new(bin("qdocco")) .args([input.to_str().unwrap(), "-o", input.to_str().unwrap()]) .output() From 93cf8e8b1975d6d4291cc7d14688e6a18d9dd99f Mon Sep 17 00:00:00 2001 From: tiye Date: Sat, 22 Aug 2026 23:38:47 +0800 Subject: [PATCH 2/5] docs: regenerate manuals for qdocco block comments --- docs/manual.classical-zh.html | 4 ++-- docs/manual.devanagari-sa.html | 4 ++-- docs/manual.en.html | 4 ++-- docs/manual.latin.html | 4 ++-- docs/manual.zh-CN.html | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/manual.classical-zh.html b/docs/manual.classical-zh.html index aea453b..e45fafb 100644 --- a/docs/manual.classical-zh.html +++ b/docs/manual.classical-zh.html @@ -62,8 +62,6 @@

数组之环可书 by step;步惟求一遍,须非零有限整数,负者自末起,映射环弗用之。

数组之环亦可系从一始之下标:for value, index in items then value + index。

后置之推导亦循严收集:value * 2 for value in items,或括以 [value * 2 for value in items]。

-

# 此段有无效 ` 文,机不求之

-

#

Code

甲 = 6
 倍 = (x) -> x * 2
 shorthand = 'yes'
@@ -126,5 +124,7 @@
 nil? == false and false? == true and 0? == true
 默认数 ?= 42
 默认数 == 42
+### 此段有无效 ` 文,机不求之
+###
 42 == 42
 
Final value: true
\ No newline at end of file diff --git a/docs/manual.devanagari-sa.html b/docs/manual.devanagari-sa.html index 55f82a1..edcc014 100644 --- a/docs/manual.devanagari-sa.html +++ b/docs/manual.devanagari-sa.html @@ -63,8 +63,6 @@

array-for by step उपयुज्यते; non-zero finite integer step एकवारं मूल्यते, negative क्रमः अन्तिम-पदात् आरभते, map तु न।

array-for शून्यात् गणितं index अपि बध्नाति: for value, index in items then value + index।

postfix-comprehension समानं strict-collection वहति: value * 2 for value in items, अथवा [value * 2 for value in items]।

-

# invalid ` source अत्र उपेक्षितः

-

#

Code

base = 40
 add = (x) -> x + base
 shorthand = 'yes'
@@ -129,5 +127,7 @@
 nil? == false and false? == true and 0? == true
 default_value ?= 42
 default_value == 42
+### invalid ` source अत्र उपेक्षितः
+###
 42 == 42
 
Final value: true
\ No newline at end of file diff --git a/docs/manual.en.html b/docs/manual.en.html index e4393f4..1631674 100644 --- a/docs/manual.en.html +++ b/docs/manual.en.html @@ -66,8 +66,6 @@

Arithmetic also has floor division // and dividend-dependent modulo %%: -7 // 5 is -2, while -7 %% 5 is 3.

return expression exits only its current function; bare return yields nil, cleans loops, and runs enclosing finally blocks.

Parameters may use strict nested array/map patterns; defaults and rest stay name-only.

-

# invalid ` source is safely ignored here

-

#

Code

base = 20
 add = (x) ->
   result = x + base
@@ -136,5 +134,7 @@
 heredoc = """answer #{add(22)}
 next"""
 heredoc == 'answer 42\nnext'
+### invalid ` source is safely ignored here
+###
 42 == 42
 
Final value: true
\ No newline at end of file diff --git a/docs/manual.latin.html b/docs/manual.latin.html index b6d43db..73cf013 100644 --- a/docs/manual.latin.html +++ b/docs/manual.latin.html @@ -63,8 +63,6 @@

Ordo seriei by step uti potest; gradus integer finitus positivus semel aestimatur, maps eum excludunt.

Iteratio seriei etiam indicem a zero numeratum ligare potest: for value, index in items then value + index.

Comprehensio postfix eandem collectionem strictam servat: value * 2 for value in items, vel [value * 2 for value in items].

-

# fons invalidus ` hic ignoratur

-

#

Code

numerus = 7
 quadratum = (x) -> x * x
 shorthand = 'yes'
@@ -127,5 +125,7 @@
 nil? == false and false? == true and 0? == true
 valor_defectus ?= 42
 valor_defectus == 42
+### fons invalidus ` hic ignoratur
+###
 42 == 42
 
Final value: true
\ No newline at end of file diff --git a/docs/manual.zh-CN.html b/docs/manual.zh-CN.html index ef8698f..18b244c 100644 --- a/docs/manual.zh-CN.html +++ b/docs/manual.zh-CN.html @@ -66,8 +66,6 @@

数组循环可写 by step;步长只求一次,须为非零有限整数,负步从末项起,映射循环不用 by。

数组循环亦可绑定从零开始的下标:for value, index in items then value + index。

后置推导沿用严格收集:value * 2 for value in items,亦可写作 [value * 2 for value in items]。

-

# 这段含无效 ` 源文,却不会参与执行

-

#

Code

base = 21
 double = (x) -> x * 2
 shorthand = 'yes'
@@ -133,5 +131,7 @@
 多行 = """答案 #{double(base)}
 次行"""
 多行 == '答案 42\n次行'
+### 这段含无效 ` 源文,却不会参与执行
+###
 42 == 42
 
Final value: true
\ No newline at end of file From 0963a07d2868f965468c7da64ac10c92e128fedb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A2=98=E5=8F=B6?= Date: Sun, 23 Aug 2026 01:51:46 +0800 Subject: [PATCH 3/5] feat: add qbench repeat median sampling (#47) * feat: add qbench repeat median sampling * docs: document qbench repeat sampling --- PERFORMANCE.md | 8 +++- README.md | 3 +- RFCs/0000-project-scope.md | 2 +- RFCs/0105-qbench-repeat-median.md | 12 +++++ src/bin/qbench.rs | 73 ++++++++++++++++++++----------- tests/cli_tools.rs | 17 +++++++ 6 files changed, 87 insertions(+), 28 deletions(-) create mode 100644 RFCs/0105-qbench-repeat-median.md diff --git a/PERFORMANCE.md b/PERFORMANCE.md index 5dc732a..435f7eb 100644 --- a/PERFORMANCE.md +++ b/PERFORMANCE.md @@ -17,7 +17,13 @@ make qbench # 或 cargo run --release --bin qbench -- --json --iterations 100 ``` -`qbench` 为每个内建负载输出一行 JSON,分别记录编译、验证和执行的纳秒总耗时,并在计时循环中校验预期最终值。其结果用于 CI 回归和绘图,不替代下文要求的三次 release 中位数报告。 +要自动取得三次样本的中位数,可加 `--repeat 3`;输出中的 `repeat` 记录样本数,三个 `*_ns` 字段仍是每轮迭代总耗时的中位数: + +```sh +cargo run --locked --release --bin qbench -- --json --iterations 100 --repeat 3 +``` + +`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 702a2ca..10c99f5 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/0104-qdocco-block-comments.md](RFCs/0104-qdocco-block-comments.md)。 +当前实现遵循 [RFCs/0000-project-scope.md](RFCs/0000-project-scope.md) 至 [RFCs/0105-qbench-repeat-median.md](RFCs/0105-qbench-repeat-median.md)。 ```coffee square = (x) -> x * x @@ -26,6 +26,7 @@ cargo run -- --check example.qc 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 --example embed cargo run --bin qdocco -- example.qc -o example.html cargo run --bin qdocco -- --markdown example.qc -o example.md diff --git a/RFCs/0000-project-scope.md b/RFCs/0000-project-scope.md index ecb68c0..f56f004 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 0104;其中 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 延伸至 RFC 0105;其中 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 重复采样中位数,均不改变脚本语言值模型的原型无关约束。 diff --git a/RFCs/0105-qbench-repeat-median.md b/RFCs/0105-qbench-repeat-median.md new file mode 100644 index 0000000..3705363 --- /dev/null +++ b/RFCs/0105-qbench-repeat-median.md @@ -0,0 +1,12 @@ +# RFC 0105:qbench 重复采样与中位数 + +- 状态:已采纳 +- 依赖:RFC 0081、RFC 0096、RFC 0103 + +`qbench` 增加 `--repeat N`(默认 `1`),对每个内建负载完整执行 N 次编译、验证和执行计时。每一轮都运行语义护栏;输出的 `compile_ns`、`verify_ns`、`execute_ns` 是各阶段样本的中位数,仍表示单轮内 `--iterations` 次迭代的纳秒总耗时。偶数样本取排序后的上侧中位数(索引 `N / 2`)。 + +JSON 与文本输出新增 `repeat` 字段/键;`--repeat 0` 或非整数返回退出码 2。默认 `--repeat 1` 保持既有单次采样行为,schema 仍为 `quickcoffee.qbench.v1`。 + +## 验收 + +集成测试必须验证默认和显式 repeat 值、JSON 的中位数记录字段、非法 repeat 参数及每轮语义护栏;性能报告可直接使用 `--repeat 3` 取得三次中位数。 diff --git a/src/bin/qbench.rs b/src/bin/qbench.rs index 5133d6b..09e0f81 100644 --- a/src/bin/qbench.rs +++ b/src/bin/qbench.rs @@ -45,15 +45,21 @@ const WORKLOADS: &[Workload] = &[ ]; fn usage() { - eprintln!("Usage: qbench [--iterations N] [--json]\n qbench --version"); + eprintln!("Usage: qbench [--iterations N] [--repeat N] [--json]\n qbench --version"); } fn json_escape(value: &str) -> String { value.replace('\\', "\\\\").replace('"', "\\\"") } +fn median(samples: &mut [u128]) -> u128 { + samples.sort_unstable(); + samples[samples.len() / 2] +} + fn main() -> ExitCode { let mut iterations = 100; + let mut repeat = 1; let mut json = false; let mut args = env::args().skip(1); while let Some(arg) = args.next() { @@ -74,6 +80,13 @@ fn main() -> ExitCode { return ExitCode::from(2); } }, + "--repeat" => match args.next().and_then(|value| value.parse().ok()) { + Some(value) if value > 0 => repeat = value, + _ => { + eprintln!("--repeat requires a positive integer"); + return ExitCode::from(2); + } + }, _ => { usage(); return ExitCode::from(2); @@ -82,40 +95,49 @@ fn main() -> ExitCode { } let engine = Engine::new(); for workload in WORKLOADS { - let start = Instant::now(); - for _ in 0..iterations { - engine - .compile(workload.source) + let mut compile_samples = Vec::with_capacity(repeat); + let mut verify_samples = Vec::with_capacity(repeat); + let mut execute_samples = Vec::with_capacity(repeat); + for _ in 0..repeat { + let start = Instant::now(); + for _ in 0..iterations { + engine + .compile(workload.source) + .expect("qbench workload must compile"); + } + compile_samples.push(start.elapsed().as_nanos()); + let program = engine + .compile_program(workload.source) .expect("qbench workload must compile"); - } - let compile_ns = start.elapsed().as_nanos(); - let program = engine - .compile_program(workload.source) - .expect("qbench workload must compile"); - let start = Instant::now(); - for _ in 0..iterations { - program.verify().expect("qbench workload must verify"); - } - let verify_ns = start.elapsed().as_nanos(); + let start = Instant::now(); + for _ in 0..iterations { + program.verify().expect("qbench workload must verify"); + } + verify_samples.push(start.elapsed().as_nanos()); - let start = Instant::now(); - for _ in 0..iterations { - let mut context = Context::new().with_fuel(100_000); - let value = context - .run_program(&program) - .expect("qbench workload must execute"); - assert_eq!(value.to_string(), workload.expected, "{}", workload.name); + let start = Instant::now(); + for _ in 0..iterations { + let mut context = Context::new().with_fuel(100_000); + let value = context + .run_program(&program) + .expect("qbench workload must execute"); + assert_eq!(value.to_string(), workload.expected, "{}", workload.name); + } + execute_samples.push(start.elapsed().as_nanos()); } - let execute_ns = start.elapsed().as_nanos(); + let compile_ns = median(&mut compile_samples); + let verify_ns = median(&mut verify_samples); + let execute_ns = median(&mut execute_samples); if json { println!( - "{{\"schema\":\"{}\",\"version\":\"{}\",\"name\":\"{}\",\"iterations\":{},\"expected\":\"{}\",\"compile_ns\":{},\"verify_ns\":{},\"execute_ns\":{}}}", + "{{\"schema\":\"{}\",\"version\":\"{}\",\"name\":\"{}\",\"iterations\":{},\"repeat\":{},\"expected\":\"{}\",\"compile_ns\":{},\"verify_ns\":{},\"execute_ns\":{}}}", OUTPUT_SCHEMA, env!("CARGO_PKG_VERSION"), json_escape(workload.name), iterations, + repeat, json_escape(workload.expected), compile_ns, verify_ns, @@ -123,11 +145,12 @@ fn main() -> ExitCode { ); } else { println!( - "schema={} version={} {} iterations={} compile_ns={} verify_ns={} execute_ns={} expected={}", + "schema={} version={} {} iterations={} repeat={} compile_ns={} verify_ns={} execute_ns={} expected={}", OUTPUT_SCHEMA, env!("CARGO_PKG_VERSION"), workload.name, iterations, + repeat, compile_ns, verify_ns, execute_ns, diff --git a/tests/cli_tools.rs b/tests/cli_tools.rs index 092bad9..197e666 100644 --- a/tests/cli_tools.rs +++ b/tests/cli_tools.rs @@ -499,6 +499,7 @@ fn qbench_json_is_guarded_and_machine_readable() { "\"schema\":\"quickcoffee.qbench.v1\"", "\"name\":\"", "\"iterations\":1", + "\"repeat\":1", "\"expected\":\"", "\"compile_ns\":", "\"verify_ns\":", @@ -517,11 +518,27 @@ fn qbench_json_is_guarded_and_machine_readable() { assert!(!text_stdout.starts_with('{')); assert!(text_stdout.contains("schema=quickcoffee.qbench.v1")); assert!(text_stdout.contains(&format!("version={}", env!("CARGO_PKG_VERSION")))); + assert!(text_stdout.contains("repeat=1")); + let repeated = Command::new(bin("qbench")) + .args(["--json", "--iterations", "1", "--repeat", "3"]) + .output() + .unwrap(); + assert!(repeated.status.success()); + assert!( + String::from_utf8_lossy(&repeated.stdout) + .lines() + .all(|line| line.contains("\"repeat\":3")) + ); let invalid = Command::new(bin("qbench")) .args(["--iterations", "0"]) .output() .unwrap(); assert_eq!(invalid.status.code(), Some(2)); + let invalid_repeat = Command::new(bin("qbench")) + .args(["--repeat", "0"]) + .output() + .unwrap(); + assert_eq!(invalid_repeat.status.code(), Some(2)); } #[test] fn qcoffee_interactive_session_preserves_context_and_recovers_from_errors() { From 5696a2018e8c888977533ac7ee8747ca41f32d6a Mon Sep 17 00:00:00 2001 From: tiye Date: Sun, 23 Aug 2026 02:21:23 +0800 Subject: [PATCH 4/5] fix: preserve multiline qdocco block comments --- src/bin/qdocco.rs | 42 +++++++++++++++++++++++++----------------- tests/cli_tools.rs | 23 +++++++++++++++++++++++ 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/src/bin/qdocco.rs b/src/bin/qdocco.rs index 9edfd58..aa4ea3c 100644 --- a/src/bin/qdocco.rs +++ b/src/bin/qdocco.rs @@ -55,17 +55,38 @@ fn prose_text(line: &str) -> Option<&str> { let text = trimmed.strip_prefix("##")?; (!text.starts_with('#')).then_some(text) } -fn render(source: &str, result: &str) -> String { +fn split_source(source: &str) -> (String, String) { let mut prose = String::new(); let mut code = String::new(); + let mut in_block_comment = false; for line in source.lines() { - if let Some(text) = prose_text(line) { - prose.push_str(&format!("

{}

\n", escape(text.trim()))) + let trimmed = line.trim_start(); + if in_block_comment { + code.push_str(line); + code.push('\n'); + if trimmed.contains("###") { + in_block_comment = false; + } + } else if trimmed.starts_with("###") { + code.push_str(line); + code.push('\n'); + in_block_comment = !trimmed[3..].contains("###"); + } else if let Some(text) = prose_text(line) { + prose.push_str(text.trim()); + prose.push('\n'); } else { code.push_str(line); code.push('\n'); } } + (prose, code) +} +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::(); format!( "QuickCoffee document

Notes

{prose}

Code

{}
Final value: {}
", escape(&code), @@ -73,20 +94,7 @@ fn render(source: &str, result: &str) -> String { ) } fn render_markdown(source: &str, result: &str) -> String { - let mut prose = String::new(); - let mut code = String::new(); - for line in source.lines() { - if let Some(text) = prose_text(line) { - let text = text.trim(); - if !text.is_empty() { - prose.push_str(text); - prose.push('\n'); - } - } else { - code.push_str(line); - code.push('\n'); - } - } + let (prose, code) = split_source(source); let fence = markdown_fence(source); format!( "# QuickCoffee document\n\n## Notes\n\n{prose}\n## Code\n\n{fence}quickcoffee\n{code}{fence}\n\n## Final value\n\n`{result}`\n" diff --git a/tests/cli_tools.rs b/tests/cli_tools.rs index 197e666..f008464 100644 --- a/tests/cli_tools.rs +++ b/tests/cli_tools.rs @@ -103,6 +103,29 @@ fn qdocco_renders_escaped_source_and_checks() { ); let block_markdown_document = fs::read_to_string(&block_markdown).unwrap(); assert!(block_markdown_document.contains("````quickcoffee\n### hidden code ###\ntrue\n````")); + let multiline_block_input = temp.join("multiline-block.qc"); + let multiline_block_output = temp.join("multiline-block.html"); + fs::write( + &multiline_block_input, + "###\n## hidden prose-looking code\n## closing ###\ntrue\n", + ) + .unwrap(); + assert!( + Command::new(bin("qdocco")) + .args([ + multiline_block_input.to_str().unwrap(), + "-o", + multiline_block_output.to_str().unwrap(), + ]) + .status() + .unwrap() + .success() + ); + let multiline_document = fs::read_to_string(&multiline_block_output).unwrap(); + assert!( + multiline_document.contains("###\n## hidden prose-looking code\n## closing ###\ntrue\n") + ); + assert!(!multiline_document.contains("

hidden prose-looking code

")); let overwrite = Command::new(bin("qdocco")) .args([input.to_str().unwrap(), "-o", input.to_str().unwrap()]) .output() From d3b5f726c55163605d884df357e7c354b13aad93 Mon Sep 17 00:00:00 2001 From: tiye Date: Sun, 23 Aug 2026 02:45:11 +0800 Subject: [PATCH 5/5] fix: satisfy clippy in qdocco block parsing --- src/bin/qdocco.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bin/qdocco.rs b/src/bin/qdocco.rs index aa4ea3c..aa3b2dd 100644 --- a/src/bin/qdocco.rs +++ b/src/bin/qdocco.rs @@ -67,10 +67,10 @@ fn split_source(source: &str) -> (String, String) { if trimmed.contains("###") { in_block_comment = false; } - } else if trimmed.starts_with("###") { + } else if let Some(after_marker) = trimmed.strip_prefix("###") { code.push_str(line); code.push('\n'); - in_block_comment = !trimmed[3..].contains("###"); + in_block_comment = !after_marker.contains("###"); } else if let Some(text) = prose_text(line) { prose.push_str(text.trim()); prose.push('\n');