From 486ccc0b623386b5503a74a8cd4ca9d4820591f8 Mon Sep 17 00:00:00 2001 From: tiye Date: Sun, 23 Aug 2026 12:34:07 +0800 Subject: [PATCH 1/2] feat: rebase signed iteration onto bang alias --- PERFORMANCE.md | 14 ++++++ README.md | 2 +- RFCs/0000-project-scope.md | 2 +- RFCs/0001-language-core.md | 4 +- RFCs/0008-array-destructuring.md | 2 +- RFCs/0012-map-destructuring.md | 2 +- RFCs/0023-nested-destructuring.md | 2 +- RFCs/0026-destructuring-parameters.md | 2 +- RFCs/0029-stepped-array-iteration.md | 8 ++-- RFCs/0070-string-iteration.md | 4 +- RFCs/0073-pattern-defaults.md | 2 +- RFCs/0074-map-spread.md | 2 +- RFCs/0075-map-pattern-rest.md | 2 +- RFCs/0076-negative-indexing.md | 2 +- RFCs/0077-qtest-json-output.md | 2 +- RFCs/0078-bytecode-fingerprint.md | 2 +- RFCs/0083-qdocco-markdown.md | 2 +- RFCs/0095-string-iteration-by-step.md | 2 +- RFCs/0096-stepped-string-benchmark.md | 12 +++-- RFCs/0098-rfc-index-consistency.md | 4 +- RFCs/0100-signed-by-iteration.md | 26 +++++++++++ docs/manual.classical-zh.html | 4 +- docs/manual.classical-zh.md | 2 +- docs/manual.devanagari-sa.html | 4 +- docs/manual.devanagari.sa.md | 2 +- docs/manual.en.html | 4 +- docs/manual.en.md | 2 +- docs/manual.latin.html | 2 +- docs/manual.zh-CN.html | 4 +- docs/manual.zh-CN.md | 2 +- docs/syntax.en.md | 6 +-- docs/syntax.zh-CN.md | 4 +- manuals/manual.classical-zh.qc | 4 +- manuals/manual.devanagari-sa.qc | 4 +- manuals/manual.en.qc | 4 +- manuals/manual.latin.qc | 2 +- manuals/manual.zh-CN.qc | 4 +- src/bin/qbench.rs | 5 +++ src/bin/qdocco.rs | 16 ++++++- src/lexer.rs | 16 +------ src/parser.rs | 4 +- src/vm.rs | 64 ++++++++++++++++++++------- tests/cli_tools.rs | 20 ++++++++- tests/rfc_core.rs | 19 +++++--- tests/rfc_index.rs | 4 ++ 45 files changed, 206 insertions(+), 96 deletions(-) create mode 100644 RFCs/0100-signed-by-iteration.md diff --git a/PERFORMANCE.md b/PERFORMANCE.md index 0ac81e7..bf220f0 100644 --- a/PERFORMANCE.md +++ b/PERFORMANCE.md @@ -138,6 +138,16 @@ for character, index in 'a☕中x' by 2 then sum += index sum ``` +signed-by-iteration(20,000 次): + +```coffee +sum = 0 +for n, index in [1...100] by -3 then sum += n + index +sum +``` + +该负载验证 RFC 0100 的反向数组步进与实际下标绑定;期望值为 `3333`。它与正向 `stepped-iteration` 负载并列,避免性能报告只覆盖顺序遍历。 + for-collection(10,000 次): ```coffee @@ -529,6 +539,10 @@ rest 绑定会复制剩余元素到新的不可变数组,以保持宿主存储 负索引在数组上做一次长度归一化,在字符串上按 Unicode 标量计数后定位;两者均保持越界错误,不复制序列。`negative-indexing` workload(20,000 次)单次样本为:编译 75.609 ms,验证 2.750 ms,执行 33.463 ms。 +## RFC 0100 有符号步长 + +`qbench --json` 现在包含 `signed-by-iteration`,并在每轮编译、验证和执行后检查 `3333`。该负载不把数组复制到宿主侧,反向位置由 VM 的有符号步长直接推进;运行基准时应与 `stepped-iteration` 一起比较编译、验证和执行三个阶段。 + ## RFC 0074 映射展开 在同一 Darwin arm64 开发机上,`cargo bench --bench core` 的 `map-spread` workload(20,000 次)单次样本为:编译 91.995 ms,验证 2.767 ms,执行 48.784 ms。映射展开为每个显式项生成单项映射,再由 `MergeMaps` 合并;后续键覆盖前值。 diff --git a/README.md b/README.md index 151c773..180738f 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/0099-bang-not-alias.md](RFCs/0099-bang-not-alias.md)。 +当前实现遵循 [RFCs/0000-project-scope.md](RFCs/0000-project-scope.md) 至 [RFCs/0100-signed-by-iteration.md](RFCs/0100-signed-by-iteration.md)。 ```coffee square = (x) -> x * x diff --git a/RFCs/0000-project-scope.md b/RFCs/0000-project-scope.md index 03eb140..ba12aad 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 0099;其中 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 延伸至 RFC 0100;其中 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` 步长,均不改变脚本语言值模型的原型无关约束。 diff --git a/RFCs/0001-language-core.md b/RFCs/0001-language-core.md index 6d785d7..66c6429 100644 --- a/RFCs/0001-language-core.md +++ b/RFCs/0001-language-core.md @@ -31,13 +31,13 @@ UTF-8 文本,以换行或分号分隔语句。缩进可形成 `if`、`unless` 纯字面量常量折叠见 RFC 0060;折叠不改变严格运行时错误边界。多行数组与映射可按 RFC 0063 省略逗号,但调用参数仍须显式分隔;RFC 0064 另定义缩进式映射字面量;RFC 0065 增加受约束的同一逻辑行隐式调用。 -`name = expression` 绑定或更新当前词法环境;顶层执行时该环境即全局环境。名称也支持严格数值前置/后置 `++`、`--`(RFC 0055)。函数调用建立子环境,因此函数内新赋值不泄漏到全局。数组与映射支持严格解构赋值;`_` 是显式忽略位。`if condition then expression else expression` 是表达式,`else` 可省略并产生 `nil`;`unless` 是条件取反的同义结构。后置 `expression if condition` 与 `expression unless condition` 在条件不满足时产生 `nil`。后缀 `value?` 仅检查是否非 nil,保持 Bool 值且不检查未绑定名称(RFC 0038)。`while condition then expression` 重复求值,`until condition then expression` 则重复至条件为真,语句位置的 `expression while condition` / `until condition` 是其后置形式(RFC 0036),`loop body` 则无限重复,三者结果均为 `nil`。数组可用严格切片 `items[start..end]`(含末端)或 `items[start...end]`(不含末端),负端点自末尾计,且端点必须为界内整数(RFC 0037)。数组或字符串可用 `for pattern in iterable [by step] [when condition] then expression` 遍历;字符串按 Unicode 标量产生单字符字符串,第二绑定为标量下标(RFC 0070)。映射可用 `for own key_pattern, value_pattern of map [when condition] then expression` 遍历;表达式也可写成后置推导 `expression for pattern in iterable` 或 `expression for own key_pattern, value_pattern of map`(RFC 0054)。绑定模式严格递归且每轮原子写入(RFC 0044),`by` 只用于数组且步长为一次求值的正整数(RFC 0029),`for` 收集每次体值为新数组,`when` 拒绝的项不收集,`break` 产生已收集前缀,`continue` 不收集当前项(RFC 0042)。`switch`/`when` 选择单一分支;`try`/`catch`/`finally` 与 `throw` 处理 QuickCoffee 运行时错误。`return expression` 仅在函数内结束当前调用并返回其值,裸 return 返回 `nil`;它清理循环并执行沿途 finally(RFC 0028)。 +`name = expression` 绑定或更新当前词法环境;顶层执行时该环境即全局环境。名称也支持严格数值前置/后置 `++`、`--`(RFC 0055)。函数调用建立子环境,因此函数内新赋值不泄漏到全局。数组与映射支持严格解构赋值;`_` 是显式忽略位。`if condition then expression else expression` 是表达式,`else` 可省略并产生 `nil`;`unless` 是条件取反的同义结构。后置 `expression if condition` 与 `expression unless condition` 在条件不满足时产生 `nil`。后缀 `value?` 仅检查是否非 nil,保持 Bool 值且不检查未绑定名称(RFC 0038)。`while condition then expression` 重复求值,`until condition then expression` 则重复至条件为真,语句位置的 `expression while condition` / `until condition` 是其后置形式(RFC 0036),`loop body` 则无限重复,三者结果均为 `nil`。数组可用严格切片 `items[start..end]`(含末端)或 `items[start...end]`(不含末端),负端点自末尾计,且端点必须为界内整数(RFC 0037)。数组或字符串可用 `for pattern in iterable [by step] [when condition] then expression` 遍历;字符串按 Unicode 标量产生单字符字符串,第二绑定为标量下标(RFC 0070)。映射可用 `for own key_pattern, value_pattern of map [when condition] then expression` 遍历;表达式也可写成后置推导 `expression for pattern in iterable` 或 `expression for own key_pattern, value_pattern of map`(RFC 0054)。绑定模式严格递归且每轮原子写入(RFC 0044),`by` 只用于数组或字符串,步长一次求值且必须为非零有限整数(RFC 0029、0100),负步长从末项开始;`for` 收集每次体值为新数组,`when` 拒绝的项不收集,`break` 产生已收集前缀,`continue` 不收集当前项(RFC 0042)。`switch`/`when` 选择单一分支;`try`/`catch`/`finally` 与 `throw` 处理 QuickCoffee 运行时错误。`return expression` 仅在函数内结束当前调用并返回其值,裸 return 返回 `nil`;它清理循环并执行沿途 finally(RFC 0028)。 函数为 `(a, b) -> expression` 或无括号普通名称形式 `a, b -> expression`,以创建时的词法环境捕获自由变量;无括号形式不接受默认、rest 或解构参数(RFC 0035)。形参可写为 `name = expression` 取默认值,且默认形参必须在必选形参之后。缺省或传入 `nil` 时,默认式在被调函数内求值;最后一个形参可写成 `rest...` 以接收剩余实参数组。`=>` 作为无 `this` 运行时中的可读同义箭头;两者均不提供 JavaScript 的绑定接收者语义。`do function` 以零实参立即调用函数。没有 rest 时实参不得多于全部形参,也不得少于必选形参。递归函数通过函数自身绑定可用。默认参数细则见 RFC 0021;`for` 见 RFC 0004;无原型工厂类见 RFC 0006;缩进块见 RFC 0009。 ## 标准库 -RFC 0095 扩展字符串 `for` 也可使用正整数 `by` 步长;RFC 0097 规定 `do (name) -> ...` 从同名外层变量转发立即调用实参。 +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`。 diff --git a/RFCs/0008-array-destructuring.md b/RFCs/0008-array-destructuring.md index be2ed0c..166e8da 100644 --- a/RFCs/0008-array-destructuring.md +++ b/RFCs/0008-array-destructuring.md @@ -5,4 +5,4 @@ `name, name, ... = expression` 要求右侧为数组且元素个数与名称严格相同;RFC 0071 允许数组模式最后一项写作 `tail...` 以绑定剩余数组。`_` 是显式忽略位置;其余名称在当前全局环境绑定。整个赋值表达式仍产生原数组。RFC 0023 进一步允许数组项为递归数组或映射模式。 -编译器发出 `Destructure(Pattern)`,VM 在写入任一名称前检查容器种类、长度和 rest 位置,故失败不会留下部分绑定。模式默认值、映射 rest/splat 与函数参数解构中的 computed/string 键仍不支持。 +编译器发出 `Destructure(Pattern)`,VM 在写入任一名称前检查容器种类、长度和 rest 位置,故失败不会留下部分绑定。模式默认值见 RFC 0073,数组 rest 见 RFC 0071;映射 rest 与字面字符串键分别见 RFC 0075、RFC 0090。 diff --git a/RFCs/0012-map-destructuring.md b/RFCs/0012-map-destructuring.md index ead463c..bdbba6f 100644 --- a/RFCs/0012-map-destructuring.md +++ b/RFCs/0012-map-destructuring.md @@ -5,4 +5,4 @@ `{key, source: target} = expression` 要求右侧为映射并包含每个列出的键。未写冒号时键名同时为绑定名;`_` 可作为目标名明确忽略该值。 -VM 在写入环境前读取并验证全部键,因此任何缺键或非映射错误都不会留下部分绑定。RFC 0023 允许映射值继续使用递归数组或映射模式;默认值、computed key 与 rest/splat 仍不支持。 +VM 在写入环境前读取并验证全部键,因此任何缺键或非映射错误都不会留下部分绑定。RFC 0023 允许映射值继续使用递归数组或映射模式;模式默认值、映射 rest 与字面字符串键分别见 RFC 0073、0075、0090,computed key 仍不支持。 diff --git a/RFCs/0023-nested-destructuring.md b/RFCs/0023-nested-destructuring.md index d5e4d98..1da0e28 100644 --- a/RFCs/0023-nested-destructuring.md +++ b/RFCs/0023-nested-destructuring.md @@ -6,7 +6,7 @@ 赋值左侧可递归组合数组和映射模式:`[first, [middle, last]] = value`、`{point: {x, y}, labels: [_, name]} = value`。原有的 `a, b = value` 是顶层数组模式的简写;`{key, source: target} = value` 仍支持键名与绑定名不同。`_` 在任何深度均显式忽略对应值。 -数组模式在每一层都要求数组和值数量精确相同;RFC 0071 为最后一项增加 `name...` rest,允许值数量不少于固定项。映射模式在每一层都要求映射包含所列的标识符键,但允许附加键。模式叶子只能是绑定名或 `_`。模式默认值、映射 rest/splat、computed/string 键与函数参数解构仍不在本 RFC 范围。 +数组模式在每一层都要求数组和值数量精确相同;RFC 0071 为最后一项增加 `name...` rest,允许值数量不少于固定项。映射模式在每一层都要求映射包含所列的标识符键,但允许附加键。模式叶子只能是绑定名或 `_`。模式默认值、映射 rest 与字面字符串键分别由 RFC 0073、0075、0090 扩展;computed key 仍不支持。 ## 字节码与原子性 diff --git a/RFCs/0026-destructuring-parameters.md b/RFCs/0026-destructuring-parameters.md index d9f6180..1bd3c55 100644 --- a/RFCs/0026-destructuring-parameters.md +++ b/RFCs/0026-destructuring-parameters.md @@ -6,7 +6,7 @@ 函数与无原型工厂类的普通形参可使用 RFC 0023 的数组/映射模式:`([left, right], {factor}) -> expression`、`class Point([x, y]) -> expression`;数组模式最后一项可按 RFC 0071 写作 `tail...`。调用时每个实参与对应模式严格匹配;数组长度不足、映射缺键或容器类型不符会使调用失败。 -形参模式的绑定只写入新调用帧的词法环境,永不修改调用者。`_` 可在任何深度忽略值。默认值仅可用于命名形参,如 `factor = 2`;普通参数 rest 仍必须为最终的单个名称 `tail...`,数组模式 rest 见 RFC 0071。模式默认、映射 rest 与函数参数解构中的 computed/string 键不支持。 +形参模式的绑定只写入新调用帧的词法环境,永不修改调用者。`_` 可在任何深度忽略值。默认值仅可用于命名形参,如 `factor = 2`;普通参数 rest 仍必须为最终的单个名称 `tail...`,数组模式 rest 见 RFC 0071。模式默认与映射 rest 分别见 RFC 0073、0075;函数参数解构中的 computed key 仍不支持,字面字符串键见 RFC 0090。 ## 字节码与 API diff --git a/RFCs/0029-stepped-array-iteration.md b/RFCs/0029-stepped-array-iteration.md index cb243d5..831ade0 100644 --- a/RFCs/0029-stepped-array-iteration.md +++ b/RFCs/0029-stepped-array-iteration.md @@ -5,14 +5,14 @@ ## 语法与语义 -数组循环可在可迭代表达式后、可选 `when` 前写 `by step`:`for item in items by 2 then body`。`step` 在建立迭代器时恰好求值一次,必须产生正的有限整数;否则为运行时错误。省略时步长为 `1`。该特性适用于数组及 `range` 结果;映射循环 `for own key, value of map` 不接受 `by`,是解析错误。 +数组循环可在可迭代表达式后、可选 `when` 前写 `by step`:`for item in items by 2 then body`。`step` 在建立迭代器时恰好求值一次,必须产生非零的有限整数;正步长从首项前进,负步长从末项倒退(RFC 0100)。省略时步长为 `1`。该特性适用于数组及 `range` 结果;映射循环 `for own key, value of map` 不接受 `by`,是解析错误。 -步长按数组下标前进:`[1..9] by 3` 依次绑定 `1`、`4`、`7`。过滤、`break`、`continue`、函数 return、fuel 与嵌套循环沿用 RFC 0004 和 RFC 0028 的规则。 +步长按数组下标前进或后退:`[1..9] by 3` 依次绑定 `1`、`4`、`7`,`[1..9] by -3` 依次绑定 `9`、`6`、`3`。过滤、`break`、`continue`、函数 return、fuel 与嵌套循环沿用 RFC 0004 和 RFC 0028 的规则。 ## 字节码与验证 -`IterStartEnumerable` 消耗栈顶的步长和其下的数组或字符串,建立帧私有迭代器;数组按步长前进,字符串仅接受步长 1(字符串语义详见 RFC 0070)。`IterStartMap` 仍只消耗映射。验证器相应要求 enumerable 开始指令有两个栈值,并继续追踪迭代器在 `IterNext`、`IterEnd` 与 `Return` 的平衡。VM 以饱和加法更新数组位置,防止极大合法步长造成整数回绕。 +`IterStartEnumerable` 消耗栈顶的步长和其下的数组或字符串,建立帧私有迭代器;数组和字符串按有符号步长前进或后退。`IterStartMap` 仍只消耗映射。验证器相应要求 enumerable 开始指令有两个栈值,并继续追踪迭代器在 `IterNext`、`IterEnd` 与 `Return` 的平衡。VM 以饱和加法或检查减法更新位置,防止极大合法步长造成整数回绕。 ## 验收 -测试覆盖普通步长、一次求值、零/小数/非数字拒绝、映射拒绝和已编译 chunk 验证。 +测试覆盖正负步长、一次求值、零/小数/非数字拒绝、映射拒绝和已编译 chunk 验证。 diff --git a/RFCs/0070-string-iteration.md b/RFCs/0070-string-iteration.md index ae42af0..a601055 100644 --- a/RFCs/0070-string-iteration.md +++ b/RFCs/0070-string-iteration.md @@ -18,14 +18,14 @@ for character, index in 'a☕中' then index 第二绑定是从零开始的 Unicode 标量下标,而不是 UTF-8 字节偏移。每轮模式匹配成功后才写入绑定;字符串为空时产生空数组。后置推导和语句位置的丢弃循环同样适用。 -字符串迭代也接受 `by step`。步长只求值一次,必须是正有限整数;它跳过 Unicode 标量而不是 UTF-8 字节,第二绑定仍是实际的标量下标: +字符串迭代也接受 `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 影响。 +由于迭代对象可在运行时求值,`for value in dynamic by step` 在运行时按实际数组或字符串类型采用同一非零有限有符号步长。映射仍使用 `of`,不受本 RFC 影响。 非数组、非字符串的 `in` 迭代对象仍是运行时错误;字符串的 `of` 迭代仍是映射类型错误。该功能不暴露 JavaScript 的 UTF-16 code unit、迭代器对象或原型链。 diff --git a/RFCs/0073-pattern-defaults.md b/RFCs/0073-pattern-defaults.md index 2676910..04fccb8 100644 --- a/RFCs/0073-pattern-defaults.md +++ b/RFCs/0073-pattern-defaults.md @@ -1,6 +1,6 @@ # RFC 0073:解构模式默认值 -状态:实现中 +状态:已采纳 数组与映射解构模式成员可写为 `pattern = expression`。输入成员缺失或为 `nil` 时,在当前词法环境中动态求值表达式;默认值可引用此前已绑定成员。多余数组成员、错误容器类型和无默认值的缺失成员均报错,且整个匹配原子回滚。 diff --git a/RFCs/0074-map-spread.md b/RFCs/0074-map-spread.md index 37cbe82..6380a9a 100644 --- a/RFCs/0074-map-spread.md +++ b/RFCs/0074-map-spread.md @@ -1,6 +1,6 @@ # RFC 0074:映射字面量展开 -状态:实现中 +状态:已采纳 映射字面量支持 `...expression` 展开:表达式必须求值为映射,展开项按从左到右合并,后续显式键或展开项覆盖此前同名键。映射保持原型链无关的不可变值语义;非映射展开在运行时报告错误。 diff --git a/RFCs/0075-map-pattern-rest.md b/RFCs/0075-map-pattern-rest.md index 46fed66..b34033e 100644 --- a/RFCs/0075-map-pattern-rest.md +++ b/RFCs/0075-map-pattern-rest.md @@ -1,6 +1,6 @@ # RFC 0075:映射解构尾部模式 -状态:实现中 +状态:已采纳 映射解构可在末尾使用 `...name` 捕获未列出的键:`{id, ...metadata} = record`。显式字段先按普通严格模式匹配;其余键组成新的不可变映射绑定到 `name`。尾部模式必须命名且只能出现一次、位于映射模式末尾;输入必须是映射,缺失的显式字段仍报错并保持原子性。 diff --git a/RFCs/0076-negative-indexing.md b/RFCs/0076-negative-indexing.md index ac733e8..0bdcd59 100644 --- a/RFCs/0076-negative-indexing.md +++ b/RFCs/0076-negative-indexing.md @@ -1,6 +1,6 @@ # RFC 0076:序列负索引 -状态:实现中 +状态:已采纳 数组与字符串索引接受有限整数负值;`-1` 表示最后一个元素或 Unicode 标量,`-2` 表示倒数第二个。超出范围仍是运行时错误,不做隐式截断。字符串按 Unicode 标量而非 UTF-8 字节定位,保持 RFC 0072 的边界语义。 diff --git a/RFCs/0077-qtest-json-output.md b/RFCs/0077-qtest-json-output.md index d083096..33cbd93 100644 --- a/RFCs/0077-qtest-json-output.md +++ b/RFCs/0077-qtest-json-output.md @@ -1,6 +1,6 @@ # RFC 0077:`qtest` JSON 输出 -状态:实现中 +状态:已采纳 `qtest --json FILE_OR_DIRECTORY...` 为每个测试文件输出一行 JSON:成功记录含 `ok: true` 与 `file`,失败记录含 `ok: false`、`file` 与经过 JSON 转义的 `error`。退出码仍以所有测试是否通过为准;`--stats` 继续把执行统计写到标准错误,不污染机器可读标准输出。 diff --git a/RFCs/0078-bytecode-fingerprint.md b/RFCs/0078-bytecode-fingerprint.md index c297544..1bca039 100644 --- a/RFCs/0078-bytecode-fingerprint.md +++ b/RFCs/0078-bytecode-fingerprint.md @@ -1,6 +1,6 @@ # RFC 0078:字节码内容指纹 -状态:实现中 +状态:已采纳 `Chunk::fingerprint()` 与 `Program::fingerprint()` 返回由已编译字节码文本表示计算的确定性 FNV-1a `u64` 指纹。相同内容的共享程序和克隆句柄指纹相同,不同字节码内容应产生不同指纹(不把指纹当作密码学哈希)。 diff --git a/RFCs/0083-qdocco-markdown.md b/RFCs/0083-qdocco-markdown.md index 18f51bf..198bd36 100644 --- a/RFCs/0083-qdocco-markdown.md +++ b/RFCs/0083-qdocco-markdown.md @@ -9,7 +9,7 @@ ## CLI 契约 -`qdocco --markdown FILE [-o OUTPUT]` 先按普通 `qdocco` 路径读取、编译、验证并执行文件,再生成 Markdown。默认输出为输入文件同名的 `.md`;`-o` 指定其他路径。文档包含 `## Notes` 说明栏、四反引号 `quickcoffee` 代码栏和 `## Final value` 最终值栏。代码放在四反引号围栏内,以保留源文本而不让其中的 Markdown 标记执行。 +`qdocco --markdown FILE [-o OUTPUT]` 先按普通 `qdocco` 路径读取、编译、验证并执行文件,再生成 Markdown。默认输出为输入文件同名的 `.md`;`-o` 指定其他路径。文档包含 `## Notes` 说明栏、`quickcoffee` 代码栏和 `## Final value` 最终值栏。代码围栏至少使用四个反引号,并按源码中最长的连续反引号序列自适应加长,以保留源文本而不让其中的 Markdown 标记执行。 `--markdown` 与 `--check` 互斥;冲突返回退出码 2。读取、解析、验证或执行错误沿用现有非零退出语义。`qdocco --check` 仍只验证而不写任何产物,HTML 默认行为不变。 diff --git a/RFCs/0095-string-iteration-by-step.md b/RFCs/0095-string-iteration-by-step.md index ceb9026..1cae979 100644 --- a/RFCs/0095-string-iteration-by-step.md +++ b/RFCs/0095-string-iteration-by-step.md @@ -16,7 +16,7 @@ for character, index in 'a☕中x' by 2 then [character, index] # => [[a, 0], [中, 2]] ``` -步长表达式只求值一次,必须是正的有限整数;数组和字符串共享该检查。动态 `in` 迭代对象在运行时决定采用数组或字符串的步进路径;`of` 映射迭代不接受 `by`。空字符串、过滤、后置推导、`break`、`continue` 与严格递归模式保持 RFC 0070 语义。 +步长表达式只求值一次,必须是非零的有限整数;正步长从首标量开始,负步长从末标量开始(RFC 0100)。数组和字符串共享该检查。动态 `in` 迭代对象在运行时决定采用数组或字符串的步进路径;`of` 映射迭代不接受 `by`。空字符串、过滤、后置推导、`break`、`continue` 与严格递归模式保持 RFC 0070 语义。 ## 实现与验收 diff --git a/RFCs/0096-stepped-string-benchmark.md b/RFCs/0096-stepped-string-benchmark.md index 5248fa4..c0f8423 100644 --- a/RFCs/0096-stepped-string-benchmark.md +++ b/RFCs/0096-stepped-string-benchmark.md @@ -1,7 +1,7 @@ # RFC 0096:字符串步进迭代性能基准覆盖 - 状态:已采纳 -- 依赖:RFC 0045、RFC 0081、RFC 0095 +- 依赖:RFC 0045、RFC 0081、RFC 0095、RFC 0100 ## 动机 @@ -9,7 +9,7 @@ RFC 0095 扩展了字符串 `for ... by` 的执行路径,但若只保留功能 ## 契约 -`cargo bench --bench core` 必须包含 `stepped-string-iteration` 工作负载;`qbench --json` 也必须输出同名记录。负载使用 ASCII 与多字节 Unicode、实际标量下标和 `by 2`,最终值严格为 `2`: +`cargo bench --bench core` 必须包含 `stepped-string-iteration` 工作负载;`qbench --json` 也必须输出同名记录。RFC 0100 另要求 `signed-by-iteration` 覆盖反向数组步进。字符串负载使用 ASCII 与多字节 Unicode、实际标量下标和 `by 2`,最终值严格为 `2`: ```coffee sum = 0 @@ -17,8 +17,14 @@ for character, index in 'a☕中x' by 2 then sum += index sum # => 2 ``` +```coffee +sum = 0 +for n, index in [1...100] by -3 then sum += n + index +sum # => 3333 +``` + 两条基准路径都必须先编译/验证并执行语义检查,再计时;结果不得包含标准输出、文件 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` 与性能报告中的基线数据保持一致。 +`cargo bench --locked --bench core` 的两个步进工作负载通过最终值护栏;`cargo run --locked --release --bin qbench -- --json --iterations 1` 输出 `stepped-string-iteration`(`expected` 为 `2`)和 `signed-by-iteration`(`expected` 为 `3333`)记录;`make check` 与性能报告中的基线数据保持一致。 diff --git a/RFCs/0098-rfc-index-consistency.md b/RFCs/0098-rfc-index-consistency.md index c5bb953..68e4d50 100644 --- a/RFCs/0098-rfc-index-consistency.md +++ b/RFCs/0098-rfc-index-consistency.md @@ -12,7 +12,7 @@ 集成测试递归读取 `RFCs/*.md`,要求: 1. 文件名以四位十进制编号开头,编号从 `0000` 连续到最新 RFC,不得有间隙; -2. 每份 RFC 有 `# RFC` 标题及中文 `状态:` 或英文 `Status:` 字段; +2. 每份 RFC 有 `# RFC` 标题及中文 `状态:` 或英文 `Status:` 字段,且当前范围内不得仍标记为“实现中”; 3. README 链接最新 RFC 文件名; 4. RFC 0000 明确提及最新编号。 @@ -20,4 +20,4 @@ ## 验收 -`make check` 必须包含该集成测试。新增 RFC 时,若忘记连续编号、状态字段或两个索引引用,测试应失败并给出具体断言。 +`make check` 必须包含该集成测试。新增 RFC 时,若忘记连续编号、状态字段、采纳状态或两个索引引用,测试应失败并给出具体断言。 diff --git a/RFCs/0100-signed-by-iteration.md b/RFCs/0100-signed-by-iteration.md new file mode 100644 index 0000000..c5f99f5 --- /dev/null +++ b/RFCs/0100-signed-by-iteration.md @@ -0,0 +1,26 @@ +# RFC 0100:`for ... by` 有符号步长迭代 + +- 状态:已采纳 +- 依赖:RFC 0029、RFC 0070、RFC 0095 + +## 动机 + +CoffeeScript 的推导支持 `by -1` 反向取值。QuickCoffee 早期只接受正步长,虽可用降序范围部分替代,却无法对运行时数组或字符串统一表达反向迭代,也与第二下标绑定的实际位置语义不一致。 + +## 契约 + +`by step` 只拒绝零、非整数和非有限值;正步长从位置 `0` 开始递增,负步长从最后位置开始递减。第二绑定始终是被取元素的实际数组或 Unicode 标量位置: + +```coffee +for value, index in [10, 20, 30, 40] by -2 then [value, index] +# => [[40, 3], [20, 1]] + +for character, index in 'a☕中x' by -2 then [character, index] +# => [[x, 3], [☕, 1]] +``` + +数组与字符串共享该步长;动态 `in` 值在运行时选择对应序列。映射 `of` 仍不接受 `by`。过滤、后置推导、模式绑定、`break`、`continue`、fuel 与嵌套循环保持既有语义。步长只求值一次,越过序列边界即结束,不回绕。 + +## 实现与验收 + +VM 迭代器保存有符号 `i64` 步长,正步长从零开始,负步长从 `len - 1` 开始;位置推进使用饱和/检查减法,避免整数回绕。验收覆盖数组和 Unicode 字符串的正负步长、实际下标、动态步长、零/小数/非数字错误、映射解析错误和字节码验证。 diff --git a/docs/manual.classical-zh.html b/docs/manual.classical-zh.html index d3f7dc5..cd92eb1 100644 --- a/docs/manual.classical-zh.html +++ b/docs/manual.classical-zh.html @@ -15,7 +15,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 可正可负,负者自末起。

do (name, other) -> ... 即调用之,转外层同名之值;do -> ... 仍零参。

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

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

@@ -59,7 +59,7 @@

loop body 者,如 while true 恒行;以 break 出之,犹受 fuel 限。

for 为收集之式:轮体诸值成新数组,when 与 continue 弗收,break 存既得之首。

for 所系可为严式:for [left, right] in pairs,每项全合乃易名。

-

数组之环可书 by step;步惟求一遍,须正有限整数,映射环弗用之。

+

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

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

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

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

diff --git a/docs/manual.classical-zh.md b/docs/manual.classical-zh.md index 515b591..3c2ab20 100644 --- a/docs/manual.classical-zh.md +++ b/docs/manual.classical-zh.md @@ -41,7 +41,7 @@ QuickCoffee 者,Rust 所为字节码机也,非 JavaScript 之运行时。其 `qtest --fuel N FILE...` 则各篇别限其指令,故一篇受限之环,不耗他篇之数。 `qtest --stats` 更书各篇所试指令与余燃料于标准错误,而 `ok` 之出不改。 -遍数组,则曰 `for item in items then expression`;其所系可为严式,如 `for [left, right] in pairs then left + right`,一项之诸名必待全合而后易。若欲间取,则置 `by step`,如 `for item in [1..9] by 3 then expression`。其体诸值聚为新数组;`when` 所拒者不聚,`break` 则反已聚之先段。其步惟求一遍,且须正有限整数;映射之遍不得用之。`break` 止其内环,`continue` 逾其一轮;while、until、loop 之值恒为 `nil`。 +遍数组,则曰 `for item in items then expression`;其所系可为严式,如 `for [left, right] in pairs then left + right`,一项之诸名必待全合而后易。若欲间取,则置 `by step`,如 `for item in [1..9] by 3 then expression`,负步则自末项反行。其体诸值聚为新数组;`when` 所拒者不聚,`break` 则反已聚之先段。其步惟求一遍,且须非零有限整数;映射之遍不得用之。`break` 止其内环,`continue` 逾其一轮;while、until、loop 之值恒为 `nil`。 同一收集之法,亦可后置作 `value * 2 for value in items`,或括之为 `[value * 2 for value in items]`。方括惟为推导之界,不更生一层数组;`by`、`when`、映射、诸式、`break`、`continue` 皆循前式。 diff --git a/docs/manual.devanagari-sa.html b/docs/manual.devanagari-sa.html index 51ea1f4..858f80c 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 मध्ये शून्य-वर्जित signed by-क्रमः अस्ति।

do (name, other) -> ... तत्क्षणं आह्वयति, बहिः समाननाम-मूल्यानि ददाति; do -> ... निरवयवम् अस्ति।

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

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

@@ -60,7 +60,7 @@

loop body अनन्तः while true; break निर्गमं करोति, fuel सीमा तिष्ठति।

for-expression शरीर-मूल्यानि नूतने array मध्ये सञ्चिनोति; when तथा continue त्यजतः, break सञ्चित-पूर्वभागं रक्षति।

for-binding strict-pattern भवति: for [left, right] in pairs प्रत्येक-pair पूर्णतया बध्नाति।

-

array-for by step उपयुज्यते; positive finite integer step एकवारं मूल्यते, map तु न।

+

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 अत्र उपेक्षितः

diff --git a/docs/manual.devanagari.sa.md b/docs/manual.devanagari.sa.md index 24467c7..6ec0982 100644 --- a/docs/manual.devanagari.sa.md +++ b/docs/manual.devanagari.sa.md @@ -43,7 +43,7 @@ nil-सुरक्षित suffix CoffeeScript-रीत्या `record?.nam `qtest --fuel N FILE...` प्रत्येक-document पृथक् instruction-budget ददाति; एकस्य सीमित-loop अन्यस्य budget न क्षिणोति। `qtest --stats` प्रत्येकस्य documentस्य instruction-संख्या तथा अवशिष्ट-fuel standard error मध्ये लिखति, `ok`-निर्गमं न परिवर्तयति। -क्रमः `for item in items then expression` इति लिख्यते; binding strict-pattern अपि भवति, यथा `for [left, right] in pairs then left + right`, तथा प्रत्येक-item-स्य सर्व-binding पूर्ण-match पश्चात् एव परिवर्तते। body-मूल्यानि नूतन-array मध्ये संगृह्णाति, `when`-अस्वीकृतानि न संगृह्णाति, `break` संगृहीत-prefix ददाति। `by step`, यथा `for item in [1..9] by 3 then expression`, एकवारं-मूल्यितं positive finite integer पदं ददाति। map-क्रमे `by` नास्ति; `break` तथा `continue` अन्तःस्थितं क्रमं नियच्छतः; while, until, loop nil फलन्ति। +क्रमः `for item in items then expression` इति लिख्यते; binding strict-pattern अपि भवति, यथा `for [left, right] in pairs then left + right`, तथा प्रत्येक-item-स्य सर्व-binding पूर्ण-match पश्चात् एव परिवर्तते। body-मूल्यानि नूतन-array मध्ये संगृह्णाति, `when`-अस्वीकृतानि न संगृह्णाति, `break` संगृहीत-prefix ददाति। `by step`, यथा `for item in [1..9] by 3 then expression`, एकवारं-मूल्यितं non-zero finite integer पदं ददाति; negative क्रमः अन्तिम-पदात् आरभते। map-क्रमे `by` नास्ति; `break` तथा `continue` अन्तःस्थितं क्रमं नियच्छतः; while, until, loop nil फलन्ति। स एव collector CoffeeScript-postfix-comprehension अपि स्वीकरोति: `value * 2 for value in items`, अथवा `[value * 2 for value in items]`। brackets केवलं comprehension-सीमा, अतिरिक्त nested-array न; `by`, `when`, map, pattern, `break`, `continue` prefix-रूपस्य नियमैः चलन्ति। diff --git a/docs/manual.en.html b/docs/manual.en.html index fd199b7..9e38307 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 accept positive by steps.

+

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

do (name, other) -> ... immediately calls and forwards same-named outer values; do -> ... remains zero-argument.

[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.

@@ -56,7 +56,7 @@

loop body is infinite while true; break exits it and fuel still bounds it.

A for expression collects body values; when and continue omit values, and break keeps the collected prefix.

for bindings may use strict patterns: for [left, right] in pairs binds each pair atomically.

-

An array for loop may use by step; the positive finite integer step is evaluated once, and maps exclude it.

+

An array for loop may use by step; the non-zero finite integer step is evaluated once, negative steps start at the last item, and maps exclude it.

Array for may bind a zero-based index too: for value, index in items then value + index.

Postfix comprehensions use the same strict collector: value * 2 for value in items, or [value * 2 for value in items].

Functions capture lexical scope; y = 2 defaults when omitted or nil, and a final rest parameter is tail....

diff --git a/docs/manual.en.md b/docs/manual.en.md index 2d72394..df5287c 100644 --- a/docs/manual.en.md +++ b/docs/manual.en.md @@ -40,7 +40,7 @@ An array or call item with trailing `...` expands an array: `[1, values..., 4]` Nil-safe suffixes use CoffeeScript-style soak syntax: `record?.name`, `values?[index]`, and `fn?(args)`. If the receiver is `nil`, the suffix returns `nil` and does not evaluate an index or argument. A non-nil receiver follows ordinary strict access rules, so a missing map member still reports an error. -Arrays (including `range` results) can be iterated with `for item in items then expression`, or with one-time-evaluated positive-integer stepping: `for item in [1..9] by 3 then expression`. A second binding receives the zero-based position, as in `for item, index in items then item + index`; with `by`, it receives the actual stepped position. The binding is a strict recursive pattern, so `for [left, right] in pairs then left + right` and `for {point: {x, y}} in values then x + y` are valid; all bindings for an item change atomically. A `for` expression collects body values into a new array; rejected `when` items are omitted and `break` returns the collected prefix. `break` and `continue` affect the innermost loop. `while`/`until`/`loop` evaluate to `nil`; map iteration excludes `by`. +Arrays (including `range` results) can be iterated with `for item in items then expression`, or with one-time-evaluated non-zero integer stepping: `for item in [1..9] by 3 then expression`; a negative step starts at the last item. A second binding receives the zero-based position, as in `for item, index in items then item + index`; with `by`, it receives the actual stepped position. The binding is a strict recursive pattern, so `for [left, right] in pairs then left + right` and `for {point: {x, y}} in values then x + y` are valid; all bindings for an item change atomically. A `for` expression collects body values into a new array; rejected `when` items are omitted and `break` returns the collected prefix. `break` and `continue` affect the innermost loop. `while`/`until`/`loop` evaluate to `nil`; map iteration excludes `by`. The same collector has CoffeeScript's postfix comprehension form: `value * 2 for value in items`, or `[value * 2 for value in items]`. The brackets delimit the comprehension and do not create an extra nested array; `by`, `when`, map iteration, patterns, `break`, and `continue` retain their prefix-form semantics. diff --git a/docs/manual.latin.html b/docs/manual.latin.html index 1d1d4be..9f64d48 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 gradus positivos by accipit.

+

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

do (name, other) -> ... statim vocat et valores externos eiusdem nominis tradit; do -> ... sine argumentis manet.

[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.

diff --git a/docs/manual.zh-CN.html b/docs/manual.zh-CN.html index 15461b6..80b798c 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 可用非零有符号整数,负步从末项起。

do (name, other) -> ... 即刻调用,并按名转发外层值;do -> ... 仍为零参。

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

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

@@ -63,7 +63,7 @@

loop body 是 while true 的无限形式,以 break 退出,仍受 fuel 限制。

for 是收集表达式:每轮体值成新数组,when 与 continue 不收集,break 保留既得前缀。

for 绑定可用严格模式:for [left, right] in pairs 会原子地绑定每个 pair。

-

数组循环可写 by step;步长只求一次,须为正有限整数,映射循环不用 by。

+

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

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

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

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

diff --git a/docs/manual.zh-CN.md b/docs/manual.zh-CN.md index e4db0bb..4d95143 100644 --- a/docs/manual.zh-CN.md +++ b/docs/manual.zh-CN.md @@ -56,7 +56,7 @@ if double(factor) == 12 then print('ok') else print('bad') nil 安全后缀采用 CoffeeScript 风格写法:`record?.name`、`values?[index]` 与 `fn?(args)`。接收者为 `nil` 时结果为 `nil`,索引或实参也不会求值;接收者非 `nil` 时沿用普通访问的严格规则,因此映射缺键仍会报错。 -数组循环写作 `for item in range(1, 4) then print(item)`;可在数组之后写 `by step`,如 `for item in [1..9] by 3 then print(item)`。第二个绑定可取得从零开始的实际下标,如 `for item, index in items then item + index`,步进时仍取数组位置。绑定位置可用严格递归模式,例如 `for [left, right] in pairs then left + right` 或 `for {point: {x, y}} in values then x + y`;每个项的全部绑定只会在模式完整匹配后写入。`for` 是收集表达式:每次循环体值组成新数组,`when` 拒绝的项不收集,`break` 返回已收集前缀。步长只求值一次,且必须是正的有限整数。`break` 和 `continue` 控制最内层循环;`while`/`until`/`loop` 的结果仍为 `nil`;映射循环不支持 `by`。 +数组循环写作 `for item in range(1, 4) then print(item)`;可在数组之后写 `by step`,如 `for item in [1..9] by 3 then print(item)` 或用负步长从末项反向遍历。第二个绑定可取得从零开始的实际下标,如 `for item, index in items then item + index`,步进时仍取数组位置。绑定位置可用严格递归模式,例如 `for [left, right] in pairs then left + right` 或 `for {point: {x, y}} in values then x + y`;每个项的全部绑定只会在模式完整匹配后写入。`for` 是收集表达式:每次循环体值组成新数组,`when` 拒绝的项不收集,`break` 返回已收集前缀。步长只求值一次,且必须是非零的有限整数。`break` 和 `continue` 控制最内层循环;`while`/`until`/`loop` 的结果仍为 `nil`;映射循环不支持 `by`。 同一收集器也支持 CoffeeScript 风格后置推导:`value * 2 for value in items`,或写作 `[value * 2 for value in items]`。方括号只是推导界标,不产生额外嵌套数组;`by`、`when`、映射、模式、`break`、`continue` 仍遵循前置形式。 diff --git a/docs/syntax.en.md b/docs/syntax.en.md index edc05cc..6b7bdc5 100644 --- a/docs/syntax.en.md +++ b/docs/syntax.en.md @@ -26,13 +26,13 @@ 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 `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]`. +String indexing and strict slices use Unicode scalar boundaries: `'a☕中'[1]` is `'☕'`, and `'a☕中'[1..2]` is `'☕中'`. String `for` iteration accepts a non-zero finite signed `by` step over Unicode scalar positions: `for character, index in 'a☕中x' by 2 then index` yields `[0, 2]`, while `by -2` starts at the end. `do` immediately invokes a function; `do (name, other) -> ...` forwards same-named outer values, while defaults, rest, and destructuring are rejected in `do` parameters. `do -> ...` remains a zero-argument IIFE. `!` is a strict Bool alias for `not`; `!=` remains strict inequality. Non-Bool operands such as `!1` are runtime errors. -Array and string `by step` expressions are evaluated once and must be positive finite integers; map iteration excludes `by`. +Array and string `by step` expressions are evaluated once and must be non-zero finite integers; positive steps start at the first item and negative steps start at the last item. Map iteration excludes `by`. Names support arithmetic compound assignment (`+=`, `-=`, `*=`, `/=`, `%=` and `**=`). Compound assignment is intentionally name-only; members, indexes, and destructuring remain immutable API boundaries. @@ -48,7 +48,7 @@ Pure literal arithmetic, comparisons, collections, and interpolation are folded Maps may also use indentation after a standalone assignment: `record =` followed by indented `first: 1` entries lowers to a prototype-free map. Nested map entries are recursive; an ordinary assignment continuation such as `value =` followed by `1 + 2` is not treated as a map. -Supported: decimal, hexadecimal (`0xff`), binary (`0b1010`), octal (`0o755`), and scientific-notation numbers; strings (double quotes support `#{expression}` interpolation), booleans including CoffeeScript aliases (`yes`/`on`, `no`/`off`), `nil`, arrays including integer range literals (`[1..3]` inclusive, `[1...3]` exclusive) and splats (`[head, items...]`), maps including identifier shorthand (`{name}`), indexing and strict array/string slices (`items[start..end]` inclusive, `items[start...end]` exclusive), map-only member access (`record.name`), nil-safe member/index/slice/call suffixes (`record?.name`, `record?[key]`, `items?[start..end]`, `fn?(args)`), postfix non-nil tests (`value?`), name-only existential assignment (`name ?= value`), arithmetic/comparison/logical expressions including strict-equality aliases `is`/`isnt`, short-circuit chained comparisons (`a < b < c`), and nil-specific fallback (`left ? right`), array membership (`value in array` / `value not in array`) and own-map key membership (`key of map` / `key not of map`), `if`/`unless` (including postfix conditions), `while … then …`, `until … then …`, statement-position postfix `body while/until condition`, infinite `loop …`, filtered array `for pattern in xs [by step] when condition then …`, map `for own key_pattern, value_pattern of map then …`, `switch`/`when`, `try`/`catch`/`finally`, `throw`, function-only `return`, `break`, `continue`, strict recursive assignment and parameter patterns (`([a, b], {factor}) -> …`), bare-name `x, y -> …` closures, expression-bodied `->` closures including named trailing default parameters (`y = 2`), rest parameters (`tail...`), and splat calls (`f(items...)`), and zero-argument `do function` IIFEs. A `for` expression collects body values into a new array; rejected `when` values and `continue` are omitted, while `break` returns the accumulated prefix. Array and string slice bounds are finite in-range scalar integers; negative bounds count from the sequence end and no implicit clipping occurs. `value?` is false only for nil and does not suppress an unbound-name error; `?=` is the sole form that treats an unbound name as nil and short-circuits its right side otherwise. Chains retain their middle operand once and short-circuit; aliases preserve Bool and strict QuickCoffee equality with no coercion. A soak suffix short-circuits only a nil receiver; non-nil access remains strict. Omitted or `nil` named optional arguments use the default; required parameters must come first. Defaults, rest, and patterns require parenthesized lambda parameters. Array `by step` evaluates a positive finite integer once; map iteration excludes it. `loop` is `while true` and remains fuel-limited. `return expression` exits only its current function, while bare `return` yields `nil`; it cleans active loops and runs enclosing finalizers. Calls may omit parentheses on one logical line (`f a, b`); explicit `f(a, b)` remains required across layout boundaries and for ambiguous grouping. +Supported: decimal, hexadecimal (`0xff`), binary (`0b1010`), octal (`0o755`), and scientific-notation numbers; strings (double quotes support `#{expression}` interpolation), booleans including CoffeeScript aliases (`yes`/`on`, `no`/`off`), `nil`, arrays including integer range literals (`[1..3]` inclusive, `[1...3]` exclusive) and splats (`[head, items...]`), maps including identifier shorthand (`{name}`), indexing and strict array/string slices (`items[start..end]` inclusive, `items[start...end]` exclusive), map-only member access (`record.name`), nil-safe member/index/slice/call suffixes (`record?.name`, `record?[key]`, `items?[start..end]`, `fn?(args)`), postfix non-nil tests (`value?`), name-only existential assignment (`name ?= value`), arithmetic/comparison/logical expressions including strict-equality aliases `is`/`isnt`, short-circuit chained comparisons (`a < b < c`), and nil-specific fallback (`left ? right`), array membership (`value in array` / `value not in array`) and own-map key membership (`key of map` / `key not of map`), `if`/`unless` (including postfix conditions), `while … then …`, `until … then …`, statement-position postfix `body while/until condition`, infinite `loop …`, filtered array `for pattern in xs [by step] when condition then …`, map `for own key_pattern, value_pattern of map then …`, `switch`/`when`, `try`/`catch`/`finally`, `throw`, function-only `return`, `break`, `continue`, strict recursive assignment and parameter patterns (`([a, b], {factor}) -> …`), bare-name `x, y -> …` closures, expression-bodied `->` closures including named trailing default parameters (`y = 2`), rest parameters (`tail...`), and splat calls (`f(items...)`), and zero-argument `do function` IIFEs. A `for` expression collects body values into a new array; rejected `when` values and `continue` are omitted, while `break` returns the accumulated prefix. Array and string slice bounds are finite in-range scalar integers; negative bounds count from the sequence end and no implicit clipping occurs. `value?` is false only for nil and does not suppress an unbound-name error; `?=` is the sole form that treats an unbound name as nil and short-circuits its right side otherwise. Chains retain their middle operand once and short-circuit; aliases preserve Bool and strict QuickCoffee equality with no coercion. A soak suffix short-circuits only a nil receiver; non-nil access remains strict. Omitted or `nil` named optional arguments use the default; required parameters must come first. Defaults, rest, and patterns require parenthesized lambda parameters. Array and string `by step` evaluate a non-zero finite integer once; negative steps start at the last item, and map iteration excludes it. `loop` is `while true` and remains fuel-limited. `return expression` exits only its current function, while bare `return` yields `nil`; it cleans active loops and runs enclosing finalizers. Calls may omit parentheses on one logical line (`f a, b`); explicit `f(a, b)` remains required across layout boundaries and for ambiguous grouping. Identifiers use Unicode XID rules: XID start or `_` first, XID continue or `_` after, with no normalization. `#` is a line comment; non-nesting `### … ###` block comments are removed before layout. Excluded: embedded JavaScript, prototypes, `this`, `new`, inheritance, modules, regexes, computed map-pattern keys, unparenthesized default/rest/destructuring parameter forms, generators, async, and member/index/destructuring `?=`. Space-only indentation blocks are supported for multi-statement bodies. `class Name(args) -> expression` is supported only as a prototype-free named factory. Conditions accept only booleans and names must be declared by assignment or the host. diff --git a/docs/syntax.zh-CN.md b/docs/syntax.zh-CN.md index 43dcc84..69548bc 100644 --- a/docs/syntax.zh-CN.md +++ b/docs/syntax.zh-CN.md @@ -16,13 +16,13 @@ 整数区间支持升序与降序:`[2..4]` 为 `[2, 3, 4]`,`[4..2]` 为 `[4, 3, 2]`;排除上界形式相应省略终点(`[4...2]` 为 `[4, 3]`)。边界必须是有限整数,过长区间仍报错。 -字符串索引与严格切片按 Unicode 标量边界:`'a☕中'[1]` 为 `'☕'`,`'a☕中'[1..2]` 为 `'☕中'`;负索引从末项计数(`items[-1]`),越界仍报错;字符串 `for` 亦支持按 Unicode 标量下标以正有限整数 `by` 步进,例如 `for character, index in 'a☕中x' by 2 then index` 得 `[0, 2]`。 +字符串索引与严格切片按 Unicode 标量边界:`'a☕中'[1]` 为 `'☕'`,`'a☕中'[1..2]` 为 `'☕中'`;负索引从末项计数(`items[-1]`),越界仍报错;字符串 `for` 支持按 Unicode 标量下标以非零有限有符号整数 `by` 步进,例如 `by 2` 得 `[0, 2]`,`by -2` 从末项开始。 映射字面量可从左至右展开:`{...defaults, theme: 'dark'}`;后写显式键或展开段覆盖先写键,展开值必须为映射。映射解构可用末尾尾部模式 `{id, ...metadata}` 捕获未列键,所得映射为新的不可变值。 普通单引号与双引号字符串支持 `\\0`、`\\b`、`\\f`、`\\n`、`\\r`、`\\t`、`\\v`、引号/反斜杠、两位 `\\xNN`、四位 `\\uNNNN` 及一至六位 `\\u{...}` Unicode 转义。字符串可以跨物理行;普通换行合并为一个空格,字符串内缩进被忽略。行末单个未转义反斜杠会同时去除反斜杠与换行。双引号多行字符串仍支持 `#{expression}` 插值,单引号仍是字面文本;三引号 heredoc 继续保留换行。非法转义或非 Unicode 标量转义是词法错误。 -标识符采用 Unicode XID:首字符为 XID start 或 `_`,后续为 XID continue 或 `_`,不做规范化;组合附标可作为续字符。`#` 是行注释;`### … ###` 是不嵌套的块注释,块内容不影响布局。`for value in array` 遍历数组(包括 `range` 的结果);可写 `by step`,步长只求值一次且必须为正有限整数。`for own key, value of map` 遍历映射且不支持 `by`。`for` 收集每轮体值为新数组;`when` 跳过的项与 `continue` 不收集,`break` 返回既得前缀;`while`、`until` 与 `loop` 的值仍为 `nil`。`return expression` 只可在函数体中使用,立即返回表达式值;裸 `return` 返回 `nil`,并在离开循环或 `try` 时完成清理与 finally。`if`、循环和函数可在换行后用空格缩进多个语句,且缩进必须一致;语句也可用 `;` 分隔。调用可写 `f(a, b)`,亦可在同一逻辑行写 `f a, b`。条件必须是布尔值,名称必须先赋值或由宿主注册。完整规范见 RFC。 +标识符采用 Unicode XID:首字符为 XID start 或 `_`,后续为 XID continue 或 `_`,不做规范化;组合附标可作为续字符。`#` 是行注释;`### … ###` 是不嵌套的块注释,块内容不影响布局。`for value in array` 遍历数组(包括 `range` 的结果);可写 `by step`,步长只求值一次且必须为非零有限整数,负步长从末项开始。`for own key, value of map` 遍历映射且不支持 `by`。`for` 收集每轮体值为新数组;`when` 跳过的项与 `continue` 不收集,`break` 返回既得前缀;`while`、`until` 与 `loop` 的值仍为 `nil`。`return expression` 只可在函数体中使用,立即返回表达式值;裸 `return` 返回 `nil`,并在离开循环或 `try` 时完成清理与 finally。`if`、循环和函数可在换行后用空格缩进多个语句,且缩进必须一致;语句也可用 `;` 分隔。调用可写 `f(a, b)`,亦可在同一逻辑行写 `f a, b`。条件必须是布尔值,名称必须先赋值或由宿主注册。完整规范见 RFC。 `do` 可立即调用函数;`do (name, other) -> ...` 将同名外层值按序转发,默认、rest 与解构形参在 `do` 中拒绝,`do -> ...` 仍是零参 IIFE。 diff --git a/manuals/manual.classical-zh.qc b/manuals/manual.classical-zh.qc index 9811550..0434150 100644 --- a/manuals/manual.classical-zh.qc +++ b/manuals/manual.classical-zh.qc @@ -15,7 +15,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 可正可负,负者自末起。 ## do (name, other) -> ... 即调用之,转外层同名之值;do -> ... 仍零参。 ## [head, tail...] = [1, 2, 3],tail 得 [2, 3];数组之 rest 必居末。 ## qtest --fuel N 者,为各可行文别限其指令之数。 @@ -59,7 +59,7 @@ ## loop body 者,如 while true 恒行;以 break 出之,犹受 fuel 限。 ## for 为收集之式:轮体诸值成新数组,when 与 continue 弗收,break 存既得之首。 ## for 所系可为严式:for [left, right] in pairs,每项全合乃易名。 -## 数组之环可书 by step;步惟求一遍,须正有限整数,映射环弗用之。 +## 数组之环可书 by step;步惟求一遍,须非零有限整数,负者自末起,映射环弗用之。 ## 数组之环亦可系从一始之下标:for value, index in items then value + index。 ## 后置之推导亦循严收集:value * 2 for value in items,或括以 [value * 2 for value in items]。 甲 = 6 diff --git a/manuals/manual.devanagari-sa.qc b/manuals/manual.devanagari-sa.qc index 8c896a7..86f9f44 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 मध्ये शून्य-वर्जित signed by-क्रमः अस्ति। ## do (name, other) -> ... तत्क्षणं आह्वयति, बहिः समाननाम-मूल्यानि ददाति; do -> ... निरवयवम् अस्ति। ## [head, tail...] = [1, 2, 3] tail-नाम्नि [2, 3] बध्नाति; array-pattern rest अन्तिमः भवति। ## qtest --fuel N प्रत्येक executable-document पृथक् instruction-budget ददाति। @@ -60,7 +60,7 @@ ## loop body अनन्तः while true; break निर्गमं करोति, fuel सीमा तिष्ठति। ## for-expression शरीर-मूल्यानि नूतने array मध्ये सञ्चिनोति; when तथा continue त्यजतः, break सञ्चित-पूर्वभागं रक्षति। ## for-binding strict-pattern भवति: for [left, right] in pairs प्रत्येक-pair पूर्णतया बध्नाति। -## array-for by step उपयुज्यते; positive finite integer step एकवारं मूल्यते, map तु न। +## 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]। base = 40 diff --git a/manuals/manual.en.qc b/manuals/manual.en.qc index 18522de..8644a70 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 accept positive by steps. +## for character, index in 'a☕中' then index yields [0, 1, 2]; strings iterate Unicode scalars and accept non-zero signed by steps. ## do (name, other) -> ... immediately calls and forwards same-named outer values; do -> ... remains zero-argument. ## [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. @@ -56,7 +56,7 @@ ## loop body is infinite while true; break exits it and fuel still bounds it. ## A for expression collects body values; when and continue omit values, and break keeps the collected prefix. ## for bindings may use strict patterns: for [left, right] in pairs binds each pair atomically. -## An array for loop may use by step; the positive finite integer step is evaluated once, and maps exclude it. +## An array for loop may use by step; the non-zero finite integer step is evaluated once, negative steps start at the last item, and maps exclude it. ## Array for may bind a zero-based index too: for value, index in items then value + index. ## Postfix comprehensions use the same strict collector: value * 2 for value in items, or [value * 2 for value in items]. ## Functions capture lexical scope; y = 2 defaults when omitted or nil, and a final rest parameter is tail.... diff --git a/manuals/manual.latin.qc b/manuals/manual.latin.qc index 1ce1fc2..6595508 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 gradus positivos by accipit. +## for character, index in 'a☕中' then index indices Unicode scalarum [0, 1, 2] reddit; iteratio stringarum gradus nonnullos signatos by accipit. ## do (name, other) -> ... statim vocat et valores externos eiusdem nominis tradit; do -> ... sine argumentis manet. ## [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. diff --git a/manuals/manual.zh-CN.qc b/manuals/manual.zh-CN.qc index 3c0e14b..ec09888 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 可用非零有符号整数,负步从末项起。 ## do (name, other) -> ... 即刻调用,并按名转发外层值;do -> ... 仍为零参。 ## [head, tail...] = [1, 2, 3] 将 tail 绑定为 [2, 3];数组模式 rest 必须居末。 ## qtest --fuel N 为每份可执行文档设置独立指令预算。 @@ -63,7 +63,7 @@ ## loop body 是 while true 的无限形式,以 break 退出,仍受 fuel 限制。 ## for 是收集表达式:每轮体值成新数组,when 与 continue 不收集,break 保留既得前缀。 ## for 绑定可用严格模式:for [left, right] in pairs 会原子地绑定每个 pair。 -## 数组循环可写 by step;步长只求一次,须为正有限整数,映射循环不用 by。 +## 数组循环可写 by step;步长只求一次,须为非零有限整数,负步从末项起,映射循环不用 by。 ## 数组循环亦可绑定从零开始的下标:for value, index in items then value + index。 ## 后置推导沿用严格收集:value * 2 for value in items,亦可写作 [value * 2 for value in items]。 base = 21 diff --git a/src/bin/qbench.rs b/src/bin/qbench.rs index 69d4ace..0b4715f 100644 --- a/src/bin/qbench.rs +++ b/src/bin/qbench.rs @@ -35,6 +35,11 @@ const WORKLOADS: &[Workload] = &[ source: "sum = 0\nfor character, index in 'a☕中x' by 2 then sum += index\nsum", expected: "2", }, + Workload { + name: "signed-by-iteration", + source: "sum = 0\nfor n, index in [1...100] by -3 then sum += n + index\nsum", + expected: "3333", + }, ]; fn usage() { diff --git a/src/bin/qdocco.rs b/src/bin/qdocco.rs index 9e58f10..3ae655e 100644 --- a/src/bin/qdocco.rs +++ b/src/bin/qdocco.rs @@ -45,10 +45,24 @@ fn render_markdown(source: &str, result: &str) -> String { code.push('\n'); } } + let fence = markdown_fence(source); format!( - "# QuickCoffee document\n\n## Notes\n\n{prose}\n## Code\n\n````quickcoffee\n{code}````\n\n## Final value\n\n`{result}`\n" + "# 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" ) } +fn markdown_fence(source: &str) -> String { + let mut longest = 0; + let mut current = 0; + for character in source.chars() { + if character == '`' { + current += 1; + longest = longest.max(current); + } else { + current = 0; + } + } + "`".repeat((longest + 1).max(4)) +} fn main() -> ExitCode { let mut check = false; let mut markdown = false; diff --git a/src/lexer.rs b/src/lexer.rs index e8647fd..d9105af 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -36,7 +36,6 @@ pub(crate) enum Token { And, Or, Not, - Bang, Plus, PlusPlus, Minus, @@ -717,7 +716,7 @@ fn lex_line( chars.next(); out.push(Token::NotEq) } else { - out.push(Token::Bang) + out.push(Token::Not) } } '<' => { @@ -1018,17 +1017,4 @@ mod tests { assert!(!tokens.contains(&Token::Indent)); assert!(!tokens.contains(&Token::Dedent)); } - - #[test] - fn bang_is_distinct_from_not_and_not_equal() { - let tokens = lex("!value != other").unwrap(); - assert!(tokens.contains(&Token::Bang)); - assert!(tokens.contains(&Token::NotEq)); - assert!(!tokens.windows(2).any(|pair| { - matches!( - pair, - [Token::Bang, Token::NotEq] | [Token::Not, Token::NotEq] - ) - })); - } } diff --git a/src/parser.rs b/src/parser.rs index 3ab6a7b..602df06 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -698,7 +698,6 @@ impl Parser { | Token::Throw | Token::Do | Token::Not - | Token::Bang | Token::PlusPlus | Token::Minus | Token::MinusMinus @@ -742,7 +741,6 @@ impl Parser { | Token::False | Token::Nil | Token::Not - | Token::Bang | Token::LParen | Token::Arrow | Token::FatArrow @@ -861,7 +859,7 @@ impl Parser { } } Token::Minus => Ok(Expr::Unary(Unary::Neg, Box::new(self.expr(12)?))), - Token::Not | Token::Bang => Ok(Expr::Unary(Unary::Not, Box::new(self.expr(12)?))), + Token::Not => Ok(Expr::Unary(Unary::Not, Box::new(self.expr(12)?))), Token::Tilde => Ok(Expr::Unary(Unary::BitNot, Box::new(self.expr(12)?))), Token::PlusPlus => self.prefix_update(Update::Increment), Token::MinusMinus => self.prefix_update(Update::Decrement), diff --git a/src/vm.rs b/src/vm.rs index 6cc8123..52fa71a 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -626,12 +626,12 @@ enum IterationKind { Array { values: Rc>, position: usize, - step: usize, + step: i64, }, String { values: Rc>, position: usize, - step: usize, + step: i64, }, Map { entries: Vec<(String, Value)>, @@ -853,13 +853,22 @@ impl Vm { Instruction::IterStartEnumerable => { let step = array_iteration_step(pop(frame)?)?; match pop(frame)? { - Value::Array(values) => frame.iterators.push(Iteration { - kind: IterationKind::Array { - values, - position: 0, - step, - }, - }), + Value::Array(values) => { + // Negative steps traverse from the final element so the + // optional index remains the actual array position. + let position = if step < 0 { + values.len().saturating_sub(1) + } else { + 0 + }; + frame.iterators.push(Iteration { + kind: IterationKind::Array { + values, + position, + step, + }, + }); + } Value::String(value) => { frame.iterators.push(Iteration { kind: IterationKind::String { @@ -871,7 +880,11 @@ impl Vm { }) .collect(), ), - position: 0, + position: if step < 0 { + value.chars().count().saturating_sub(1) + } else { + 0 + }, step, }, }); @@ -915,7 +928,7 @@ impl Vm { } }); if value.is_some() { - *position = position.saturating_add(*step); + advance_position(position, *step); } value } @@ -932,7 +945,7 @@ impl Vm { } }); if value.is_some() { - *position = position.saturating_add(*step); + advance_position(position, *step); } value } @@ -1291,14 +1304,31 @@ fn numeric_range(start: f64, end: f64, inclusive: bool) -> Result .collect(), ))) } -fn array_iteration_step(value: Value) -> Result { +fn array_iteration_step(value: Value) -> Result { let Value::Number(step) = value else { - return Err(Error::runtime("for by step must be a positive integer")); + return Err(Error::runtime( + "for by step must be a non-zero finite integer", + )); }; - if !step.is_finite() || step.fract() != 0. || step < 1. || step > usize::MAX as f64 { - return Err(Error::runtime("for by step must be a positive integer")); + if !step.is_finite() + || step.fract() != 0. + || step == 0. + || step < i64::MIN as f64 + || step > i64::MAX as f64 + { + return Err(Error::runtime( + "for by step must be a non-zero finite integer", + )); + } + Ok(step as i64) +} +fn advance_position(position: &mut usize, step: i64) { + if step >= 0 { + *position = position.saturating_add(step as usize); + } else { + let amount = step.unsigned_abs() as usize; + *position = position.checked_sub(amount).unwrap_or(usize::MAX); } - Ok(step as usize) } fn truth(v: Value) -> Result { v.as_bool() diff --git a/tests/cli_tools.rs b/tests/cli_tools.rs index 2fe3168..72cfc90 100644 --- a/tests/cli_tools.rs +++ b/tests/cli_tools.rs @@ -55,6 +55,23 @@ fn qdocco_renders_escaped_source_and_checks() { assert!(document.contains("## Notes\n\n")); assert!(document.contains("````quickcoffee\n1 + 2\n````")); assert!(document.contains("## Final value\n\n`3`")); + let fenced_input = temp.join("fenced.qc"); + let fenced_output = temp.join("fenced.md"); + fs::write(&fenced_input, "## Fence\n# ````\ntrue\n").unwrap(); + assert!( + Command::new(bin("qdocco")) + .args([ + "--markdown", + fenced_input.to_str().unwrap(), + "-o", + fenced_output.to_str().unwrap(), + ]) + .status() + .unwrap() + .success() + ); + let fenced_document = fs::read_to_string(&fenced_output).unwrap(); + assert!(fenced_document.contains("`````quickcoffee\n# ````\ntrue\n`````")); let conflict = Command::new(bin("qdocco")) .args(["--check", "--markdown", input.to_str().unwrap()]) .output() @@ -352,8 +369,9 @@ 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(), 5); + assert_eq!(lines.len(), 6); assert!(stdout.contains("\"name\":\"stepped-string-iteration\"")); + assert!(stdout.contains("\"name\":\"signed-by-iteration\"")); for line in lines { assert!(line.starts_with('{') && line.ends_with('}')); for field in [ diff --git a/tests/rfc_core.rs b/tests/rfc_core.rs index 51dc9db..519e89b 100644 --- a/tests/rfc_core.rs +++ b/tests/rfc_core.rs @@ -248,10 +248,7 @@ fn bang_is_a_strict_boolean_alias_for_not() { "[false, true, false, true]" ); assert_eq!(eval("!!true").as_bool(), Some(true)); - let error = Context::new().eval("!1").unwrap_err(); - assert_eq!(error.kind(), ErrorKind::Runtime); - assert!(compile("value = 1\nvalue !in [1]").is_err()); - assert!(compile("value = {key: 1}\nkey !of value").is_err()); + assert!(Context::new().eval("!1").is_err()); let chunk = compile("value = true\n!value").unwrap(); assert!(chunk.verify().is_ok()); assert!(chunk.disassemble().contains("Not")); @@ -969,6 +966,10 @@ fn for_loops_break_and_continue_are_bytecode_control_flow() { eval("for value, index in [10, 20, 30] by 2 then index").to_string(), "[0, 2]" ); + assert_eq!( + eval("for value, index in [10, 20, 30, 40] by -2 then [value, index]").to_string(), + "[[40, 3], [20, 1]]" + ); assert_eq!( eval("for value, index in [10..14] when value % 2 == 0 then index").to_string(), "[0, 2, 4]" @@ -1006,6 +1007,10 @@ fn string_iteration_uses_unicode_scalars_and_optional_scalar_indices() { eval("for character, index in 'a☕中x' by 2 then index").to_string(), "[0, 2]" ); + assert_eq!( + eval("for character, index in 'a☕中x' by -2 then [character, index]").to_string(), + "[[x, 3], [☕, 1]]" + ); assert_eq!( eval("step = 2\nfor character in 'a☕中x' by step then character").to_string(), "[a, 中]" @@ -1120,7 +1125,7 @@ fn postfix_for_comprehensions_reuse_strict_iteration_and_collection_rules() { assert!(Context::new().eval("[n for n in [1], 2]").is_err()); } #[test] -fn array_for_by_uses_a_strict_once_evaluated_positive_integer_step() { +fn array_for_by_uses_a_strict_once_evaluated_signed_integer_step() { assert_eq!( eval("sum = 0\nfor n in [1..9] by 3 then sum = sum + n\nsum").as_number(), Some(12.) @@ -1153,6 +1158,10 @@ fn array_for_by_uses_a_strict_once_evaluated_positive_integer_step() { .verify() .is_ok() ); + assert_eq!( + eval("for n in [1..5] by -2 then n").to_string(), + "[5, 3, 1]" + ); } #[test] fn loop_control_outside_a_loop_is_rejected() { diff --git a/tests/rfc_index.rs b/tests/rfc_index.rs index c42d004..3898fd4 100644 --- a/tests/rfc_index.rs +++ b/tests/rfc_index.rs @@ -23,6 +23,10 @@ fn rfc_numbers_and_index_references_are_consistent() { source.contains("状态:") || source.contains("Status:"), "RFC {name} needs a status field" ); + assert!( + !source.contains("状态:实现中") && !source.contains("Status: Implementing"), + "RFC {name} is still marked as implementing; completed scope RFCs must be adopted" + ); numbered.push((number, name.to_owned())); } From b5544c7199df82f26840703230b28f73804c5a01 Mon Sep 17 00:00:00 2001 From: tiye Date: Sun, 23 Aug 2026 12:35:24 +0800 Subject: [PATCH 2/2] perf: avoid duplicate string iteration traversal --- src/vm.rs | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/vm.rs b/src/vm.rs index 52fa71a..106598a 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -870,21 +870,23 @@ impl Vm { }); } Value::String(value) => { + let values: Rc> = Rc::new( + value + .chars() + .map(|character| { + Value::String(Rc::from(character.to_string())) + }) + .collect(), + ); + let position = if step < 0 { + values.len().saturating_sub(1) + } else { + 0 + }; frame.iterators.push(Iteration { kind: IterationKind::String { - values: Rc::new( - value - .chars() - .map(|character| { - Value::String(Rc::from(character.to_string())) - }) - .collect(), - ), - position: if step < 0 { - value.chars().count().saturating_sub(1) - } else { - 0 - }, + values, + position, step, }, });