zamak · git:20261024.e1fdd3e · 2026-10-24 · sha256 051b988b7b7598b4
zamak git:20261024.e1fdd3eA
Immutable. This exact content never changes and is served at /api/v1/blob/051b988b7b7598b4.
---
name: zamak
description: 编写、运行、调试 Zamak(.zm)程序,或修改 Zamak 编译器本身(Rust 前端、C 后端、IR/缓存版本、VS Code 语法、测试与验收)。当任务涉及 .zm 源码、zam 命令、examples/ 示例、Zamak 语法/标准库/错误值,或 zam/ 下的编译器改动时使用。
---
# Zamak
Zamak 是静态类型、编译到原生可执行文件的小语言,支持 Windows x64、Linux、macOS 三个平台,仓库位置以当前工作目录为准(本环境为 `E:\CODE\Zamak`)。`zam` 就是它自己的编译器:Rust 前端(`zam/src/compiler.rs` 词法/语法/检查)→ C11 后端(`zam/src/native.rs`)→ Windows 默认 MSVC `cl`,Linux/macOS 用 `cc`(细节见「平台差异」)。生成的产物不需要安装 Zamak 或 Rust。
## 命令
- `zam check [project|file]`、`zam build ...`、`zam run ...`、`zam fmt ...`、`zam test [project|file] [过滤词]`、`zam add <名字> [版本范围|git地址|目录] [rev]`、`zam remove <名字>`、`zam update [project]`、`zam search [词]`、`zam publish [project]`、`zam new <directory>`、`zam demo`;无路径参数时用当前目录;源码后缀统一 `.zm`,可直接对单个源码文件操作;`run` 也能直接执行已构建的产物;`zam run <file> -- a b` 把 `--` 之后的参数原样交给程序(只有 `run` 接受 `--`)。
- `zam new <directory>` 生成最小骨架(`zam.toml` + 问候用的 `src/main.zm` + 忽略 `build/` 的 `.gitignore`),列出文件到 stderr,stdout 保持空;目录已存在就报错且不覆盖,目录名必须是合法标识符(写进 `zam.toml` 的 `name`)。
- `zam fmt` 整理格式:缩进统一成 4 空格(行首 Tab 先展开再解析,所以 Tab 缩进的文件 `check` 报错但能 `fmt`),括号内续行按括号层数缩进,注释按上下文层级缩进;多行形式的 `if`/`for`/`while`/`match` 前后各加一个空行(块内第一条、前一行是注释、`elif`/`else` 前、块结束处除外),连续空行压成一个,顶层声明之间一个空行(连续 `use` 之间不加);单行形式不算。去行尾空白、结尾留一个换行、保留 CRLF/LF;注释和字符串原样保留。改过的文件以 `FORMAT <path>` 打到 stderr。语法坏掉的代码先被拒绝。不做行内空格重排与折行。实现在 `compiler::format`(逻辑行 + 物理行两层),幂等。
- `zam test`:运行所有模块里 `fn test_x():` / `fn test_x() -> !E:`(不能带参数;签名不对报 ZM200),不要求 `fn main()`。每个测试单独起进程(`Project::test` → `build_with(cache, tests)` 编出 `build/<name>-test-<hash>`,C `main` 按 `argv[1]` 的 `模块:test_x` 分派,见 `native::generate_with`),并行跑、60 秒超时;stdout 每个测试一行 `test 名字 ... ok/FAILED`,再列失败输出,最后 `test result: ...`,有失败时 stderr 报 `N test(s) failed` 并退出非零。`assert 条件[, 说明]` 是保留字语句,解析时展开成 `if not 条件: $assert(格式化报错)`,`project.rs` 把 `$assert` 换成内建 `std/core:assert_failed` 并在报错前补模块路径;`a == b`/`a != b` 两边是变量、字段、元素或字面量时报错里再显示两边的值。
- 包索引(`zam/src/registry.rs`):默认 `https://github.com/VeyraLux/zamak-index`,`ZAMAK_INDEX` 可换成 git 地址或本地目录(本地目录时 `zam publish` 直接写文件;测试都用本地目录)。条目 `packages/<名字>.toml`:name、description、repository(https)、`[[versions]]` version/commit(40 位)/yanked。依赖写 `mathx = "^1.2"` 或 `{ version = "..." }` → `Source::Registry(Requirement)`;`Requirement` 语义同 Cargo(^ 默认、~、=、>、>=、<、<=、逗号、*,部分版本按段补齐),只支持 `主.次.修订`、不支持预发布。`package::resolve` 循环:`walk` 按当前选择走依赖树,收集每个索引包的(要求方, 范围),`complete` 表示都已选定;每轮对每个包沿用仍满足的选择 → 锁文件里仍满足的版本 → 读索引取满足全部范围的最高未撤回版本,否则 `registry::conflict` 报错;选择不再变化且完整时写锁返回(最多 64 轮,不回溯)。锁文件条目 `[[package]]` name + version 或 rev + git + commit。检出后核对包 zam.toml 的 version 与选中版本一致。`[project]` 新增可选 version、description;没有 entry 的项目是库(`Project.library`,check 不要 main,build/run 报 is a library)。命令:`zam add 名字`(`package::latest` 写 `^x.y.z`)、`zam add 名字 ^1.2`(`requirement_like`)、`zam remove`(`package::remove` 删 [dependencies] 里那一行),二者经 `change_manifest` 失败回滚;`zam search`;`zam publish`(检查、无路径依赖、仓库根、无未提交改动(忽略 zam.lock)、标签 v<版本> 已推送到 origin、名字未被别的仓库占用、版本未发布,打印条目或写本地索引)。测试 `tests/packages.rs::registry_versions_resolve_lock_update_and_conflict`。索引仓库自带 `tools/validate.py`(格式;`--base` 时检查已发布版本不变、不删包、克隆仓库核对新提交与 zam.toml)和 PR 上运行它的 CI。
- C 接口:`extern fn name(a: i64, s: &string) -> f64` 顶层声明、没有函数体(`Function.external`),参数限 i64/f64/bool/&string/&bytes/&mut bytes/C 句柄,返回限无/i64/f64/bool/string/`Option[句柄]`(`compiler::extern_signature` 在解析时查形状,句柄相关的在 `check()` 开头由 `check_extern_handles` 查,那时才知道结构体是不是句柄);不能有类型参数、`!E`、大写名字。生成器 `extern_wrapper` 先写 C 原型(i64→int64_t、f64→double、bool→int、&string/string→const char *),再按 Zamak 签名包一层:&string 经 `zam_cstr` 复制成带 \0 的串、调用后 free,string 返回值 `own(r, strlen(r))`(NULL→空串);只为整个程序里调用到(Call 或 FnRef)的 extern 生成,未用的不需要链接。`zam.toml` 的 `[native]`(`package::Native`):sources(包内相对路径 .c)、include、libraries(裸名,MSVC `name.lib`、GNU `-lname`)、library_paths(MSVC `/LIBPATH:`、GNU `-L`);项目与每个依赖包各一份(`Project.native`)。`build_with` 把 C 源码按 源码+`header_digest`(include 目录里全部 .h)+工具链 缓存成 `.obj`(`OBJECT HIT/MISS <key> <相对路径>`),链接键加上库与库目录;路径用 `std::path::absolute`(编译在缓存临时目录里进行);C 源码自身目录自动加进 include。`native::compile_with` 带 include 编译。测试 `tests/ffi.rs`,示例 `examples/ffi`。
- C 句柄与头文件(E1,IR-36 / native-c-66):`extern type Db: "sqlite3", close = sqlite3_close` 解析成 `Struct { fields: [("$handle", Int)], handle: Some(Handle { c_type, close }) }`(C 侧 `R<id>` 的 `m0` 存 `(int64_t)(intptr_t)` 指针),所以移动、借用、放进列表与结构体都沿用结构体;`dropR` 改成「非 0 时调用 close 再清零」,`copyR` 兜底报错、`showR` 显示 `Db(<C handle>)`、`eqR` 比较指针(`native_values::record`)。检查器:构造 `Db(...)` 报 `is a C handle`(`Checker::constructor`),`copy` 含句柄的类型报错(`compiler::holds_handle` 递归看字段与元素)。extern 签名:参数 `&Db`/`&mut Db` → C 指针,`&mut Option[Db]` 是输出参数(包装里先 `ctype *t = NULL` 传 `&t`,调用后 `dropR` 旧值再写 Some/None),返回 `Option[Db]`(NULL → None,`m0` 是标签 Some=0/None=1、`m1.m0` 是指针,见 `compiler::option_handle`);`&bytes`/`&mut bytes` 传 `p->data`。`[native] headers`(`package::Native.headers`,只允许字母数字与 `_-./`):`generate_with(.., headers)` 在用到 extern 包装或句柄 drop 的目标文件里、`RUNTIME` 之后插 `#include "x.h"`,并且不再生成 extern 原型与 close 原型(句柄指针用真实的 `c_type *`,否则是 `void *` 并生成 `void close(void *);`);`project.rs` 在有 headers 时用 `compile_with` 带上所有 include 目录,模块缓存键加上这些目录的 `header_digest`。示例 `examples/ffi_handles`,测试 `tests/ffi.rs::c_handles_are_closed_exactly_once`。顺带修了解析器:方法链之后也能接 `?`/`catch`(`after_call` 在链后递归一次)。
- 包管理(`zam/src/package.rs`):`zam.toml` 的 `[dependencies]` 写 `name = { git = "url", rev = "v1" }`(rev 省略为默认分支)或 `{ path = "../dir" }`;依赖包是普通项目(`entry` 可省),`[project].name` 必须等于依赖名。`package::resolve` 解析传递依赖(同名不同来源报错),git 依赖按 `zam.lock` 锁定的提交检出到 `缓存/packages/src/<名字>-<提交>`(镜像在 `packages/repos/<url 哈希>`),锁定且已检出时不访问网络;`zam update` = `Project::load_updating` 忽略锁文件。`project.rs` 的 `load_package` 把包模块挂到 `包名/模块`(与包同名的模块就是 `包名`),并改写包内部的 `use`;`Input.package` 标记来源,`zam test`、`zam fmt` 跳过依赖模块。`zam add` 在 `[dependencies]` 下插一行(没有就追加这一节),加载失败时恢复 zam.toml 与 zam.lock;参数是已存在的目录就是路径依赖,否则当 git 地址。测试用本地仓库的 `file://` 地址(`tests/packages.rs`)。
- `zam doc [project|file] [目录]`(`zam/src/doc.rs`):按源码行结构读 `pub fn/struct/enum/const` 与它们上方紧邻的 `#` 注释(第一行当摘要),结构体字段、枚举变体与方法列在类型下面;不跑检查器,所以代码写到一半也能出页面。输出一张自包含的 `index.html`(默认 `docs/`,无 JavaScript,跟随系统深浅色),进度打到 stderr 的 `DOC <路径>`。`Project::sources`/`summary` 提供模块源码与项目名、描述。测试 `tests/doc.rs`、`src/doc.rs` 的单元测试。
- `zam --version` / `--help`;缓存目录默认是用户主目录下的 `.zam/cache`,可用环境变量 `ZAMAK_CACHE_DIR` 覆盖。`check` 在任何平台都不需要 C 编译器。
- C 编译按模块并行,只有缓存 MISS 的模块进编译队列,默认并发度为 `min(可用核数, 8)`,可用环境变量 `ZAMAK_JOBS` 调整(设 `1` 即串行)。并行时 `OBJECT ...` 进度行的顺序不确定,解析进度请按行前缀匹配而不要依赖顺序。
- `zam check --json` / `zam build --json` 在 stdout 输出一行机器可读诊断:`{"command","ok","diagnostics":[{file,line,column,function,code,category,message,suggestion}]}`。`code`(如 `ZM100`)与 `category` 是稳定机器字段,分支判断只依赖它们;`message` 是人类可读文本,`suggestion` 在有修复路径时给出(`ZM999` 无建议,值为 `null`);`file`/`line`/`column`/`function` 无法定位时为 `null`。**词法/语法/模块解析错误只报第一条;之后类型与所有权检查逐个函数进行,每个出错的函数各报它的第一条,`diagnostics` 按文件、行、列排序,可能有多项。** 一轮可以修完各函数的问题再检查;同一函数里的后续错误要等前一条修好才出现。 失败时 stderr 仍有一行人类可读信息、退出码非零;其他子命令不接受 `--json`。
- 构建进度(`CACHE`/`OBJECT`/`LINK`/`TIMING`/`OUTPUT`)与错误都写 **stderr**,stdout 只有程序输出或 `--json` 的那一行;`TIMING` 一行给出 `check_ms`/`key_ms`/`object_ms`/`object_prepare_ms`/`link_ms`/`total_ms`;`OUTPUT <path>` 的产物名按平台不同(见下表)。命令失败返回非零码。
## 平台差异(Windows / Linux / macOS)
| | Windows x64 | Linux | macOS |
|---|---|---|---|
| 默认 C 工具链 | MSVC:`vswhere` 找带 `VC.Tools.x86.x64` 组件的 VS / Build Tools,直接调 `Hostx64\x64` 下的 `cl.exe`/`link.exe`;找不到就依次用 PATH 上的 `gcc`、`clang`(本机有 MinGW gcc:`C:\ProgramData\mingw64\mingw64\bin`) | 依次 `cc`、`clang`、`gcc`(例如装 `build-essential`) | 同 Linux(Xcode Command Line Tools 的 Apple clang,`xcode-select --install`) |
| 换编译器 | 设 `CC=gcc`(MinGW)改走 GNU 后端 | 设 `CC=clang` 等 | 同 Linux |
| 产物 | `build/<name>-<hash>.exe` | `build/<name>-<hash>`,无后缀,权限 0755 | 同 Linux |
| 同键同字节 | `.obj` + `/Brepro` + `/d2pathmap`;MinGW 链接加 `-Wl,--no-insert-timestamp`(否则 PE 头带链接时间,重链接报 corrupt cache entry) | `.o` + `-ffile-prefix-map=<临时目录>=.` | 同 Linux |
| 缓存目录 | `%USERPROFILE%\.zam\cache` | `$HOME/.zam/cache` | 同 Linux |
| 额外缓存 | MSVC 环境一次性捕获到 `%TEMP%\zam-msvc-<hash>.txt`,按 `vcvars64.bat` 的大小和修改时间失效 | 无 | 无 |
| 验证 | 本机 + CI `windows-latest` | 本机 WSL2(见下)+ CI `ubuntu-latest` | 只有 CI `macos-latest`,本机无法验证 |
| 控制台中文 | 输出走 `WriteConsoleW`、输入走 `ReadConsoleW`(`zam_write`、`zam_stdin_getc`),重定向时照常读写 UTF-8 字节;改动后跑 `pwsh -File tools/check-console-input.ps1`(真控制台里注入中文按键) | 终端本身是 UTF-8 | 同 Linux |
- `CC` 必须是单个 GNU 风格的编译器程序:不能带参数、不能是 MSVC `cl`;在 Windows 上也不能是 `.bat`(`.bat` 会被当成 MSVC 的 vcvars 脚本)。要加 sanitizer 之类的参数,就写一个包装脚本再把 `CC` 指过去(Unix)。
- 缓存键包含 `OS` 与 `ARCH`,同一缓存目录被多个平台共用时不会混用产物。
- 选择逻辑在 `native.rs` 的 `select()`(`CC` → Windows 的 `msvc()` → PATH 回退),单元测试 `toolchain_selection_prefers_cc_then_msvc_then_path_fallbacks` 覆盖顺序;本机模拟「没有 Visual Studio」:把环境变量 `ProgramFiles(x86)` 指向空目录(`tests/toolchain.rs` 就是这么做的)。
- **本机 Linux 验证**:WSL 发行版 `Ubuntu`(24.04,默认用户 root)已装 gcc 13.3、clang 18.1、Rust 1.99(含 clippy/rustfmt)。不要直接在 `/mnt/e/...` 上跑:Windows 检出是 CRLF(与 CI 的 LF 不同),而且会和 Windows 共用 `zam/target`。`tools/wsl-verify.sh` 把仓库 clone 到 WSL 自己的文件系统,套上 Windows 侧用 `git diff --output` 导出的未提交补丁(不要用 shell 重定向,pwsh 可能改编码),依次跑 fmt、gcc 与 `CC=clang` 两套全量测试、clippy,再用 ASan/UBSan/LeakSanitizer 跑 `SANITIZE_FILES` 里的示例(正常路径,参数是一个存在的文件、stdin 是几行整数)和 `SANITIZE_FAILING` 里的示例(参数指向不存在的文件,覆盖错误冒出 `main` 的路径)。在仓库根目录用 pwsh 执行:
```powershell
git diff HEAD --output=zam/target/wsl.patch
wsl -d Ubuntu -u root -- env SANITIZE_FILES="examples/io/wc.zm examples/control/loops.zm" SANITIZE_FAILING="examples/io/wc.zm" bash tools/wsl-verify.sh
```
每行输出 `passed=N failed=0`、`CLIPPY_OK`、`... sanitizer_reports=0` 才算通过。新增的未跟踪文件不会进 `git diff HEAD`,先 `git add -N <文件>` 再导出。`.sh` 由 `.gitattributes` 固定为 LF,否则 Windows 检出的 CRLF 会让 bash 报 `$'\r'`。
- 给用户的命令按对方的 shell 写,环境变量写法不同:
| 操作 | pwsh(Windows 默认) | bash / zsh(Linux、macOS) |
|---|---|---|
| 单次设变量 | `$env:ZAMAK_JOBS='1'; zam build` | `ZAMAK_JOBS=1 zam build` |
| 换编译器 | `$env:CC='gcc'; zam build` | `CC=clang zam build` |
| 取消变量 | `Remove-Item Env:ZAMAK_JOBS` | `unset ZAMAK_JOBS` |
注意 pwsh 的 `$env:X=...` 会留在当前会话里,后续命令都受影响,用完要取消;bash 的前缀写法只作用于这一条命令。
## 语法速览(动手前先看 `examples/`)
- **块靠缩进(Python 式)**:块头 `fn`/`struct`/`enum`/`if`/`elif`/`else`/`while`/`for`/`match`/每个 match 分支/`catch e` 以 `:` 结尾;同一行写一条语句或一个值(`if n < 1: return n`、`fn double(n: i64) -> i64: n * 2`),或换行多缩进 4 空格写一块;空块 `pass`。括号内忽略换行和缩进;空行、纯注释行跳过。缩进里有 Tab 报错(建议 `zam fmt`);退回到不存在的层级报 `unindent does not match any outer indentation level`;多缩进报 `unexpected indent`;块头后面没缩进报 `expected an indented block after the X line`,整个文件几乎没缩进时再加一句 `the indentation may have been lost when the code was copied`。花括号只用于字典字面量、字典类型和插值;用 `{` 写块会报 `blocks are indented, not wrapped in braces`。词法层(`compiler::lex`)用缩进栈产生 `Indent`/`Dedent`,括号栈非空时不产生换行;未闭合的括号报 `'(' was never closed`。
- 函数:`fn f(a: i64, s: &string) -> i64:` 加缩进块;函数尾部的值表达式即返回值(函数体最后一条,或尾部 if/elif/else 分支的最后一条),也可写 `return`;**循环体和非尾部分支末尾的调用只求值、不返回**;跨模块可见要写 `pub fn`;**函数名首字母必须小写**(大写留给结构体字面量)。
- 绑定:`let x = 1`、`let mut x = 1`、`let mut x: i64 = 1`。**不支持遮蔽**(同一作用域重名报错);块内变量不能逃出块。
- 常量:顶层 `const LIMIT = 100`、`const RATE: f64 = 0.25`、`pub const NAME = "x"`;名字全大写(`compiler::constant_name`);值由 `compiler::fold` 在解析期算好(字面量、前面的常量、运算符;溢出/除零/移位越界报错),存进 `Module.constants`;`project.rs` 的 `constant_use` 把 `LIMIT`、`config.LIMIT`、字符串常量的 `&NAME` 换成字面量,IR 里没有常量节点。常量不能赋值,局部名不能与常量同名,私有常量跨模块报 `private constant`。`zam fmt` 连续的单行 const 之间不加空行。
- 类型:`i64`、`f64`、`bool`、`string`、`bytes`、命名结构体、`[i64; N]`(定长,长度 1–1024,只能是局部值)、`[T]`(可增长列表,`T` 是 `i64`/`f64`/`bool`/`string` 或结构体,可作局部、参数、返回值、结构体字段)、`{K: V}`(字典,`K` 是 `i64`/`string`,`V` 是 `i64`/`f64`/`bool`/`string` 或结构体,用法同列表)。
- 结构体借用与结构体列表:参数 `&Point` / `&mut Point`,实参 `&p` / `&mut p` / `&xs[i]` / `&mut xs[i]`;`&mut` 能改字段或整体赋值,`&` 不能写。`xs[i].f` 读写元素字段(复合赋值的下标不能调用函数),`&xs[i].name` 借用元素字段,`xs[i].tags.push(v)` 调用元素字段上的容器方法,`xs[i] = Point(...)` 整体替换,`for p in &xs` 得到 `&Point`。**结构体赋值是移动**:`let p = xs[i]` 报 `cannot move a struct out of xs[..]`,要副本写 `copy(&xs[i])`,拿走用 `pop`;结构体值的字典没有 `get`。不支持 `xs[i].tags[j]`(元素字段的列表再接下标)。C 侧:`Type::RecordRef` 是 `R*`,`Elem::Record` 的列表叫 `LR<id>`、字典值后缀 `R<id>`;`generate()` 先把结构体与容器收集到不动点,再「前置声明 → 列表/字典本体 → 结构体(按值依赖排序)→ 字典条目 → drop 原型 → 函数」发射。
- 枚举与 match:`enum Shape:` 下面每行缩进一个变体 `Circle(r: f64)`、`Rect(w: f64, h: f64)`、`Empty`(首字母大写);构造 `Shape.Circle(1.0)`(按位置)、`Shape.Empty`;`match s:` 下面每行一个分支 `Circle(r): 语句或值`、`Rect(w, _):` 换行缩进块、`_: ...`;`let v = match s:` 是表达式形式(分支冒号后只能是一个值)。分支写 `=>` 会报错。**分支必须覆盖所有变体或以 `_` 结尾**(否则 `ZM511`);match 只借用被匹配值,标量载荷按值、其他载荷是只读借用,分支里不能改被匹配的变量;枚举能 `==`(标签相同且数据相等)和 `println`(`Shape.Circle(1.5)`)。字面量 match:分支 `1 | 2:`、`'a':`、`"go":`、`true:`、`_:`,解析期(`Parser::pattern` → `Arm`,`literal_arms`/`literal_test`)展开成 if/elif 链;被匹配值不是变量/字段/元素时由 `Parser::subject` 存进隐藏变量 `$matchN`(`Parser.pending` 插到当前语句前);语句形式可无 `_`,表达式形式要 `_` 或 true/false 全列。实现:枚举是 `Struct` 加 `variants`,字段 0 是 `$tag`、载荷平铺为 `Variant$field`;`Expr::Variant`、`Expr::Match`、`Instruction::Match`。
- 浮点 `f64`:字面量 `1.5`、`2e-3`、`1_000.5`(小数点两边都要有数字);IEEE 754,不检查溢出;除以零(`/`、`//`、`%`)与 Python 一样终止程序(C 侧 `zam_fdiv`/`zam_ffloordiv`/`zam_fmod`),无穷大用 `math.inf`。**整数与小数混用时整数变小数**(`x * 2`、`1 + 0.5`、`n < 2.5`;`compiler::promote` 在检查器/生成器入口把整数一侧换成内部的 `Unary('f', x)`,生成 `(double)x`);期望 `f64` 的位置给整数也放行(检查器 `expr()` 开头 `expected == Float && ty == Int`,靠 C 的 int64_t → double 隐式转换;新增“期望 f64”的生成路径时要确认是赋值/参数/return/初始化之一)。显式转换 `f64(n)`/`float(n)`、`i64(x)`/`int(x)`(向零截断,NaN 或越界终止程序)。`println`/插值输出最短可读回十进制(`0.1`、`2.0`、`1e16`、`2.5e-7`、`NaN`、`inf`);`fmt.fixed(x, n)`(`use std/fmt`)保留 n 位小数;`string.parse_f64(&s) -> f64!string`。字典键不能是 `f64`。
- 列表:空列表**必须标注类型** `let mut xs: [i64] = []`(不带标注的 `[1, 2]` 是 `[i64]` 列表,2026-10-24 起;定长数组要写 `[i64; N]` 标注。检查器 `ty` 和生成器 `Generator::ty` 各有一份字面量推断,两处必须一致,否则检查通过、生成时 panic);`xs.len()`、`xs.push(v)`(字符串移入)、`xs.pop()`;`xs[i]` 读写(越界或空 `pop` 终止程序);字符串元素**不能** `xs[i]` 取出,借用 `&xs[i]` 或 `string.copy(&xs[i])`;`for x in &xs` 遍历(字符串元素是只读借用),`for i, x in &xs` 带下标,`for x in &mut xs` 原地改元素(标量元素是可赋值别名,C 侧 `T *p = &data[i]`、读写 `(*p)`;字符串/结构体元素是 `&mut` 借用),循环体内不能增删/移动 `xs`;参数 `&[T]` / `&mut [T]`,传 `&xs` / `&mut xs`;`xs.swap(i, j)`、`xs.sort_by(fn(a, b): a.x < b.x)`(任何元素,稳定,`compiler::sort_by_type`,C 侧 `sortby<L>` 只在比较函数类型出现过时生成;闭包读接收者报 borrow conflict)、`xs.insert(i, v)`、`xs.pop(i) -> T`、`xs.remove(x)`(按值删第一个,同 Python)、`xs.reverse()`、`xs.clear()`、`xs.extend(ys)`(移入);标量与字符串元素还有 `xs.sort()`(稳定归并排序,NaN 最后)、`xs.contains(v)`、`xs.index_of(v)`(-1)、`xs.slice(a, b) -> [T]`(复制),结构体列表没有这四个;方法接收者是变量、字段路径或调用结果(`f().len()`、`xs.slice(0, 2).len()`,IR 是 `Expr::Chain(接收者, 方法, 实参)`,接收者是临时值、登记在 owners 里语句结束释放)。
- 嵌套容器:`[[i64]]`、`{string: [string]}`、`[{string: i64}]` 等任意层;`grid[i][j]` 读写(解析成 `ElementMethod(.., "$at"/"$put", ..)`,C 侧 `valat`/`put`,两层以内,字符串读出副本),`grid[i].push(x)`、`for row in &grid`、`&grid[i]`;`grid[i]` 不能移出。实现:`Elem::Nested(id)`,id 是内层类型 Debug 文本的哈希,`compiler::nest`/`nested` 维护全局表;`project.rs` 的 `resolve_type`/`resolve_elem` 改写内层的结构体 id 后重新登记;C 名字按结构拼接(`LLi`、`MsLs`),生成器先给所有容器的 drop 原型。
- 字典:字面量 `{"a": 1, "b": 2}`(逗号分隔,可分行),空字典**必须标注类型** `let mut m: {string: i64} = {}`;`m[k]` 读(键不存在**终止程序**)、`m[k] = v` 插入或替换;字符串键按只读借用传:`m[&name]`、`m["lit"]`、遍历得到的键直接 `m[k]`(owned 变量不加 `&` 会报 `expected explicit borrow`);`m.len()`、`m.contains(k)`、`m.get(k, default)`(缺失返回 default,字符串值返回副本)、`m.remove(k) -> bool`、`m.keys() -> [K]`、`m.values() -> [V]`(结构体值没有)、`m.clear()`;`for k in &m` 按**插入顺序**遍历键,`for k, v in &m`/`for k, v in &mut m` 带值(`for k in &mut m` 报错),循环体内不能增删 `m`;IR 是 `Instruction::Each(名1, Option<名2>, 路径, 是否&mut, 体)`,绑定类型由 `compiler::each_bindings` 统一给出;字符串值不能 `m[k]` 取出,用 `&m[k]` 或 `get`。计数惯用法:`counts[word] = counts.get(word, 0) + 1`。
- 方法:写在 struct/enum 块里(字段、变体之后),`fn norm(&self)`、`fn scale(&mut self, k: f64)`、`fn into(self)`,没有 self 的是关联函数 `P.new(...)`(跨模块 `geo.P.new`);调用 `p.norm()`、`xs[i].norm()`、`make().norm()` 自动借用接收者,按值 self 从元素调用会报 cannot move;跨模块要 `pub fn`(`private method` 报错)。实现:函数表键 `P.norm`(链接后 `模块:P.norm`),`Parser::function`/`self_param`/`method`;检查器与生成器的 `call_target` 经 `compiler::record_method` + `method_args`(`Receiver::Path/Element/Temp`)改写成普通调用,`?`/`catch` 也走它;临时结构体可借给 `&T` 参数;`needed` 按方法名后缀带上所有同名方法的原型。
- 结构体:`struct P:` 下面每行缩进一个字段 `name: string`;字面量 `P(name: "a", age: 3)`(**逗号分隔,每个字段都写名字**,可分行);没有字段时 `struct P: pass`、`P()`;跨模块 `model.P(...)`。字段读写 `p.name`、连续 `a.b.c`;支持整体移动、整体重新赋值、可变字段赋值;字符串字段自动释放。
- 控制:`if c:`/`elif c:`/`else:`(**没有 `else if`**,写了报错提示 `elif`)、`while c:`、`for i in start..end:`、`for x in &list:`、`break`、`continue`;条件必须是 `bool`,条件里写单个 `=` 报“比较要用 ==”。`for` 区间左闭右开,`start`/`end` 进入循环前各求值一次,循环变量只读且不能与已有变量同名;定长数组按下标写 `for i in 0..N:`。单行分支后面的 `elif`/`else` 可以另起一行,也可以接在同一行。
- 表达式:`+ - * / // % **`、`== != < <= > >=`、`in`/`not in`、逻辑 **`and`、`or`、`not`**(Python 优先级,从低到高 or < and < not < 比较;`not a == b` 是 `not (a == b)`,`not` 不能直接跟在比较/算术运算符右边;and/or 短路;三个词是保留字),写 `&&`、`||`、前缀 `!` 会报错并提示单词;`!` 只用于 `T!string`、`-> !string`;一元 `-expr`、括号;位运算 `& | ^ ~ << >>`(只用于 i64,Python 优先级:比较 < `|` < `^` < `&` < 移位 < 加减 < 乘除;中缀 `&` 是按位与、前缀 `&` 仍是借用;移位量 0–63 否则终止,左移按无符号不报溢出,右移是算术右移;C 侧 `zam_shl`/`zam_shr`);进制字面量 `0xFF`/`0b1010`/`0o17`(词法层转成十进制记号);复合赋值 `+= -= *= /= //= %= &= |= ^= <<= >>=`(`Parser::compound` 识别;变量、字段、元素;元素下标里不能有调用);if 表达式 `let v = if c: a elif d: b else: e` 或 Python 的 `a if c else b`(`Parser::expr` 末尾识别,解析成 `Expr::If`)(必须有 else,每个分支一个表达式);**除法照 Python**:`/` 永远得 f64(整数两边 promote),`//` 向下取整(C 侧 `zam_floordiv`,正常量除数用 `zam_floordiv_k`,区间分析证明两边非负时直接 `(uint64_t)a / b`),`%` 符号跟除数(`zam_mod`/`zam_mod_k`),`**` 解析成内建 `$pow` → `std/core:pow`/`pow_f64`(右结合,比乘除紧);`x in xs`/`x not in xs` 由 `compiler::membership` 在检查器/生成器入口改写成 `contains`(元素是字符串时自动借用);`True`/`False`、`f"..."` 直接接受;字符串 `a + b` 拼接(两边都不移动)与 `== != < <= > >=` 比较,字符串不能和整数相加;整数可用数字之间的单个 `_`(`1_000_000`)。溢出(含 `i64::MIN // -1`)报 `integer arithmetic error`,除以零报 `division by zero`。
- 复合值:全局 `copy(&x)` 深复制 string/列表/字典/结构体/枚举(必须写借用或传已是借用的参数;标量报 already copied by assignment);结构体、枚举、列表、字典支持 `==`/`!=`(字典与顺序无关),`<` 报 only support == and !=;`println(x)` 与插值显示 `Point(x: 1, y: 2)`、`Shape.Rect(2.0, 3.0)`、`["a"]`、`{"k": 1}`(内部字符串加引号转义)。三者都只读。实现:`compiler::COPY`(`std/core:copy` 不在函数表,检查器 `value_type`/`read`、生成器 `value_type`/`operand` 特判);C 函数在 `native_values.rs`(`copyX`/`eqX`/`showX`/`textX`,`Generator.values` 为真时为目标文件里全部结构体与容器发射)。
- 泛型函数:`fn largest[T](items: &[T]) -> T`、`fn swap[A, B](p: (A, B)) -> (B, A)`;调用不写类型参数,按实参推断(必须出现在参数类型里)。实现:解析时 `Parser.type_params` 让 `T` 变成特殊结构体编号 `compiler::type_param`(`$param:T`),`Function.generics` 记参数名;模板不检查不生成;`compiler::instance` 统一推断(`unify`)与替换(`substitute`,含函数体里的 let 标注),副本名 `模块:largest[Int]` 存进全局 `INSTANCES`,`linked()` 放进函数表,`Project::check` 循环到副本数不再增加;检查器与生成器的 `callee()` 都走它。含类型参数的元组不进链接表(`mentions_param`)。
- 泛型结构体与枚举:`struct Pair[A, B]:`、`enum Tree[T]:`,类型写 `Pair[i64, string]`;构造按字段/载荷推断类型实参,推断不出(`Option.None`、空列表)时用期望类型(标注、参数、返回值、元素、if/match 分支;`==` 右边不算)。内置 `Option[T]`(`Option.Some(v)`/`Option.None`,链接名 `compiler::OPTION` = `std/core:Option`,模块自己声明 `Option` 时优先)。泛型类型里的方法是泛型函数(`Parser.owner_generics` 让 self 成为 `Owner[T]`),`record_method` 按接收者实例生成副本 `Stack.push[Int]`;方法自己还能再带类型参数(`fn map[U](&self, f: fn(T) -> U) -> U`),这时 `record_method_with` 把调用点的实参类型一并传给 `instance`,`U` 从实参推断(推不出来报 `cannot infer U`)。生成器只在模板确实带了自己的类型参数时才去估实参类型,且跳过匿名函数实参。实现:类型位置的 `Name[args]` 由 `Parser::applied` 登记进 `APPLICATIONS`,`project.rs` 的 `resolve_record` 限定模板名与实参后换成 `compiler::struct_instance`(实例名 `模板[实参Debug]`,编号是名字哈希);`linked()` 用 `substitute_fields` 生成实例结构体,含类型参数的实例不链接;检查器 `Checker::constructor` 与生成器都走 `compiler::constructed` 推断,生成器靠 `Generator.hint`(表达式地址 + 目标类型,由 `expr_as` 设置)拿期望类型;`Project::check` 的循环也看 `struct_instance_count`。泛型副本里 `copy(v)` 对数字/布尔也成立(`Checker.generic`)。打印时实例只显示模板名。
- 函数值与匿名函数:类型 `fn(i64, &string) -> bool`(`Type::Func(编号)`,签名表 `compiler::func_sig`;参数位置的函数类型是 `Type::FuncRef`,按借用对待:只能调用或转传,`is_reference()` 为真,所以存下来、返回都报 borrow escape)。值:具名函数 `double`/`util.double`(`project.rs` 把非局部名改写成 `Expr::FnRef`),匿名函数 `fn(x: i64) -> i64: x * 2`(`Expr::Lambda(参数, 返回类型, 一条语句的 Block)`,函数体是一个表达式或一条 `println`;参数/返回类型可省略,按期望类型补;泛型调用里先用其他实参推断类型参数再补全,见 `compiler::instance_with` 的 `late`)。调用 `f(x)` 是 `Expr::Apply(路径, 实参)`,结构体函数字段 `p.on_click(x)` 由检查器/生成器的 `field_call` 识别。闭包:`expr_roots` 找出函数体用到的外层局部变量,按 `captured_view` 只读(owned → 共享借用),捕获的变量登记成本次调用的共享借用;捕获了变量的匿名函数只能当 `FuncRef` 实参。C 侧:`struct F<id> { R (*call)(void *env, ...); void *env; }`,匿名函数提成静态函数 `<所在函数符号>_fn<N>`(`Generator.lifted`),环境结构体放调用方栈上存变量地址;具名函数当值时生成 `<符号>_value` 包装。函数值 `copied()` 为真(按值复制、不释放)。泛型函数、会失败的函数暂不能当值。
- B7 闭包补全:函数体里的 `fn name(...) -> T:` 解析成 `Let(name, false, None, Lambda(参数, Some(T), 完整代码块))`(`lambda_body()` 对单条 Call/Print 返回 Some,完整代码块返回 None,检查器/生成器按 `self.block` 处理)。捕获:`block_roots` 找出用到的外层名;外层可写(let mut 或 &mut)时视图是可变借用(`captured_view(ty, mutable)`,生成器一律用可写视图,ctype 相同);检查期间 `thawed()` 把写过的根记进 `Checker.written`,`lambda()` 返回 `(名字, 是否写)`,并把函数体里调用的闭包的捕获并进来(传递)。`let f = 捕获了变量的 fn` 绑成 `FuncRef`,捕获记在 `Checker.closures`;每次 `Apply(f)` 或把 `f` 当实参转传时由 `capture_loans` 检查捕获仍有效、写的要 thawed,并按读/写登记共享/可变借用(报错带“the fn f uses/changes it”)。局部 fn 不能 mut、不能泛型、不能失败(ZM200),但可以递归:解析时 `declared` 把函数里定义的 fn 名字记进 `locals`,函数体里的 `f(...)` 因此改写成 `Expr::Apply`;检查器在 `lambda_named` 里把“这是我自己”的签名放进 `inner`(要写在参数表之外,`block` 退出时会清掉块内声明),生成器 `lambda_named` 记 `Generator.own`,递归调用直接生成对该静态函数的调用(环境指针原样传下去),不经过局部变量。互递归(前向引用)仍不支持。函数元素 `Elem::Func(id)`(`copyable()` 让 get/values 可用,不 comparable);`inner_container` 解析 `[fn(...) -> T]`。`==`/`!=`:`value_type` 一边失败时取另一边的复合类型,生成器 `known_type`/`operand_as` 按它构造。泛型函数当值:`compiler::generic_value(functions, name, 期望函数类型)` 用期望参数推断副本,检查器 FnRef 分支与生成器 `expr_as(FnRef, Func)` 共用,`needed` 对泛型 FnRef 带上副本。顺带修了:局部变量与未导入的标准库模块同名(`let fs = ...`)时当变量用。IR-31 / native-c-46。
- 会失败的函数值:签名表是(参数, 结果, 错误),`func_type_full`/`func_error`,编号文字带 `!E`;`func_type` 是不带错误的简写。所有重建函数类型的地方(`substitute`、`project.rs` 的 `resolve_type`、生成器的 FnRef 类型)都要保留错误类型。类型语法 `fn(...) -> T!E`、`-> !E`。函数里定义的 `fn -> T!E` 把完整类型放进 `Let` 的标注,`lambda()` 的 hint 是(参数, 结果, 错误),检查函数体时 `self.error` = 期望的错误。调用:`Apply`/`Invoke`/函数字段直接调用时若会失败报“must be handled with ? or catch”;`?`/`catch` 先走 `Checker::value_call`(返回 标签, 错误, 结果),不是函数值调用再按具名函数处理。生成器:失败结果结构体改成具名 `struct F_x` 并在函数值结构体之前前置声明(函数值的 `call` 返回它,`value_result(id)`),`fallible` 列表包含会失败的函数类型;`Generator::value_call` 给出 `f.call(f.env, ...)` 文本供 Try/Catch 共用;匿名函数体按 `g.error` 生成 ok/err 包装。`steps[0](x)` 是 `Expr::Invoke(被调用的值, 实参)`,解析后经 `after_call` 接 `?`/`catch`/方法链。顺带修了 C2 引入的泄漏:catch 块走到底时要释放错误值(`returned` 为假时 drop)。IR-32 / native-c-47。
- 元组:`(i64, string)`、`(a, b)`、`t.0`(字段名就是项号,`Parser::member` 允许点号后跟数字)、`let (a, b) = expr`/`let mut (a, b)`(`Instruction::Unpack`,`_` 丢弃)。实现:元组是匿名结构体,名字 `(Int, Owned)` 由字段类型 Debug 拼成,`compiler::tuple`/`linked_tuple`/`tuple_fields` 维护全局表,`project.rs` 的 `resolve_record` 遇到 `(` 开头的名字按结构重新登记,`linked()` 把所有已限定的元组放进结构体表,`Project::check` 若检查中推断出新元组就重新链接再查;生成器 `Generator.records` 登记函数体里出现的结构体类型;打印显示 `(1, "a")`。`for x in xs:` 等同 `for x in &xs:`。
- bytes(E1,IR-35 / native-c-65):类型 `bytes` 就是 `Type::List(Elem::Byte)`(`&bytes`/`&mut bytes` 是 `ListRef`,`[bytes]`/`{string: bytes}` 经 `Parser::elem` 走 `Elem::Nested`),所以移动、释放、复制、比较、借用、结构体字段全部沿用列表。`Elem::Byte.ty()` 是 `Int`:读出来是 0–255 的 i64。C 侧存储类型由 `native::elem_ctype` 决定(`uint8_t`,列表名 `Ly`),`push/insert/put` 收 `int64_t`,存之前经 `elem_store` → `zam_byte()`(`LIST_PRELUDE`,越界报 `byte must be in range(0, 256)`);列表元素赋值 `b[i] = v`、`b[i] += 1` 也经 `elem_store`。`for x in &mut b` 在 `each_bindings` 拒绝(别名指针无法检查范围)。`index_of/contains` 的参数是 int64_t(300 不会截断成 44)。字面量:词法在 `"` 前看到紧贴的 `b` 时把它换成 `Word("$bytes")`,只允许 ASCII 与转义(字符串转义新增 `\xNN`、`\0`,与 Python 相同),解析成 `Call("$bytes", [Text(十六进制)])`,`project.rs` 改名 `std/core:bytes_literal`。方法:`compiler::bytes_method`——列表方法优先,否则找 `std/bytes:<名>` 且首参是 `&bytes` 的函数(`decode`、`hex`);检查器/生成器的 `string_call` 先试 `bytes_call`,`call_target` 也先试 `string_call`(所以 `text.decode()?`、`s.parse_i64()?` 能接 `?`/`catch`),`needed` 带上 `std/bytes:<方法>`。打印 `b"..."`(`native_values::zam_show_bytes`)。函数:`std/string:encode`、`std/bytes:decode/hex/from_hex/zeros`、`std/fs:read_bytes/write_bytes/append_bytes`。测试 `tests/bytes.rs`,示例 `examples/values/bytes.zm`。
- 默认参数与关键字实参(E2,IR-37 / native-c-67):`Function.defaults`(与 params 一一对应,没有默认值时是空表);解析时 `Parser::signature` 遇到 `= 值` 经 `default_value` 检查(`fold` 常量折叠,或 `Option.None`),后面的参数也要有默认值;`Parser.constants` 让默认值能用前面的 const。调用处 `name=value` 解析成 `Call("$kw:name", [值])`(`Parser::arguments`,`==` 不算)。归位在 `compiler::bind_args`:普通调用在 `project.rs::resolve_expr` 解析出被调函数后由 `bind_call` 做(默认值在被调函数的模块里解析,所以私有常量也能当默认值),方法在 `method_args` 里做(检查器与生成器共用,跳过 self);没有关键字实参且个数够时原样返回,个数不对留给检查器报。函数值、标准库函数、枚举变体不接受关键字实参。`check()` 开头核对默认值类型。结构体字面量也接受 `Point(x=1, y=2)`。
- 集合(E2):类型 `{K}`(K 是 i64/string)就是 `Map(K, compiler::SET)`,`SET` = `Elem::Byte`(用户写不出值为 byte 的字典,所以这个组合只代表集合;`is_set`)。字面量 `{1, 2}` 是 `Expr::SetLit`(解析器看第一项后面是不是冒号),空集合 `{}` 靠类型标注。方法在 `method_signature` 的 `Map(key, SET)` 分支:add/remove/discard/contains/clear/union/intersection/difference/issubset/sorted,C 辅助在 `SET_TEMPLATE`(只为值是 SET 的字典生成)。下标与 `for k, v` 报错,打印 `{1, 2}`/`set()`(`native_values::map`)。字典、集合字面量后面可以接方法链。
- 用 Zamak 写的标准库(E3,native-c-68):`zam/std/*.zm` 经 `project.rs::STD_SOURCES`(`include_str!`)编进编译器;`add_std_sources` 在模块扫描后把被 `use` 的这些模块加进来(反复补到不再新增,标准库模块之间也能互相 `use`),模块 id 就是 `std/json`,`Input.package = Some("std")`,路径显示成 `<std/json>.zm`,所以 `zam test`、`zam doc` 跳过它们。`resolve()` 里 `std/` 目标只有在 `modules` 里没有时才当 C 内建。C 实现的内部模块以下划线开头(`std/_process`、`std/_re`),要加进 `check_with` 的白名单。生成器收集方法原型时只跳过 `function.builtin`(以前跳过所有 `std/`,会漏掉标准库里会失败的方法的结果结构体)。语言服务器的 `module_source` 也认 `project::std_source`(补全、悬停;跳转定义只对真实文件)。模块级 C 辅助代码 `MODULE_PRELUDES`(hashlib、base64、_process、_re)只在用到该模块的函数时拼进目标文件。
- std/hashlib、std/base64:C 实现(SHA-256、SHA-1、MD5、HMAC-SHA256,补位按 `zam_hash_byte`),测试对照 Python 的 hashlib/hmac/base64。std/process:`_process.run(args, input, capture) -> [code, stdout, stderr]`,输出经临时文件(不用管道,避免两个管道都写满时互相等待);Windows 用手写声明的 CreateProcessW(参数按 CommandLineToArgvW 规则加引号,有 `_WINDOWS_` 时用系统声明),POSIX 用 posix_spawnp + mkstemp;process.zm 包出 `Completed`、`run`、`call`、`check_output`。std/json:纯 Zamak(`Reader` 结构体在 bytes 上逐字节解析,数字按 JSON 语法严格扫描,`\u` 代理对合成)。std/re:C 的 Pike VM(`REGEX_PRELUDE`:解析成节点数组 → 编译成指令 → 线程以“待展开”形式进入下一轮,所以断言知道当前字符;没有线程时继续往后扫;空匹配规则同 Python 3.7;16 项编译缓存),`_re.groups`/`_re.find` 返回组名与字符位置,re.zm 包出 Python 的全部入口。已知差异:`(a*)*b` 这类可空组外再套星号时,最后一次空迭代的捕获与回溯引擎不同(整体匹配相同)。
- 字符串字面量:`\xNN`、`\uXXXX`、`\UXXXXXXXX`、`\0`;`r"..."` 原始字符串(词法把紧贴的 `r` 换成 `Word("$raw")`,解析成不插值的 `Expr::Text`);`"""..."""` 三引号(可含 `"`,目前不能跨行)。
- 接口(E4,IR-38 / native-c-69):`interface Shape:` 解析成 `Struct { fields: [], interface: true }`,每个方法登记成函数 `Shape.area`,`Function.dispatch = true`、没有函数体(检查器跳过它)。约束 `fn f[T: Shape]` 存在 `Function.bounds`(链接时与参数一起 `resolve_type`),`instance_with` 推断出类型实参后用 `compiler::implements(functions, 实参, 接口)` 检查:具体类型要有每个同名方法(`method_owner` 从函数名拆出拥有者),self 的可变性兼容,其余参数、结果、错误类型相同;报错用 `RECORD_NAMES`(`check()` 开头登记)显示类型名。接口值:C 侧 `struct R<id> { void *data; const struct V<id> *vt; }`,虚表 `V<id>`(drop、show,然后按方法名排序的 `m_<方法>`,第一个参数是数据指针;在结构体定义之后、drop 原型之前发射,函数指针的返回类型可以是还没定义的结果结构体)。分派函数体 `p0->vt->m_x(p0->data, ...)`。装箱:检查器 `expr()` 开头遇到“期望接口、给具体类型”按具体类型检查(不满足时直接报 implements 的错误);生成器 `expr_as` 开头生成 `box<接口>_<具体>(值)`,把(接口, 具体)记进 `Generator.boxes`,模块汇总后在原型之后生成转发函数、虚表常量与装箱函数(`box_definition`)。为了转发函数能调用具体方法,`needed` 在程序里有接口时带上所有与分派方法同名的方法。接口值的 drop 经虚表释放并 free,show 经虚表,eq 比较数据指针,copy 在检查器里拒绝(`holds_interface`),构造 `Shape()` 报错。
- 网络(E5,native-c-71):`zam/std/net.zm` 用 `extern type Socket: "void", close = zam_net_close` 包住套接字(句柄里存“套接字 + 1”),C 部分 `NET_PRELUDE` 经 `UNIT_PRELUDES` 只拼进 `std/net` 这个模块的目标文件(extern fn 在那里定义;Windows 手写 Winsock 声明,有 `_WINSOCK2API_` 时用系统的,addrinfo 按各平台的字段顺序取)。连接、监听、接受用输出参数 `&mut Option[Socket]` 直接写进结构体字段(match 取出的是借用,从 Option 里移不出来)。Windows 链接时总是带上 ws2_32(没用到时产物不变)。POSIX 上忽略 SIGPIPE。`zam/std/http.zm` 纯 Zamak:URL 解析(含 IPv6)、HTTP/1.1 请求(Connection: close,读到对端关闭)、响应解析(Content-Length、chunked、同名头合并),服务端 `serve`/`serve_one`/`respond`/`respond_json`。测试:单线程里先 connect、写请求,再 serve_one(数据进了内核缓冲区);客户端用 Rust 线程起服务器。HTTPS:`zam/vendor/mbedtls` 是 Mbed TLS 3.6.7 的 library/ 与 include/(未改动,版本与 sha256 见其 README),`build.rs` 打包压缩后 `include_bytes!` 进 zam;程序导入 std/net 时 `mbedtls_sources()` 解压到 `缓存/vendor/mbedtls-<哈希>`(先写临时目录再改名,`.complete` 标记),作为一个 `[native]` 条目编译(Windows 链 bcrypt、crypt32)。C 胶水 `TLS_PRELUDE` 与 NET_PRELUDE 一起拼进 std/net:客户端校验证书与主机名(SNI 同名),根证书依次取 `SSL_CERT_FILE`、Windows 的 ROOT/CA 证书库(手写 Cert* 声明)、Linux/macOS 常见的证书文件;对方不发 close_notify 直接断开也算正常结束。`Conn` 的 `tls` 字段写在 `socket` 前面,析构时先发 close_notify 再关套接字。`[native]` 源码用工作队列并行编译(mbedTLS 冷编译约 13 秒);头文件目录在 MSVC 上用 `/d1trimfile:` 裁掉(`/d2pathmap` 管不到 `__FILE__`),GCC/Clang 用 `-ffile-prefix-map=<目录>=I<n>`,否则换个缓存目录,同一个键会得到不同字节。测试:`tests/fixtures/tls` 有一张自签证书(localhost 与 127.0.0.1,100 年),服务端与客户端各起一个 Zamak 进程,共用缓存。顺带:括号表达式后面也能接方法调用(`(a + b).upper()`)。
- 线程(E6,native-c-72):`zam/std/thread.zm` 写 Zamak 那层(`Thread`、`Task[R]`、`Queue[T]` 都是包着 `extern type` 句柄的结构体,`join` 与队列方法用 `&self`,状态记在 C 里),C 部分 `THREAD_PRELUDE` 只拼进 std/thread 的目标文件(Windows:`_beginthreadex`、SRW 锁、条件变量,有 `_WINDOWS_` 时用系统的类型;其他平台 pthread,链接时加 `-lpthread`)。`thread.spawn(work, a, b)` 在检查之前由 `compiler::rewrite_block`(`linked()` 里对每个函数执行,生成泛型副本时也执行)改写成调用合成的 `std/thread:spawn[函数类型编号]`:参数 `(f: fn值, a0, a1...)`,返回 `Thread` 或 `Task[R]`,`builtin: true`;实参检查、移动都走普通调用。第一个实参必须是函数名(`FnRef`),参数不能是借用,错误只能是字符串。生成器的 `intrinsic()` 给这个合成函数与 `_queue_new/_queue_put/_queue_get/_task_take`(泛型占位函数,按 T 的 C 大小与释放函数生成)提供函数体;线程入口把参数打包进堆上的 job,返回值复制到堆上等 join 取走,句柄析构时等线程结束并释放没取走的返回值。同一个 `rewrite_block` 还做「按标注推断」:`let x: Box[i64] = f()`,f 的某个类型参数不出现在参数里时用标注推断(`instance_full` 的 `expected`)。运行时:`ZAM_THREAD` 宏(`__declspec(thread)`/`_Thread_local`);报错缓冲区、随机数状态线程局部;全局 `zam_threaded` 在第一个线程启动前置 1(只写一次,TSan 会抓重复写),之后列表空闲链表与正则缓存停用(正则现编现用,`zam_rx_done` 释放);`print` 在多线程时锁 stdout 输出整行。Mbed TLS:解压时生成 `zamconfig/mbedtls/mbedtls_config.h`(包含原版配置再打开 `MBEDTLS_THREADING_C`,Windows 用 `THREADING_ALT` + SRW 锁,其他 pthread)与 `zamconfig/threading_alt.h`,include 目录里 zamconfig 排第一;TLS 胶水的一次性准备(`mbedtls_threading_set_alt` + `psa_crypto_init` + 先调一次 `mbedtls_ssl_list_ciphersuites`,它第一次调用时无锁地填静态表)用 pthread_once / Interlocked 自旋;覆盖配置还关掉了 `MBEDTLS_SELF_TEST`(ecp.c 的 mul_count 计数器无锁)。`zam_net_ready` 的一次性标志用原子读写。验证:WSL 里用 `gcc -fsanitize=thread` 的包装脚本做 `CC`,先 `zam build` 再 `setarch $(uname -m) -R ./程序` 运行(不关地址随机化 TSan 报 unexpected memory mapping)。测试:`tests/threads.rs`;`tests/net.rs` 的 `tls_pair` 起服务端与客户端两个进程,客户端没连上时主动连一下让服务端退出(只杀 `zam run` 杀不掉它的子进程)。
- 窗口程序(E7,native-c-73):`zam/vendor/libui-ng` 是 libui-ng 43ba1ef 的库源码(只收 meson.build 编译的文件,去掉 OLD_table),`build.rs` 打包成 libui.bin,导入 std/ui 时 `libui_native()` 解压并选本平台的源码:Windows 是 C++(MSVC,`/EHsc /std:c++17 /bigobj`,链 14 个系统库,`/MANIFEST:EMBED /MANIFESTINPUT:windows/libui.manifest` 打开 Common Controls 6;MinGW 暂不支持),macOS 是 Objective-C(Foundation、AppKit、-lobjc),Linux 用 `pkg-config gtk+-3.0`。都定义 `_UI_STATIC`。`[native]` 因此支持 .cpp/.m(按扩展名选语言、C++ 运行库自动链接)与内部的 flags/link_args。`zam/std/ui.zm`:控件是 `{ id: i64 }` 的纯数据结构体(可复制),满足接口 `Control`(`handle()`),会变化的控件还满足 `Changing`(`change_kind()`),布局的 `add[C: Control]` 是泛型方法。`App[T]` 先登记回调(`kept fn(&mut T)`),`run(window, state: T) -> T` 由生成器的 `_run[T]` 把状态留在栈帧里、指针交给 C;`_connect[T]` 按 T 生成调用器 `F{id}.call(env, (R*)state)`。C 胶水 `UI_PRELUDE`:句柄是 intptr,libui 返回的文字复制后 uiFreeText,构造控件前 `zam_ui_need()` 初始化(失败时打印原因退出)。窗口关闭或 `ui.quit()` 都会结束 `uiMain` 并销毁窗口。语言改动:`kept fn` 参数是可存的 `Func`;纯数据结构体/枚举/元组(只含数字、布尔、函数值)按值复制(`compiler::note_plain` 在链接时算,`copied()` 包含它们;生成器读变量时不再 `take`);非泛型结构体的泛型方法也实例化(`record_method_with`),生成器推断方法类型参数时跳过关键字实参、看所有同名方法。测试 `tests/ui.rs`:没有图形环境或 GTK 时跳过;CI 的 Linux 装 libgtk-3-dev 与 xvfb,用 `xvfb-run` 跑测试。截图:起程序后用进程的 MainWindowHandle 取窗口位置再 CopyFromScreen(`FindWindow` 在 PowerShell 里传 `$null` 会变成空字符串)。
- 实战找漏洞(2026-10-23 起,examples/apps/ 下的 linkcheck、todo,端到端测试在 tests/apps.rs):读字符串元素/字段得到副本(检查器放行 Owned 期望,生成器 `copy(...)`);`s.find(sub, start)` 映射到 find_from(生成器的 needed 要一并登记);插值里能写字符串字面量(词法分析在 `{...}` 里跟踪层数,原样保留嵌套字符串;插值解析跳过引号里的括号;只认括号外的冒号为格式说明);http 默认跟随重定向、`http.urljoin`(与 Python 逐项对照);调用结果后的 `[i]`/`[a:b]`/`.field` 在 `after_call` 里记成 `Chain(.., "$at"|"slice"|"$field.名", ..)`(字符串的 `$at` 经 string_alias 变成 char_at,`call_field()` 识别字段);`ElementMethod` 与结构体字面量也走 `after_call`;catch 变量可以遮住外层同名变量(检查器在每个出口恢复,生成器本来就恢复);以 return/fail/break/continue 结束的 catch 块恢复块前的 live 状态;借来的纯数据值在要值的地方自动解引用复制(检查器的 Record 移动分支 + `fallback_fits` + 生成器 `expr_as` 开头);模块解析递归进 `Formatted`。std/datetime 纯 Zamak(Hinnant 的 civil 算法,`time.local_offset` 是新加的内建函数),常量不能是列表,名字表写成函数。
- 任何类型与 JSON 互转(native-c-73 之后):json.zm 里 `to_json[T]`/`from_json[T]` 的函数体只是占位,`compiler::derive_json` 在 `instance_full` 生成实例时按 T 合成 Zamak AST 函数体(结构体按 `STRUCT_TABLE` 快照的字段,`note_plain` 时存下;列表、`{string|i64: V}`、Option、枚举分支),先登记实例再 `rewrite_block`,递归类型能终止。所以 rewrite 必须在 `linked()` 末尾、`note_plain` 之后跑。解码错误路径靠每层 `catch e: fail ...` 在前面加上位置(`read` 闭包:内层错误以 `[` 或 `.` 开头就直接接上,否则接 `: `;缺必需字段报 `.f: missing`),`decode` 去掉开头的 `.`。不经用户写的宏,派生是编译器内置的。从自己拥有的局部结构体移出列表/字典/结构体字段:检查器把整个结构体标成 dead,生成器 `take{R}(&字段)` 清零原位置并登记 owner。`catch e: []`/`{}` 经 `empty_fallback` 按成功类型。
- `Json` 的数字分成 `Int(i64)` 与 `Float(f64)`(native-c-74,同 Python):`Reader.number` 没有小数点和指数时 `parse_i64`,超出 i64 报错;`dumps` 的 Float 总带小数点(`str(n)`),所以类型往返不变。派生里 i64 ↔ `Int`、f64 ↔ `Float`;`as_f64` 也收 Int,`as_i64` 只收 Int。
- std/sqlite(native-c-74):`zam/vendor/sqlite` 是 3.54.1 合并源码(只留 sqlite3.c/h),build.rs 打成 sqlite.bin;用到 std/sqlite 时 `sqlite_native()` 解压并只编译 zam 补的 `zamconfig/zam_sqlite.c`(定义 `SQLITE_OMIT_LOAD_EXTENSION` 后 `#include "../sqlite3.c"`,不需要 libdl;非 Windows 链 pthread、m)。C 胶水 `SQLITE_PRELUDE` 经 UNIT_PRELUDES 只进 std/sqlite 的目标文件:`Db`/`Stmt` 是 extern type,打开时 `sqlite3_busy_timeout(5000)`,prepare 拒绝多条语句,失败原因放在线程局部的 `zam_sqlite_failure`。sqlite.zm:`query[T]` = `query_json` + `json.from_json`(错误里的 "the JSON value" 换成 "the value"),`execute_named[T]` = `json.to_json` 后按 `sqlite3_bind_parameter_name` 取字段;INTEGER 列读成 `Json.Int`,声明类型以 BOOL 开头的列读成 Bool,列表/对象参数报错。参数助手叫 `integer/real/text/null`(不叫 `int`:模块里的同名函数会遮住内建的 `int(x)`)。
- 按标注推断方法的类型参数:`rewrite_let` 遇到 `let x: T = recv.m(args)` 时由 `annotated_method` 按方法名找(程序里只有一个非泛型结构体有这个名字、带隐藏类型参数的 `&self` 方法才算),改写成直接调用实例 `Owner.m[T](recv, args)`,接收者靠 `&` 参数的自动借用。生成器 `call_target` 判断“方法自己有类型参数”时只和**所属结构体**的类型参数比(以前和模块里所有结构体比,`Option[T]` 的 T 让 `put[T]` 被当成没有自己的类型参数,`s.put(x)?` 在生成时崩溃)。列表的 `append` 是 `push` 的别名(`method_signature` 与 `method_call` 两处)。
- match 语句的拥有式绑定:被匹配的不是位置(变量、字段、容器元素,见检查器 `scrutinee` 的 root 与生成器 `place_scrutinee`)时,`arm_bindings(.., owned=true)` 让绑定拿载荷本身(可变);生成器在 `bind` 里 `take{T}(&handle->mN)` 并登记 owner,分支没有 return/break 时在分支末尾释放。match 表达式(`let x = match ...`)仍是借用。
- 并发 http.serve:主线程 accept,连接经 `thread.Queue[net.Conn]` 交给 `work` 线程(`workers`,默认 max(8, 2×CPU)),HTTPS 握手在工作线程里做(`Conn.accept_tls`);handler 是 `kept fn`(`spawn` 不收借用的函数值)。`answer(conn, handler)` 是一次请求的处理,`serve_one` 也用它。`Request.args/arg`、`parse_query`、`quote`、`unquote`(与 urllib.parse 对照),`respond_json[T]` 用 `json.encode`。
- 线程安全:运行时 C 代码不能用 localtime/gmtime/strtok 这类共用静态缓冲区的函数(`zam_local_offset` 用 localtime_r / localtime_s)。TSan 压测(WSL 里 `CC=$HOME/tsancc` 编译 examples/apps/notes、并发发请求、数 `WARNING: ThreadSanitizer`)能抓到这类问题。编译器身份(缓存键的一部分)是「命令 + 换行 + `--version` 输出」,所以 `CC=tsancc` 这样的包装与 gcc 不共用缓存;空的 `CC` 当作没设。
- ui.table:C 里的 `zam_ui_table_data`(`uiTableModelHandler` 放第一个字段,模型回调拿到的指针就是整个结构体)存一份行 × 列的字符串,跟程序一样长寿;Zamak 的 `Table` 有控件句柄 `id` 与数据句柄 `data`。`set_rows` 先查每行格数,再 `set_count`(从末尾通知删除 / 逐行通知插入)、逐格 `set_cell`、逐行 `changed`。选中变化走 `ZAM_UI_SELECT`(`uiTableOnSelectionChanged`,`on_change` 传的是控件句柄);`set_selected` 不触发回调。
- 画布(ui.canvas):C 里 `zam_ui_canvas_data`(`uiAreaHandler` 放第一个字段)记着 draw/mouse/key 三个回调;画的时候栈上有一支 `zam_ui_paint`(上下文、宽高、当前路径),Zamak 的 `Painter`/`Mouse`/`Key` 都是只有一个 i64 字段(C 指针)的纯数据结构体,只在回调期间有效。回调经泛型 intrinsic `std/ui:_connect_event[T, E]` 生成 `{symbol}_event(f, state, arg)`:把 arg 放进 `R{E}.m0` 再以 `&E` 交给函数值(`zam_ui_handler.event`)。路径 fill/stroke 后保留,可接着描边;`begin` 换新路径;画完一帧释放。文字:`zam_ui_layout` 用缓存的系统字体(`uiLoadControlFont` 在 Windows 上每次重扫字体集合,约 26 ms/次,缓存后快 35 倍)。std/widgets 纯 Zamak,立即模式:组件是 `draw_xxx(p, rect, 状态.., t: &Theme)` 函数,状态在用户结构体里,点击用 `Rect.contains`。表格的程序改动(set_rows/set_selected)用 `zam_ui_table_quiet` 屏蔽选中回调(libui 在 GTK 上会发、Windows 不会)。
- 列表/字典常量:`fold` 接受 Array/MapLit;用处换成字面量(`constant_use` 允许借用列表/字典常量)。`for n in NAMES` 在 resolve_body 里插入隐藏的 `$for_const_行_列` 局部变量;`NAMES[i]`、`NAMES.m()`、`Call("NAMES.m")`、`GRID[i][j]`(ElementMethod)在 resolve_expr 里改写成对字面量的 `Chain`。多行 catch 块以单独的值(不是调用)结尾时,`catch_tail` + 检查器/生成器的 `block_tail`:前面几行执行完、块里的变量释放前求值写进结果。列表 `pop(i)` 在 rewrite_block 里改名 `$pop_at`(record_method_with 对结构体映射回 pop),`remove(x)` 是按值删(`removeval`,没有时运行时错误)。
- std/cli(纯 Zamak):`cli.parse[T](args)` 先用 `json.shape(none)`(编译器派生 `derive_shape`:结构体是 `{"字段": "种类"}`,种类 bool/int/float/string/list:x/option:x/struct/other,`shape_kind` 递归)知道每个字段怎么读,按种类把 args 读成 Json 对象,再交给 `json.from_json[T]`(错误 `.f: missing` 换成 `--f is required`)。`parse_or_exit` 的 catch 块以 `env.exit(2)` 结尾(检查器认 env.exit 不返回)。`env.args()` 不含程序名,`env.program()`(新 builtin,Windows 读 `__argv[0]`)去掉目录、.exe 和 zam 构建名后面的 `-<64 位哈希>`。类型参数只出现在返回类型、又没有实参能带出 T 的函数(如曾经的 `usage[T](program)`)写不出来:调用处没法显式写类型实参。
- 可重现构建:MSVC 编译加 `/cgthreads1`(2026-10-24)。默认多线程代码生成在负载高时会对同一份 C 选不同指令(如 `mov r15d, r12d` 与 `mov r15, r12`),`/Brepro` 只管时间戳,管不到这个;并发构建写同一个缓存键时就报 corrupt cache entry。排查办法:每个进程用独立的 `ZAMAK_CACHE_DIR` 并发构建同一程序,比较各缓存里 .obj 的内容哈希(别把路径算进去),再用 `ZAMAK_DUMP_C=目录` 确认 C 源码是否相同。编译器身份另有缓存(临时目录 `zam-tool-*.txt`,按可执行文件的路径、大小、修改时间失效),什么都没改时的构建因此从约 100 ms 降到约 4 ms。
- 字符串方法按实参个数选 builtin 由 `compiler::string_function` 统一决定(`find`→find_from,`ljust/rjust/center` 两个实参→`*_with`),检查器与生成器收集 needed 共用;`zam_pad_with` 的填充必须正好一个字符,否则运行时报错(同 Python 的 TypeError)。
- 检查器的 `Expr::Catch` 在期望只读借用(`&string` 参数)时先按拥有的类型检查(`int(env.var("P") catch e: "8")`)。
- 字符字面量 `'a'`、`'#'`、`'\n'`、`'\''`:值是 Unicode 码点(`i64`),与 `string.byte_at` 比较只用 ASCII 字符。
- 字符串:UTF-8,转义 `\n \r \t \" \\`,插值 `"{name}"` / `"{p.name}"` / `"{a + b}"` / `"{xs[i]}"` / `"{f(x)}"`(变量、字段、元素只读不移动;花括号里不能有字符串字面量或花括号),字面大括号写 `{{` / `}}`。冒号后面可以写格式说明 `{x:.2}` / `{x:>5}` / `{x:*^7}` / `{n:03}`:解析在 `compiler.rs::literal`(按第一个 `:` 拆出说明,表达式那部分装进 `Expr::Formatted`,非简单路径的再由 `Parser::literal` 按表达式解析并装回;`{{}}` 优先于说明),规则在 `compiler_spec.rs`(`[[填充]对齐][宽度][.小数位]`,精度只给 f64,宽度上限 1024、精度上限 17),C 侧 `zam_format`(`S` 是值类型不是指针;`S.data` 不带结尾 `\0`,解析数字前要先抄进缓冲区;`cat` 会吃掉两个参数,别再 `drop`)——报错按 ZM110 归类并给统一建议。
- 注释:只有 `#`(与 Python 相同);`//` 是整除,写在行首时词法报 `comments start with #`(ZM100);**没有块注释**。
- 模块:`use greeting`(同目录 `greeting.zm`,只能调用其 `pub fn`)、`use std/string`、`use std/io`、`use std/fmt`、`use std/fs`、`use std/env`、`use std/math`、`use std/time`、`use std/random`;调用 `greeting.hello()`、`io.println("x")`;`println(...)` 无需导入。
- 所有权:字符串赋值/传参是**移动**,移动后再用报错;借用作为参数时调用返回即结束;**只读借用参数自动借用**:实参是变量/字段/元素时检查器与生成器经 `compiler::auto_borrow` 改写成 `Borrow`/`BorrowIndex`(`show(name)` = `show(&name)`;`copy(x)` 在 `project.rs` 改写),`&mut` 必须显式写(漏写报 ZM425 并提示 `write &mut x`);共享借用可多个,可变借用必须独占且要求可变所有者;借用参数不能移动或返回。借用局部 `let x = &place` / `&xs[i]` / `&p.f`(不带标注、不能 mut):检查器 `Instruction::Let` 里用 `local_borrow` 算出只读借用类型,把根压进 `frozen`,`block()` 结束时截回进块时的长度;生成器把指针存进 locals。带引用类型的标注仍报 borrow escape。整数/布尔是拷贝。
- 错误值:签名 `fn load(p: &string) -> i64!string`(成功 T / 失败 E)或 `-> !string`(只失败);`fail "msg"` 产生错误并立即返回;`let v = load(&p)?` 失败时向调用方传播(当前函数签名必须带相同的 `!E`);`load(&p) catch e:` 加缩进块就地处理(**不传播**,错误值只在块内可见,**值要用时块必须在所有路径离开;没有值或单独成句时可以接着往下走**(`Checker.discarded`,由 `Instruction::Call(Expr::Catch)` 设置,在 Catch 分支开头 take));`fn main() -> !string` 允许,未捕获错误写 stderr 并以退出码 1 结束。`E` 可以是 `string`、结构体或枚举:`fail ConfigError.Missing(k)`,`catch e:` 里 `match e`,`?` 只在同一种 `E` 之间传递(不同的用 catch 转换),`main -> !E` 未捕获时按 println 格式写 stderr;catch 块可以用 return、fail、break、continue 收尾。C 侧结果结构体按(T, E)命名:E 是字符串时仍叫 `F_<T>`,否则 `F_<T>_<E>`(`fallible_name(ty, error)`)。
- 列表下标免检查:`native_ranges.rs` 给整数变量的 `Fact` 记 `below`(小于某个长度不变的列表的长度)与 `len_of`(等于它的长度);`for i in range(len(xs))`、`let n = len(xs)` 后的 `range(a, n)` 让计数器带上 `below`。长度不变 = `stable_lists` 扫整个函数(含匿名函数与 catch 块)找不到 push/pop/insert/remove/clear/extend、`&mut xs`、`xs = ...`。证明按下标表达式的地址记在 `Proofs::list_index`,生成器的 `slot()` 据此输出 `(&(p)->data[i])`,否则仍走带负下标换算与越界检查的 `at`。看生成的 C:`ZAMAK_DUMP_C=<目录> zam build ...` 把每个模块的 C 写到该目录。
- 字符串位置按字符(与 Python 相同):`len`/`s.len()` → `Expr::CharLen`,`s[i]` → `compiler::char_at`(检查器/生成器的 `string_call` 入口改写,`text_operand`/`Print`/`text_value` 里按容器元素借用的分支都加了 container 守卫),`for ch in s` 由检查器按 `[string]` 绑定、生成器在 UTF-8 字节上切字符。C 的 `S` 带 `chars` 字符数缓存(0 = 未数;**改 data/len 的地方都要把 chars 清零**),`zam_chars`/`zam_byte_of`/`zam_char_of` 在 `chars == len`(全 ASCII)时 O(1)。
- 字面量方法与 Python 方法名:`"a,b".split(",")` 解析成 `Chain(Text, ..)`;`sep.join(xs)` 在 `string_method` 里换成 `string.join(&xs, &sep)`;别名表 `compiler::string_alias`(检查器、生成器与 `needed` 收集共用,新增别名只改这里)。借用的字符串变量在要 owned 的地方:检查器 `Expr::Variable if expected == Owned` 放行 Shared/Mutable,生成器用 `copy(...)` 而不是 `take`。列表字面量借给 `&[T]`:检查器 temporary_borrow 分支、生成器 `argument()` 按形参元素类型 `expr_as`。
- Python 内置函数(`project.rs::resolve_expr`,本模块自己定义了同名函数时不改写):`len(x)` → `x.len()`,`str(x)` → `Expr::Format([x])`,`sum/sorted/min/max(xs)`(单参数)→ 列表方法,`int`/`float`/`abs`/`min`/`max`(两参数)→ `std/core:*` 再经 `compiler::overload` 按第一个参数选版本(`int(f64)` → `std/core:i64`、`int(字符串)` → `std/string:parse_i64` 会失败);`for i in range(n)`/`range(a, b)` 在解析 `for` 时变成 `Instruction::For`。下标与切片照 Python:负下标在运行时 `checked_index`/`at{l}` 里加上长度;`xs[a:b]`、`xs[:b]`、`s[a:]` 解析成 `Method(path, "slice", [a, b])`(省略的结尾写 `i64::MAX`),`slice{l}` 与 `string.slice` 都经 `zam_clamp` 收拢,不报错;`insert` 位置也收拢。
- 标准库(`&string` 参数可以传变量(自动借用)、`&name`、已是借用的变量,或直接写字符串字面量/插值 `"x"`、`"{n}"`):
- `std/string`:`byte_len(&s) -> i64`、`char_len(&s) -> i64`(按 UTF-8 码点)、`equal(&a, &b) -> bool`、`parse_i64(&s) -> i64!string`(严格十进制,可带 `+`/`-`,不允许空白)、`parse_f64(&s) -> f64!string`(十进制,可带小数点与指数,不接受空白、`inf`、`nan`)、`trim(&s) -> string`、`find(&s, &needle) -> i64`(字符位置,找不到 -1;`byte_find`/`byte_find_from` 给字节位置)、`byte_at(&s, i) -> i64`、`slice(&s, start, end) -> string`(字符区间,与 Python 切片相同:负数从末尾数、越界收拢;`byte_slice` 是字节区间,切在多字节字符中间时终止程序)、`char_at(&s, i) -> string`(`s[i]`)、`find_from(&s, &needle, start) -> i64`、`starts_with`/`ends_with`/`contains(&s, &p) -> bool`、`replace(&s, &from, &to) -> string`、`lower`/`upper(&s) -> string`(仅 ASCII)、`copy(&s) -> string`、`split(&s, &sep) -> [string]`(保留空段)、`words(&s) -> [string]`、`lines(&s) -> [string]`(去 `\r`,末尾换行不产生空行)、`join(&list, &sep) -> string`、`chars(&s) -> [string]`(每个字符一项,坏字节单独成项)、`codepoints(&s) -> [i64]`(坏字节记 U+FFFD)、`from_code(n) -> string`(非法码点终止程序)、`bytes(&s) -> [i64]`、`from_bytes(&[i64]) -> string!string`(越界或非法 UTF-8 返回错误)、`repeat(&s, n)`(n<1 得空串)、`count(&s, &sub)`(不重叠;空子串得字符数+1)、`pad_left`/`pad_right(&s, width)`(按字符数补空格)。C 侧共用 `zam_utf8_next`/`zam_utf8_put`/`zam_pad`(`STD_PRELUDE`)。
- `std/io`:`print(&s)`(不换行)、`eprintln(&s)`(写 stderr)、`read_line(&mut line) -> bool!string`(去掉行尾 `\n`/`\r\n`,结束时返回 false;典型写法 `let mut line = ""` + `while io.read_line(&mut line)?:`)、`read_all() -> string!string`。
- `std/fs`:`read(&path) -> string!string`、`write(&path, &text) -> !string`(覆盖写)、`append`、`exists`/`is_file`/`is_dir -> bool`、`size -> i64!string`、`list(&dir) -> [string]!string`(按字节排序)、`mkdir`(建上级目录、已存在不算错)、`remove`(文件或空目录)、`rename`(替换已有文件,Windows 用 MoveFileExW)。C 侧 `zam_path`(Windows 转 UTF-16)、`zam_kind`(_wstat64/stat);列目录 Windows 用 _wfindfirst64,POSIX 用 opendir。`std/path`:`join/parent/name/stem/suffix`,纯文字处理,Windows 上 `\` 也算分隔符。
- `std/env`:`arg_count() -> i64`、`arg(i) -> string!string`(不含程序名,从 0 开始)、`args() -> [string]`、`var(&name) -> string!string`(Windows 走 `_wgetenv`)、`exit(code)`(0–255,越界终止;检查器与生成器经 `compiler::terminates` 把它当作代码块结束,`catch` 块不用再 `return`)。`zam run` 原样带回程序的退出码,不再打印 `program failed`。
- 全局 `abs(x)`/`min(a, b)`/`max(a, b)`:不用导入,i64 与 f64 都行(i64 `abs` 溢出终止);`project.rs` 在本模块没有同名函数时改写成 `std/core:abs` 等,检查器与生成器经 `compiler::overload` 按第一个参数类型选 `std/core:abs_f64` 等版本,IR 里只有原名。
- `std/math`(参数都是 `f64`):`sqrt`、`pow(x, y)`、`exp`、`log`(自然对数)、`log10`、`sin`、`cos`、`tan`、`atan`、`atan2(y, x)`、`floor`、`ceil`、`round`(远离零)、`trunc`、`abs`、`min(a, b)`、`max(a, b)`、`is_nan(x) -> bool`;常量 `math.pi`、`math.e`、`math.inf`(`project.rs` 解析时替换成字面量)。
- `std/fmt`:`fixed(x, digits) -> string`(digits 0–30,超出终止程序);`println` 同全局。
- `std/time`:`time() -> f64`、`monotonic() -> f64`、`sleep(seconds: f64)`(负数当 0,封顶一天)、`iso_utc(t) -> string`、`iso_local(t) -> string`(NaN/越界终止程序)。`std/random`:`seed(n)`、`random() -> f64`、`uniform(a, b)`、`randint(a, b)`(含两端)、`randrange(a, b)`(不含 b);xoshiro256**,状态 `zam_random_state[4]`/`zam_random_seeded` 定义在入口模块目标文件(普通 main 与测试 main 都定义),STD_PRELUDE 里 extern 声明;同一种子跨平台同一串数(测试钉住了值)。C 侧:RUNTIME 开头在非 Windows、非 macOS 上定义 `_POSIX_C_SOURCE 200809L`(glibc 在 -std=c11 下才给 clock_gettime/nanosleep);Windows 声明 GetSystemTimePreciseAsFileTime/QueryPerformanceCounter/Sleep,不引入 windows.h;日期用 Hinnant 的 civil/days 算法,本地偏移由 localtime 反推。
- `println` / `print` / `io.println` / `fmt.println` 打印一个值再换行(全局 `print` 由 `project.rs::resolve_body` 改写成 `Instruction::Print`,模块自己有 `fn print` 时不改写);`io.print` 不换行。
- 字符串方法:`s.upper()`、`line.trim().len()` → `compiler::string_method`(std/string 里同名、首参 `&string` 的函数;`len`/`byte_len` → `ByteLen`,`char_len` → `CharLen`),检查器/生成器的 `string_call` 在 `ty`/`expr` 开头改写,生成器 `needed` 对每个方法名带上 `std/string:<名>`。
- 临时值借给只读借用:`compiler::temporary_borrow`(`&string`/`&[T]`/`&{K:V}`/`&T` 只读)+ `produces_value`(调用、拼接、if/match、构造等),检查器 `expr()` 开头放行,生成器 `borrowed()` 兜底分支求值成临时值;`&f(x)` 在解析时等于 `f(x)`,`&mut f(x)` 报错。
- `for x in <表达式>:`(不是路径也不是区间):解析时把值存进隐藏变量 `$forN`(循环体解析完再放进 `pending`),`Each` 遍历它;`$for` 隐藏变量的值是列表字面量时按第一项定元素类型(`compiler::loop_list`)。
- 可失败函数用 `?` 时当前函数要带 `!string`,所以读写文件的 `main` 一般写成 `fn main() -> !string`;`fs.write(&p, &t)?` 可以单独成一条语句。
- 标准库实现:`zam/src/native.rs` 的 `BUILTINS` 表(签名 + C 函数体,`p0`/`p1`… 是参数)和 `STD_PRELUDE`(共用 C 辅助);`project.rs` 把 `std/*` 调用解析成 `std/<模块>:<函数>`,检查器按普通函数签名检查。新增函数只需在表里加一项,再升 `native-c-N`。列表的 C 侧是每种元素一组 `Li`/`Lf`/`Lb`/`Ls` 结构与 `push/pop/at/len/swap/insert/removeat/reverse/clear/extend/drop/take` 函数(`list_helpers`),可比较元素另有 `cmp/sort/index/has/slice`(`comparable_list_helpers`),字典是 `M<键><值>`(如 `Msi`)一组 `set/at/has/getor/del/keys/len` 与插入顺序表 + 开放寻址下标表(`MAP_TEMPLATE`)。用到哪些容器由生成器记录在 `Generator.containers`(每个 `scalar()` 都登记类型)加上签名和结构体字段,函数体先生成、容器辅助后拼在前面。容器方法签名只在 `compiler::method_signature` 一处定义,检查器与生成器共用。
- **尚未实现,不要写**:`bytes` 的 `for x in &mut b`、函数里定义的泛型 fn、三层及以上的连续下标(`a[i][j][k]`)、`xs[i].tags[j]`、字符串字面量上直接调方法(`"a,b".split(",")`)、调试器、REPL、unsafe、extern fn 的普通结构体参数、按值传的 C 结构体与回调函数、块注释、从借来的结构体移出字段。
## 常见诊断(照文本改代码)
- Python 习惯写法在报错末尾带 Zamak 写法:未知函数/变量(`len`、`range`、`str`、`int`、`float`、`sum`、`sorted`、`input`、`True`/`False`/`None`、`self`)由 `main.rs::with_python_hint` 补;语法位置(`x in s`、`a if c else b`、`xs[1:]`、`f"..."`、`**`、括号里的 `//`)由解析器 `python_hint` / `python_hint_before` 与 `lex` 补;记号在报错里经 `compiler::shown` 按源码样子显示。
- `fallible call f must be handled with ?` → 调用处漏了 `?` 或 `catch`。
- `? cannot propagate the error of f from this function` → 当前函数签名缺少 `!E`,或错误类型不同。
- `fail requires a function that declares an error type` → 在非 `!E` 函数里用了 `fail`。
- `catch block for f must return a value` → 用到了调用的值,catch 代码块要在所有路径上 `return`(或 break/continue)。
- `unsupported error type` → 错误类型要是 `string`、结构体或枚举(`ZM509`)。
- 移动后使用、借用冲突/逃逸、不可变赋值、重复/缺失字段等都在 `zam check` 阶段报错。
- `an empty list literal needs a type annotation` → 给 `[]` 加类型;`cannot move a string out of xs[..]` → 用 `&xs[i]` 或 `string.copy`;`xs is borrowed by an enclosing for loop` → 循环里改了正在遍历的列表或字典;`an empty map literal needs a type annotation` → 给 `{}` 加类型。
- `type mismatch: * mixes i64 and f64` / `x is i64 but f64 is expected` → 整数字面量写成 `2.0`,变量用 `f64(n)` 或 `i64(x)` 转换;`zam: f64 value out of i64 range` → `i64(x)` 碰到 NaN 或越界。
- 运行时(stderr,退出码 1):`zam: list index out of bounds`、`zam: pop from empty list`、`zam: key not found` 直接终止;`cannot open <path>`、`invalid integer: <text>`、`argument index out of range` 是错误值,可 `catch`;`zam: string index out of bounds`、`zam: array index out of bounds`、`zam: integer arithmetic error` 是直接终止。
- 人类可读诊断(stderr):`error[ZM301]: <信息>`,下一行 `--> <文件>:<行>:<列>`,然后是源码行和 `^` 下划线(词法/语法错误只划记号,语义错误从语句起点划到行尾;中文按 2 格对齐),最后 `= help: <建议>`;多条之间空一行,结尾 `zam: N errors`。没有位置的错误仍是一行 `zam: <信息>`。列从 1 开始、按 Unicode 字符计数。**程序化处理一律用 `--json`**,别解析这段文本。
- 实现:编译器内部错误仍是字符串 `line L, column C: 模块:函数: 信息`,多条用 `compiler::DIAGNOSTIC_SEPARATOR`(`\u{1e}`)拼接;`main.rs` 的 `diagnostics()` 拆开排序,`diagnostic()` 反解字段,`render()`/`underline()` 渲染。
- 中文报错(`zam/src/zh.rs`):编译器内部与 `--json` 始终是英文;终端显示时 `render()` 经 `zh::text()` 按句式表翻译(`%` 是原样保留的名字或类型,`{0}`… 依次对应,最具体的句式优先,整句对不上就按 `; ` 拆开,仍对不上的部分保留英文)。语言由 `ZAMAK_LANG`(`zh`/`en`)决定,不设时跟随系统语言;`zam/.cargo/config.toml` 让 `cargo test` 固定用英文,中文 Book 的 `zamak,fails` 示例在测试里用 `ZAMAK_LANG=zh` 逐字核对。**新增或改写报错句子时,在 `zh.rs` 的 `TABLE` 里同步加上中文**(`placeholders_line_up` 测试会检查占位个数)。
- 语言服务器(`zam/src/lsp.rs`,`zam lsp`):stdin/stdout 上的 LSP,`serde_json` 解析。读线程把消息送进通道,主线程一次处理完已到达的消息再检查,连续打字只检查一次。打开或修改的文档经 `project::set_overlay` 覆盖磁盘内容,检查按 `zam.toml` 所在项目分组,诊断复用 `main.rs` 的 `diagnostics()`(文字经 `zh::text`);列号换算成 UTF-16。补全、悬停、跳转按文字扫描声明(`declarations`/`locals`/`imports`),代码写到一半也能用,不依赖类型检查。`tests/lsp.rs` 用真实进程走一遍协议。VS Code 扩展(`editors/vscode/extension.js`,依赖 `vscode-languageclient`)只负责启动 `zam lsp`,`zamak.path` 可改命令;报错语言跟随 VS Code 的显示语言。
- MCP 服务器(`zam/src/mcp.rs`,`zam mcp`):stdio 上每行一条 JSON-RPC,工具 check/build/run/test/format 另起 `current_exe()` 子进程调用 zam 自己的子命令(`ZAMAK_LANG=en`,输出截到 64 KB,超时结束),所以结果与命令行一致、编译器内部出意外也不拖垮服务器;`explain` 读编进来的 `book/en/src/appendix-diagnostics.md` 表格并附改法,`docs` 给一页纸参考(`REFERENCE`)、`lsp::std_functions` 的模块函数签名,或参考里提到关键词的 `## ` 小节;`hover` 按文件、行、列返回那里的类型或签名,复用 `lsp::type_of` 与 `lsp::describe_at`(与编辑器悬停同一套查询)。资源(`resources/list`、`resources/read`)把同一份参考与错误码表作为 `zamak://reference`、`zamak://diagnostics` 交给客户端挂进上下文,`initialize` 因此声明 `resources`。`zam new --template ai` 另写 `.mcp.json`(`mcp::project_config`)。`tests/mcp.rs` 用真实进程走一遍。改了诊断附录或参考,MCP 自动跟着变。
## 修改 Zamak 编译器本身
1. 验收(工作目录 `zam/`):`cargo test --locked`(25 个集成套件,Windows 冷跑约 30 秒;MSVC 环境捕获会缓存在临时目录,首次多付约 4 秒)、`cargo clippy --all-targets --locked -- -D warnings`、`cargo fmt --check`;改完源码要在**仓库根目录**重新 `cargo install --path zam --locked` 才能用新的 `zam`。CI(`.github/workflows/ci.yml`)在 `ubuntu-latest`/`windows-latest`/`macos-latest` 三个平台跑同样三项;AI 评测(`bench/ai/run.ps1`)和构建体积测量(`tools/measure-build.ps1`)只在 Windows 上用 pwsh 跑。**让原来不支持的写法变成支持时,本地也跑一遍** `pwsh -File bench/ai/run.ps1 -ZamPath zam/target/debug/zam.exe -LatencyRepeats 3` 和 `python bench/ai/live/run.py --verify-solutions --zam zam/target/debug/zam.exe`:`bench/ai/cases.json` 的 `unsupported-feature` 等用例拿「当前不支持的写法」当反例,支持之后要换一个仍不支持的写法。**格式以 CI 的最新稳定版 rustfmt 为准**:本机 Windows 与 WSL 截至 2026-10-14 都是 Rust 1.99,与 CI 一致;CI 升级后若本机落后,不同版本对长 `match` 臂的排版可能不同,先 `rustup update stable`,或以 `tools/wsl-verify.sh` 的 `FMT_OK` 为准。
- 版本号:IR 编码(`Module`/`Function`/`Expr`/`Instruction` 的 Debug 输出)一变就要升 `native-c-N`,否则同一个缓存键会撞上旧内容,报 `corrupt cache entry ...zmo`(这是缓存在拦截,不是坏了)。
2. 新增语言构造时按惯例升版本标记:`zam/src/compiler.rs` 的 encode 头 `ZAM-IR-N` 与 `zam/src/project.rs` 的 `VERSION`(`native-c-M`)。两者都参与缓存键,只作失效标记——缓存命中时按字节全等校验,没有 decode,旧产物只会报错不会静默错译;同时更新 `README.md` 里的 IR 号。
3. C 后端源码按 UTF-8 写出,MSVC `cl` 默认按本地代码页(936)读取,会吞掉中文注释后的下一行 → 编译命令行必须保留 `/utf-8`。`zam/src/native.rs` 的 `OPTIONS`(`-vN`)是 MSVC 与 GNU 两套命令行**共用**的一个版本标记,改动任一边(`compile`/`link` 里的 `/O2 /MT ...` 或 `-std=c11 -O2 -funroll-loops ...`)都要考虑升它——但只在**产物字节真的变了**时升版:若只是改了调用方式(例如从 `cmd /c call vcvars64.bat && cl` 改成直接调 `cl.exe`),先用旧缓存验证仍全部 `OBJECT HIT`/`LINK HIT` 再决定不升,避免无谓的缓存失效。
4. 跨平台约束:
- 平台分支只放在 `native.rs`(`tool`/`uses_msvc`/`compile`/`link`/MSVC 环境捕获)和 `project.rs`(`cache_dir`、产物 `.exe` 后缀与 Unix 0755 权限),用 `cfg!(windows)` / `#[cfg(unix)]`,不要在前端或检查器里分平台。
- 生成的 C 必须同时能被 MSVC 和 GCC/Clang 编译:GCC/Clang 专用写法(如 `__builtin_add_overflow`)放在 `#if defined(__GNUC__) || defined(__clang__)` 里并给 MSVC 留等价分支,见 `native.rs` 开头的运行时前导。
- 测试和示例不要写死 `.exe`、`\` 路径分隔符或 CRLF;产物路径从 stderr 的 `OUTPUT` 行取。Windows 检出可能把示例转成 CRLF(git `autocrlf`),断言输出时不要依赖源码的换行风格。
- macOS 本机无法验证,涉及 `cc` 命令行、链接或目标文件后缀的改动,提交前说明「macOS 待 CI 确认」,推送后看 GitHub Actions 结果。
5. 示例放 `examples/<主题>/*.zm`(多模块项目为 `<主题>/src/main.zm` + `zam.toml`),并在 `zam/tests/<主题>.rs` 里用 `include_str!(...)` 引用示例做正例与反例,真正运行产物并断言 stdout/stderr/退出码。
6. 文档边界:`README.md`(英文,GitHub 首页)与 `README.zh-CN.md`(中文,更详细)都入库,改能力范围时两份都要同步;`SYNTAX.md`、`ROADMAP.md`、`DESIGN.md`、`NAMING.md`、`MIGRATION.md` 被 `.gitignore` 忽略,只在本地维护。二者都要同步(README 如实标注缺口,ROADMAP 记进度)。
7. 提交:编译器、标准库、测试、示例的改动先上 `dev`,CI 在三个平台通过后再把 `main` 快进到同一提交;只改文档的可以直接上 `main`。提交信息用英文 `<type>: summary`(feat/fix/docs/refactor/test/perf/build/chore),正文说明改了什么、为什么。**不加任何 AI 署名**(`Co-Authored-By`、`Generated with` 等),PR 和发布说明也一样。改完按仓库根目录 `AGENTS.md` 的「Done means everything is in sync」清单同步文档。
8. 编辑器/AI 生态:`editors/vscode/` 是 TextMate 语法高亮加一个很薄的客户端(`extension.js`,用 `vscode-languageclient` 启动 `zam lsp`;诊断、补全、悬停、跳转、重命名都在 `zam` 里实现),改动关键字、符号或类型时要同步 `editors/vscode/syntaxes/zamak.tmLanguage.json` 与 `language-configuration.json`。
## 参考资料
`README.md` / `README.zh-CN.md`(能力与缺口)、`book/`(The Zamak Book,中英双语,`zam/tests/book.rs` 运行每段示例并逐字核对输出;`python book/build.py` 构建,`book/wrangler.jsonc` 部署到 zamak.veyralux.org)、`SYNTAX.md`(语法细节)、`examples/`(可运行示例)、`ROADMAP.md`(当前进度的权威记录)、`zam/src/compiler.rs`(前端)、`zam/src/native.rs`(C 后端)、`zam/src/project.rs`(构建与缓存)。