Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

QuickCoffee 是一台以 Rust 编写、受 CoffeeScript 启发的字节码脚本引擎。它保留紧凑、可读的表达式语法,却不兼容 JavaScript:没有原型链、`this`、`eval` 或嵌入 JavaScript。

当前实现遵循 [RFCs/0000-project-scope.md](RFCs/0000-project-scope.md) 至 [RFCs/0087-reproducible-locked-builds.md](RFCs/0087-reproducible-locked-builds.md)。
当前实现遵循 [RFCs/0000-project-scope.md](RFCs/0000-project-scope.md) 至 [RFCs/0090-string-map-pattern-keys.md](RFCs/0090-string-map-pattern-keys.md)。

```coffee
square = (x) -> x * x
Expand Down
28 changes: 28 additions & 0 deletions RFCs/0090-string-map-pattern-keys.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# RFC 0090:映射解构的字面字符串键

- 状态:已采纳
- 依赖:RFC 0012、RFC 0023、RFC 0075

## 动机

QuickCoffee 的映射字面量允许任意字符串键,但映射解构过去只接受标识符键。这样无法
解构外部数据常见的 `first-name`、`content-type` 等键,也使“映射键是字符串”与“映射
解构键必须是标识符”产生不必要的不对称。

## 契约

映射模式的键可以是标识符或已解码的非插值字符串字面量:

```coffee
{"first-name": first} = {"first-name": "Ada"}
```

字符串键按字面值查找,不执行插值、名称读取或其他表达式;动态 computed key 仍不支持。
重命名、默认值、递归模式、末尾 map rest 和原子匹配规则保持 RFC 0012、0023、0075 不变。
非法缺键、类型错误和模式失败仍不会留下部分绑定。

## 验收

`tests/rfc_core.rs` 覆盖连字符字符串键、单引号键、缺键错误和成功绑定;双语语法索引必须
区分字面字符串键与未支持的 computed key。现有验证器与 VM 指令无需新增,`make check` 仍
执行完整回归与五份文学手册。
4 changes: 2 additions & 2 deletions docs/syntax.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The bundled `qtest --json` runner emits one stable JSON result per file for CI a

Array and Unicode string indexing accepts negative finite integers (`items[-1]`, `'a☕中'[-2]`); out-of-range indices remain errors.

Map destructuring may capture unlisted keys with a final named rest pattern (`{id, ...metadata} = record`); explicit fields remain strict and the captured map is immutable.
Map destructuring accepts identifier or literal string keys (`{"first-name": first} = record`) and may capture unlisted keys with a final named rest pattern (`{id, ...metadata} = record`); keys remain literal (computed expressions are not evaluated), explicit fields remain strict, and the captured map is immutable.

Map literals support left-to-right spread (`config = {...defaults, theme: 'dark'}`); each spread must evaluate to a map and later entries override earlier keys.

Expand Down Expand Up @@ -42,6 +42,6 @@ Maps may also use indentation after a standalone assignment: `record =` followed

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.

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

Implicit calls are supported on one logical line: `print value`, `add 20, 22`, `double add 20, 22`, and `len [1, 2, 3]`. Their arguments use ordinary expression precedence; explicit parentheses remain required for calls spanning layout boundaries.
2 changes: 1 addition & 1 deletion docs/syntax.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
| 字面量 | 十进制、十六进制 `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` |
| 运算 | 算术、严格有符号 32 位位运算 `&`、`|`、`^`、`~`、`<<`、`>>`、`>>>` 及其名称复合赋值、名称复合赋值 `name += value`、`-=`, `*=`, `/=`, `%=`, `**=`、名称前后置更新 `++`/`--`、比较(`==`/`is`、`!=`/`isnt`,可短路成链 `a < b < c`)、`and`/`or`、`not`、仅对 `nil` 回退的 `left ? right`、后缀非 nil 测试 `value?`、仅名称的存在性赋值 `name ?= value`、数组成员 `value in array` / `value not in array`、映射自身键 `key of map` / `key not of map`、数组索引与严格切片 `a[start..end]` / `a[start...end]`、映射成员访问、nil 安全后缀 `a?.name`、`a?[i]`、`a?[start..end]`、`f?(args)` | 成员/索引/解构复合赋值、成员/索引/解构 `?=`、字符串/映射切片、隐式截断、未声明名称检查 |
| 控制 | `if`/`unless`、后置条件、`while … then …`、`until … then …`、语句后置 `body while/until condition`、前置或后置列表推导 `for value[, index] in xs [by step] [when condition] then …` / `value for value in xs`、`switch`/`when`、`try`/`catch`/`finally`、`throw`、函数内 `return`、`break`、`continue` | JS Error 对象、顶层 `return`、映射 `for` 的 `by` |
| 赋值/函数 | `a, b = array`、`[a, tail...] = array`(末项 rest 可为空)、`[a, {point: [b, c]}] = array`、`{key, from: to, ...metadata} = map` 的严格递归解构、`_` 忽略位;`x, y -> expression` 无括号名称闭包、`([a, b], {factor}) -> expression` 解构形参、`(x, y = 2, rest...) -> expression`(缺省或 `nil` 取默认)、`f(items...)` 展开调用、`=>` 同义箭头、`do` 立即调用 | 无括号默认/rest/解构参数、生成器 |
| 赋值/函数 | `a, b = array`、`[a, tail...] = array`(末项 rest 可为空)、`[a, {point: [b, c]}] = array`、`{key, "first-name": first, ...metadata} = map`(标识符或字面字符串键)的严格递归解构、`_` 忽略位;`x, y -> expression` 无括号名称闭包、`([a, b], {factor}) -> expression` 解构形参、`(x, y = 2, rest...) -> expression`(缺省或 `nil` 取默认)、`f(items...)` 展开调用、`=>` 同义箭头、`do` 立即调用 | 动态 computed 映射键、无括号默认/rest/解构参数、生成器 |
| OO/模块 | 无原型工厂类 `class Name(args) -> expression` | 继承、`this`、`new`、import/export |

字符串索引与严格切片按 Unicode 标量边界:`'a☕中'[1]` 为 `'☕'`,`'a☕中'[1..2]` 为 `'☕中'`;负索引从末项计数(`items[-1]`),越界仍报错;字符串 `for` 仍不支持 `by`。
Expand Down
5 changes: 5 additions & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,11 @@ impl Parser {
}
let key = match self.next() {
Token::Ident(key) => key,
Token::String(key, interpolate)
if !interpolate || !key.contains("#{") =>
{
Comment thread
tiye marked this conversation as resolved.
Outdated
key
}
_ => {
self.at = saved;
return None;
Expand Down
21 changes: 21 additions & 0 deletions tests/rfc_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,27 @@ fn map_destructuring_supports_renaming_and_is_atomic() {
assert!(cx.eval("absent").is_err());
}
#[test]
fn map_destructuring_accepts_literal_string_keys() {
assert_eq!(
eval("{\"first-name\": first} = {\"first-name\": 'Ada'}\nfirst").as_str(),
Some("Ada")
);
assert_eq!(
eval("{'answer': value} = {answer: 42}\nvalue").as_number(),
Some(42.)
);
assert!(
Context::new()
.eval("{\"missing-key\": value} = {other: 1}")
.is_err()
);
assert!(
Context::new()
.eval("{\"#{missing}\": value} = {other: 1}")
.is_err()
);
}
#[test]
fn nested_destructuring_is_strict_and_atomic() {
assert_eq!(
eval("[first, [middle, last]] = [1, [2, 39]]\nfirst + middle + last").as_number(),
Expand Down