---
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()` 报错。
- 运算符方法（Python 的 dunder）：`compiler::operator_method` 把运算符映射到方法名（`==`/`!=` 都是 `__eq__`），`operator_call`/`negation_call` 在左边（`-x` 的 x）是定义了该方法的结构体时，把 `a op b` 写成 `Method`（变量、字段）或 `Chain`（其他表达式），`!=` 再包一层 `Unary('!')`。**检查器（`Checker::overloaded`/`negated`）和生成器（`Generator::overloaded`/`negated`）的 `ty` 与 `expr` 都在二元/一元分支最前面先试一次**，然后按方法调用走完；`needed` 遇到运算符就带上所有同名的结构体方法。只看左边，不支持 `__radd__` 这类反射方法。没定义 `__eq__` 时 `==` 仍逐项比较（报错分支要排除 ==/!=）。`std/datetime` 的 `TimeDelta` 与 `DateTime.__sub__` 等都按整数微秒算（`epoch_microseconds`、`microseconds_delta`、`from_microseconds`）。`__str__`：`native_values::custom_text` 找 `Owner.__str__`（非泛型、`&self`、返回 string、不会失败），找到时结构体的 `showR` 改成调用它（先声明原型，因为 show 函数在函数原型之前输出）；`needed` 总是带上所有 `.__str__` 方法。容器里的元素也用 `__str__`（Python 用 `__repr__`，这里没分）。`std/bigint` 用 10 亿进制的 `[i64]`（低位在前），除法逐位二分找商，除以 0 与负指数靠 `1 // 0`、`2 ** -1` 触发与整数相同的运行时错误。报错显示名：`compiler::called`（函数，`std/string:parse_i64` → `int`）与 `record_text`（类型，`main:P` → `P`、`std/datetime:DateTime` → `datetime.DateTime`）。
- 程序图标与无控制台（native-c-76）：`[app]`（`package::App`，`console = false` / `icon`）与命令行 `--noconsole`、`--icon <文件>`（`main.rs` 先抽出来，只许 build/run；`Project::with_app` 覆盖清单）。只在 Windows 生效，其他系统输出一行 `NOTE` 并忽略。链接时 `native::link_with(.., gui, icon)`：MSVC 加 `/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup`（默认入口要 WinMain），图标写成 `1 ICON "app.ico"` 的 .rc 经 `rc.exe`（在 `msvc_environment` 的 PATH 里找，Windows SDK）编成 `app.res` 一起链接；MinGW 用 `-mwindows` 与 `windres`。`gui` 与图标内容的哈希进了链接缓存键（`link_settings`）；`zam test` 的测试程序不用。图标只认 `.ico`（`read_icon` 查头 0,0,1,0），别的格式报错不猜。**运行时**：GUI 子系统的程序从资源管理器启动没有标准输出，运行时用 `__ImageBase` 读自己的 PE 头认出 GUI 子系统，`zam_stream_missing` 让写入当作成功，`output_mode`、`zam_newline`、`zam_flush`（main 的收尾）也跳过，否则一打印就 `exit(1)`；控制台程序保持原样，写失败仍是失败。测试 `tests/app.rs`（含用 `start /wait` 模拟没有标准句柄）。
- 网络（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` 里实现）。**找 zam 在 `locate.js`**：npm 在 Windows 上装的是 `zam.cmd`，Node 启动 .cmd 会报 EINVAL，Linux/macOS 上是指向 zam.js 的链接（要 PATH 上有 node），所以顺着包装找到平台包里的原生 zam 直接启动；顺序是 zamak.path（只写名字不算）→ PATH → `npm root -g` → 安装脚本/Cargo 的常见目录。测试 `editors/vscode/scripts/check-locate.mjs`（CI 的 editor-config 任务），改动关键字、符号或类型时要同步 `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`（构建与缓存）。
