Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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/0098-rfc-index-consistency.md](RFCs/0098-rfc-index-consistency.md)。
当前实现遵循 [RFCs/0000-project-scope.md](RFCs/0000-project-scope.md) 至 [RFCs/0099-bang-not-alias.md](RFCs/0099-bang-not-alias.md)。

```coffee
square = (x) -> x * x
Expand Down
2 changes: 1 addition & 1 deletion RFCs/0000-project-scope.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ QuickCoffee 是一个 Rust 实现的、受 CoffeeScript 2016 启发的脚本引

本仓库中的测试即 0.1 的语义基线。对语法或运行时的新增特性必须先以 RFC 补充定义,并至少添加:成功测试、错误测试及字节码验证测试。

当前已实现的后续语义与工具 RFC 延伸至 RFC 0098;其中 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 延伸至 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 定义 `!` 否定别名,均不改变脚本语言值模型的原型无关约束。
2 changes: 1 addition & 1 deletion RFCs/0098-rfc-index-consistency.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

## 契约

集成测试遍历 `RFCs` 目录下的 `*.md` 文件,要求:
集成测试递归读取 `RFCs/*.md`,要求:

1. 文件名以四位十进制编号开头,编号从 `0000` 连续到最新 RFC,不得有间隙;
2. 每份 RFC 有 `# RFC` 标题及中文 `状态:` 或英文 `Status:` 字段;
Expand Down
23 changes: 23 additions & 0 deletions RFCs/0099-bang-not-alias.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# RFC 0099:前缀 `!` 的严格否定别名

- 状态:已采纳
- 依赖:RFC 0024、RFC 0057

## 动机

CoffeeScript 将文字 `not` 定义为前缀 `!` 的可读别名。QuickCoffee 已有严格 Bool 的 `not` 运算,但单字符形式被词法器拒绝;这使同一语言契约不完整。

## 契约

单独的 `!expression` 与 `not expression` 完全等价,均要求操作数为 Bool,并产生 Bool 的相反值:

```coffee
[!true, !false, not true, not false]
# => [false, true, false, true]
```

`!=` 仍是严格不等比较,不被拆成 `!` 与 `=`。QuickCoffee 不采用 JavaScript 的真值转换,因此 `!0`、`!''` 和 `!1` 都是运行时类型错误;`!!value` 只适用于 Bool。别名不改变字节码、fuel、嵌入 API 或原型无关值模型。

## 验收

词法测试覆盖 `!` 与 `!=` 的区分;核心测试覆盖真/假、双重否定、非 Bool 错误及已验证 `Not` 字节码。五语手册和中英文语法索引同步说明该别名。
1 change: 1 addition & 0 deletions docs/manual.classical-zh.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<p># 注一行;### … ### 注一段,弗相嵌,先于布局析法略之。</p>
<p>名从 Unicode XID 之法,合附之记可续名,而机不正其 Unicode。</p>
<p>yes、on 同 true;no、off 同 false;is、isnt 则严等也。</p>
<p>! 同 not,严反 Bool;!= 仍严不等。</p>
<p>严等与数较可相连,中项惟求一遍,前否则后不求。</p>
<p>qcoffee - 者,自标准输入读其文也。</p>
<p>qcoffee --stats 则书所试指令与余燃料于标准错误,程序之标准输出不改。</p>
Expand Down
1 change: 1 addition & 0 deletions docs/manual.devanagari-sa.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<p># line-comment अस्ति; ### … ### non-nesting block-comment layout तथा parse पूर्वं त्यज्यते।</p>
<p>Unicode XID-नामानि संयोजक-चिह्नानि गृह्णन्ति, अतः स्थित इत्यादि नाम executable अस्ति।</p>
<p>yes/on true, no/off false; is/isnt strict-साम्यम् स्तः।</p>
<p>! strict-Bool not-पर्यायः अस्ति; != strict-असाम्यमेव तिष्ठति।</p>
<p>chained-comparison मध्ये मध्य-मूल्य एकवारं, पूर्व-false चेत् short-circuit भवति।</p>
<p>सामान्य-library साधारण-function रूपेण print, len, type, range, str, keys, values, join, split, assert ददाति।</p>
<p>कार्यं lexical-environment गृह्णाति; y = 2 omitting अथवा nil दत्ते कार्यस्य अन्तरे default भवति; अन्तिमः rest-parameter tail... इति लिख्यते।</p>
Expand Down
1 change: 1 addition & 0 deletions docs/manual.en.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<p># is a line comment; ### … ### is a non-nesting block comment removed before layout and parsing.</p>
<p>Identifiers use Unicode XID rules; combining marks may continue a name and no normalization occurs.</p>
<p>yes/on and no/off are Boolean aliases; is/isnt preserve strict equality.</p>
<p>! is a strict Bool alias for not; != remains strict inequality.</p>
<p>Chained strict or numeric comparisons keep the middle value once and short-circuit.</p>
<p>The standard library is ordinary functions: print, len, type, range, str, keys, values, join, split, and assert.</p>
<p>switch/when selects one strict-equality branch without fallthrough.</p>
Expand Down
1 change: 1 addition & 0 deletions docs/manual.latin.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<p># commentarium lineae est; ### … ### commentarium non-nidificatum ante layout et analysin removetur.</p>
<p>Nomina Unicode XID sequuntur; signa combinantia ea continuant, sine normalizatione.</p>
<p>yes/on sunt true, no/off false; is/isnt aequalitatem strictam servant.</p>
<p>! est negatio Bool stricta sicut not; != inaequalitas stricta manet.</p>
<p>Comparationes conectae medium semel servant atque priore falso breviant.</p>
<p>Bibliotheca communis functiones ordinarias habet: print, len, type, range, str, keys, values, join, split, assert.</p>
<p>Functiones ambitum lexicalem capiunt; y = 2 omissus vel nil intra functionem adhibetur; rest ultimus scribitur tail....</p>
Expand Down
1 change: 1 addition & 0 deletions docs/manual.zh-CN.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<p># 是行注释;### … ### 是不嵌套块注释,内容在布局和解析前忽略。</p>
<p>标识符遵循 Unicode XID:组合附标可作续字符,且引擎不做 Unicode 规范化。</p>
<p>yes/on 与 no/off 是布尔别名;is/isnt 保持严格相等。</p>
<p>! 是严格 Bool 的 not 别名;!= 仍为严格不等。</p>
<p>严格或数值比较可成链,保留中间值且前段失败会短路。</p>
<p>标准库皆为普通函数:print、len、type、range、str、keys、values、join、split 与 assert。</p>
<p>映射字面量可从左至右展开:{...defaults, theme: 'dark'};后写键覆盖先写键。</p>
Expand Down
2 changes: 2 additions & 0 deletions docs/syntax.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ String indexing and strict slices use Unicode scalar boundaries: `'a☕中'[1]`

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

Names support arithmetic compound assignment (`+=`, `-=`, `*=`, `/=`, `%=` and `**=`). Compound assignment is intentionally name-only; members, indexes, and destructuring remain immutable API boundaries.
Expand Down
2 changes: 2 additions & 0 deletions docs/syntax.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@

`do` 可立即调用函数;`do (name, other) -> ...` 将同名外层值按序转发,默认、rest 与解构形参在 `do` 中拒绝,`do -> ...` 仍是零参 IIFE。

`!` 是严格 Bool 的 `not` 别名;`!=` 仍是不等比较,`!1` 等非布尔操作数产生运行时错误。

隐式调用只消费同一逻辑行的普通表达式:`print value`、`add 20, 22`、`double add 20, 22`、`len [1, 2, 3]` 均可;跨布局边界请使用显式括号调用。

`for` 的绑定可用严格递归模式:`for [left, right] in pairs then left + right`、`for own _, value of record then value` 均可。模式不匹配是运行时错误,且本轮绑定绝不部分写入。
Expand Down
1 change: 1 addition & 0 deletions manuals/manual.classical-zh.qc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
## # 注一行;### … ### 注一段,弗相嵌,先于布局析法略之。
## 名从 Unicode XID 之法,合附之记可续名,而机不正其 Unicode。
## yes、on 同 true;no、off 同 false;is、isnt 则严等也。
## ! 同 not,严反 Bool;!= 仍严不等。
## 严等与数较可相连,中项惟求一遍,前否则后不求。
## qcoffee - 者,自标准输入读其文也。
## qcoffee --stats 则书所试指令与余燃料于标准错误,程序之标准输出不改。
Expand Down
1 change: 1 addition & 0 deletions manuals/manual.devanagari-sa.qc
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
## # line-comment अस्ति; ### … ### non-nesting block-comment layout तथा parse पूर्वं त्यज्यते।
## Unicode XID-नामानि संयोजक-चिह्नानि गृह्णन्ति, अतः स्थित इत्यादि नाम executable अस्ति।
## yes/on true, no/off false; is/isnt strict-साम्यम् स्तः।
## ! strict-Bool not-पर्यायः अस्ति; != strict-असाम्यमेव तिष्ठति।
## chained-comparison मध्ये मध्य-मूल्य एकवारं, पूर्व-false चेत् short-circuit भवति।
## सामान्य-library साधारण-function रूपेण print, len, type, range, str, keys, values, join, split, assert ददाति।
## कार्यं lexical-environment गृह्णाति; y = 2 omitting अथवा nil दत्ते कार्यस्य अन्तरे default भवति; अन्तिमः rest-parameter tail... इति लिख्यते।
Expand Down
1 change: 1 addition & 0 deletions manuals/manual.en.qc
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
## # is a line comment; ### … ### is a non-nesting block comment removed before layout and parsing.
## Identifiers use Unicode XID rules; combining marks may continue a name and no normalization occurs.
## yes/on and no/off are Boolean aliases; is/isnt preserve strict equality.
## ! is a strict Bool alias for not; != remains strict inequality.
## Chained strict or numeric comparisons keep the middle value once and short-circuit.
## The standard library is ordinary functions: print, len, type, range, str, keys, values, join, split, and assert.
## switch/when selects one strict-equality branch without fallthrough.
Expand Down
1 change: 1 addition & 0 deletions manuals/manual.latin.qc
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
## # commentarium lineae est; ### … ### commentarium non-nidificatum ante layout et analysin removetur.
## Nomina Unicode XID sequuntur; signa combinantia ea continuant, sine normalizatione.
## yes/on sunt true, no/off false; is/isnt aequalitatem strictam servant.
## ! est negatio Bool stricta sicut not; != inaequalitas stricta manet.
## Comparationes conectae medium semel servant atque priore falso breviant.
## Bibliotheca communis functiones ordinarias habet: print, len, type, range, str, keys, values, join, split, assert.
## Functiones ambitum lexicalem capiunt; y = 2 omissus vel nil intra functionem adhibetur; rest ultimus scribitur tail....
Expand Down
1 change: 1 addition & 0 deletions manuals/manual.zh-CN.qc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
## # 是行注释;### … ### 是不嵌套块注释,内容在布局和解析前忽略。
## 标识符遵循 Unicode XID:组合附标可作续字符,且引擎不做 Unicode 规范化。
## yes/on 与 no/off 是布尔别名;is/isnt 保持严格相等。
## ! 是严格 Bool 的 not 别名;!= 仍为严格不等。
## 严格或数值比较可成链,保留中间值且前段失败会短路。
## 标准库皆为普通函数:print、len、type、range、str、keys、values、join、split 与 assert。
## 映射字面量可从左至右展开:{...defaults, theme: 'dark'};后写键覆盖先写键。
Expand Down
16 changes: 15 additions & 1 deletion src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pub(crate) enum Token {
And,
Or,
Not,
Bang,
Plus,
PlusPlus,
Minus,
Expand Down Expand Up @@ -716,7 +717,7 @@ fn lex_line(
chars.next();
out.push(Token::NotEq)
} else {
return Err(Error::parse("expected '=' after '!'").at_line(line_number));
out.push(Token::Bang)
}
}
'<' => {
Expand Down Expand Up @@ -1017,4 +1018,17 @@ 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]
)
}));
}
}
4 changes: 3 additions & 1 deletion src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,7 @@ impl Parser {
| Token::Throw
| Token::Do
| Token::Not
| Token::Bang
| Token::PlusPlus
| Token::Minus
| Token::MinusMinus
Expand Down Expand Up @@ -741,6 +742,7 @@ impl Parser {
| Token::False
| Token::Nil
| Token::Not
| Token::Bang
| Token::LParen
| Token::Arrow
| Token::FatArrow
Expand Down Expand Up @@ -859,7 +861,7 @@ impl Parser {
}
}
Token::Minus => Ok(Expr::Unary(Unary::Neg, Box::new(self.expr(12)?))),
Token::Not => Ok(Expr::Unary(Unary::Not, Box::new(self.expr(12)?))),
Token::Not | Token::Bang => 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),
Expand Down
15 changes: 15 additions & 0 deletions tests/rfc_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,21 @@ fn floor_division_and_dividend_dependent_modulo_are_strict_numeric_operators() {
);
}
#[test]
fn bang_is_a_strict_boolean_alias_for_not() {
assert_eq!(
eval("[!true, !false, not true, not false]").to_string(),
"[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());
let chunk = compile("value = true\n!value").unwrap();
assert!(chunk.verify().is_ok());
assert!(chunk.disassemble().contains("Not"));
}
#[test]
fn strict_bitwise_operators_use_signed_32_bit_numbers() {
assert_eq!(
eval("[5 & 3, 5 | 2, 5 ^ 1, ~1, 1 << 3, -8 >> 2, -1 >>> 1]").to_string(),
Expand Down
9 changes: 3 additions & 6 deletions tests/rfc_index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ use std::{fs, path::Path};

#[test]
fn rfc_numbers_and_index_references_are_consistent() {
let workspace = Path::new(env!("CARGO_MANIFEST_DIR"));
let rfc_dir = workspace.join("RFCs");
let mut entries = fs::read_dir(&rfc_dir)
let mut entries = fs::read_dir("RFCs")
.expect("RFC directory exists")
.map(|entry| entry.expect("RFC entry is readable").path())
.filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("md"))
Expand Down Expand Up @@ -36,13 +34,12 @@ fn rfc_numbers_and_index_references_are_consistent() {
);
}
let latest_name = &numbered.last().unwrap().1;
let readme = fs::read_to_string(workspace.join("README.md")).expect("README exists");
let readme = fs::read_to_string(Path::new("README.md")).expect("README exists");
assert!(
readme.contains(latest_name),
"README must link the latest RFC"
);
let scope =
fs::read_to_string(rfc_dir.join("0000-project-scope.md")).expect("scope RFC exists");
let scope = fs::read_to_string("RFCs/0000-project-scope.md").expect("scope RFC exists");
assert!(
scope.contains(&format!("RFC {latest:04}")),
"scope RFC must mention the latest RFC"
Expand Down