diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..cd682e4 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + - package-ecosystem: "maven" + directory: "/" + schedule: + interval: "monthly" + # 项目支持JDK 8,JUnit 6需要Java 17,忽略其大版本升级 + ignore: + - dependency-name: "org.junit.jupiter:*" + versions: [">=6"] + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 91106d3..b998496 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -4,14 +4,106 @@ on: [push] jobs: build: + # 只跑 JDK 21:协程依赖虚拟线程(JEP 444),这是本实现的下限。 + # 旧的多版本矩阵(8/11/17)在引入 release=21 之后已经无意义。 + runs-on: ubuntu-latest + name: Build (JDK 21) + steps: + - uses: actions/checkout@v7 + - name: Set up JDK 21 + uses: actions/setup-java@v6 + with: + java-version: '21' + distribution: 'temurin' + cache: 'maven' + - name: Build with Maven Wrapper + run: ./mvnw -B test package + conformance: + # 一致性测试:拿真实的 lua5.4 当裁判。 + # + # 这是本项目的主闸门——「看起来像 Lua」没有价值,唯一能证明「真的是 Lua」 + # 的办法是同一份脚本在两边跑出逐字节相同的 stdout。所以这个 job 先从源码 + # 构建一份参考实现,再把它的路径交给测试。 + # + # 从源码构建而不是 apt install,是因为发行版里的 lua 版本不受控(可能是 + # 5.1/5.3/5.5),拿错版本当参考会得出错误的「不一致」结论,那比没有参考更糟。 runs-on: ubuntu-latest + name: Conformance (against lua5.4) + steps: + - uses: actions/checkout@v7 + - name: Set up JDK 21 + uses: actions/setup-java@v6 + with: + java-version: '21' + distribution: 'temurin' + cache: 'maven' + - name: Build reference lua5.4 + run: | + set -e + curl -sL -o /tmp/lua.tar.gz https://www.lua.org/ftp/lua-5.4.7.tar.gz + mkdir -p target + tar xzf /tmp/lua.tar.gz -C target + mv target/lua-5.4.7 target/lua54 + make -C target/lua54 -j2 linux + ./target/lua54/src/lua -v + # + # 字节码差分的另一半:一个挂在参考实现上的小转储器。 + # 不用 luac -l 是因为它每个 opcode 打的字段不一样、还夹注释,反解回原始 + # 字段要抄那个大 switch;这里自己对所有 opcode 统一打印全部字段。 + # 它必须和参考二进制同目录,测试才知道去哪儿找。 + - name: Build fkdump (bytecode dumper) + run: | + set -e + # + # 必须写成一行。YAML 的块标量(|)不处理反斜杠续行:写 "\\" 换行的话 + # bash 收到的是两个字符,被当成一个名为 "\" 的输入文件,ld 报 + # "cannot find \:" —— 这一步挂了,后面整个 conformance job 就静默地 + # 再也没跑过(连续 8 次 push 全是这个错,一致性闸门形同虚设)。 + gcc -O2 -I target/lua54/src -o target/lua54/src/fkdump src/test/resources/fkdump.c target/lua54/src/liblua.a -lm + test -x target/lua54/src/fkdump + # + # 编译期差分的语料:上游自带的测试套件。这批脚本是 Lua 作者自己写的、 + # 专门用来压边界的,比手写几十条用例强得多。目前只编译不运行。 + - name: Fetch upstream lua test suite + run: | + set -e + curl -sL -o /tmp/tests.tar.gz https://www.lua.org/tests/lua-5.4.7-tests.tar.gz + mkdir -p target/lua54-tests + tar xzf /tmp/tests.tar.gz -C target/lua54-tests --strip-components=1 + ls target/lua54-tests/*.lua | wc -l + # + # 跑全量而不是只挑几个类,并且不跳过覆盖率门禁。 + # + # 只挑 ConformanceTest/CodegenTest/ParserTest 等于把上游套件、差分 fuzz、 + # 二进制 chunk 兼容这两批闸门留在 CI 之外——它们在 CI 上一次都没跑过, + # 绿的只是本地。而覆盖率门禁跟一致性测试绑在一起(没有参考二进制时 + # 一致性测试整类跳过,门禁跟着跳过),所以 -Djacoco.skip=true 一加, + # 覆盖率这条线在 CI 上也是空的:两条最硬的闸门都形同虚设。 + - name: Run conformance tests + env: + LUA54_BIN: target/lua54/src/lua + FKDUMP_BIN: target/lua54/src/fkdump + run: ./mvnw -B test + quality: + # spotbugs静态分析报告(报告模式,不阻断) + runs-on: ubuntu-latest + name: Static analysis steps: - - uses: actions/checkout@v1 - - name: Set up JDK 1.8 - uses: actions/setup-java@v1 + - uses: actions/checkout@v7 + - name: Set up JDK 21 + uses: actions/setup-java@v6 + with: + java-version: '21' + distribution: 'temurin' + cache: 'maven' + - name: SpotBugs + run: ./mvnw -B verify -Pquality -DskipTests + - name: Upload SpotBugs report + if: always() + uses: actions/upload-artifact@v7 with: - java-version: 1.8 - - name: Build with Maven - run: mvn -B package --file pom.xml + name: spotbugs-report + path: target/spotbugsXml.xml + if-no-files-found: ignore diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..e74dab3 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,99 @@ +name: Publish to Maven Central + +on: + push: + paths: + - 'pom.xml' + branches: + - master + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + fetch-depth: 2 + + - name: Set up JDK 17 + uses: actions/setup-java@v6 + with: + java-version: '17' + distribution: 'temurin' + cache: 'maven' + + - name: Check version change + id: check_version + run: | + CUR_VER=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "Current version: $CUR_VER" + echo "current_version=$CUR_VER" >> $GITHUB_OUTPUT + + # If manually triggered or HEAD^ does not exist, proceed + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "Triggered manually. Proceeding to publish." + echo "should_publish=true" >> $GITHUB_OUTPUT + exit 0 + fi + + git checkout HEAD^ pom.xml || true + PREV_VER=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout || echo "") + git checkout HEAD pom.xml || true + echo "Previous version: $PREV_VER" + + if [ "$CUR_VER" != "$PREV_VER" ]; then + echo "Version changed from '$PREV_VER' to '$CUR_VER'. Proceeding to publish." + echo "should_publish=true" >> $GITHUB_OUTPUT + else + echo "Version has not changed ($CUR_VER). Skipping publish." + echo "should_publish=false" >> $GITHUB_OUTPUT + fi + + - name: Import GPG key + if: steps.check_version.outputs.should_publish == 'true' + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + run: | + if [ -z "$GPG_PRIVATE_KEY" ]; then + echo "GPG_PRIVATE_KEY secret is not set. Please configure it in repository secrets." + exit 1 + fi + mkdir -p ~/.gnupg + chmod 700 ~/.gnupg + echo "allow-loopback-pinentry" > ~/.gnupg/gpg-agent.conf + echo "pinentry-mode loopback" > ~/.gnupg/gpg.conf + gpgconf --kill gpg-agent || true + gpg-agent --daemon || true + echo "$GPG_PRIVATE_KEY" | gpg --batch --import --pinentry-mode loopback + + - name: Configure Maven settings + if: steps.check_version.outputs.should_publish == 'true' + env: + MAVEN_USERNAME: ${{ secrets.CENTRAL_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.CENTRAL_TOKEN }} + run: | + mkdir -p ~/.m2 + cat << 'EOF' > ~/.m2/settings.xml + + + + central + ${env.MAVEN_USERNAME} + ${env.MAVEN_PASSWORD} + + + + EOF + + - name: Publish to Central Repository via Sonatype Portal + if: steps.check_version.outputs.should_publish == 'true' + env: + MAVEN_USERNAME: ${{ secrets.CENTRAL_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.CENTRAL_TOKEN }} + run: | + ./mvnw -B clean deploy -Prelease \ + -DskipTests \ + -Dgpg.passphrase="" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad96c2d --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +target/ +*.class +*.jar +*.log + +# Jazzer 模糊测试产物:语料库与 crash 复现文件由本地 fuzzing 生成, +# 只有经过确认并落到 JazzerFuzzTestInputs 的输入才应当入库 +.cifuzz-corpus/ +crash-* + +.idea/ +*.iml +.project +.classpath +.settings/ + + +# WorkBuddy 工作空间记忆(本地) +.workbuddy/ + +# 上游 code.lua / luac.lua 跑出来的字节码产物 +luac.out diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..7c6b218 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip diff --git a/README.md b/README.md new file mode 100644 index 0000000..1c02e4e --- /dev/null +++ b/README.md @@ -0,0 +1,3058 @@ +# fakescript-java + +[![License](https://img.shields.io/github/license/esrrhs/fakescript_java)](https://github.com/esrrhs/fakescript_java) +[![Language](https://img.shields.io/github/languages/top/esrrhs/fakescript_java)](https://github.com/esrrhs/fakescript_java) +[![Maven Central](https://img.shields.io/maven-central/v/com.github.esrrhs/fakescript-java)](https://central.sonatype.com/artifact/com.github.esrrhs/fakescript-java) +[![Build Status](https://github.com/esrrhs/fakescript_java/actions/workflows/maven.yml/badge.svg?branch=master)](https://github.com/esrrhs/fakescript_java/actions) + +**Lua 5.4 in pure Java — no GC of its own; the JVM does the collecting.** + +[简体中文](./README_CN.md) + +--- + +## Design Goal + +> Run existing Lua projects on the JVM **without changing a single line of their Lua code.** + +This project is a **module-by-module port of the Lua 5.4 reference implementation** +(`lua-5.4.x` C sources) to Java. It is not a Lua-inspired language that merely looks +similar — it is Lua 5.4, with the memory-management layer removed and handed to the +host runtime. + +| | | +|---|---| +| **Target** | Full Lua 5.4 language semantics + complete standard library | +| **Method** | Direct, module-by-module translation of the C reference sources | +| **GC** | **Not implemented.** Object lifetime is entirely the JVM's job (G1/ZGC are concurrent collectors already) | +| **Runtime** | Java 17+ (coroutines are plain stacks, no threads — see below) | +| **Compatibility** | Pure-Lua projects; C extension modules (`.so` / `luaopen_*`) are out of scope for now | + +### Why there is no GC + +`lgc.c` is one of the most intricate parts of the C implementation: tri-colour +incremental marking, generational mode, write barriers, finalizers, weak tables. +All of it exists to do what the JVM already does — concurrently, and better tuned +than a hand-rolled collector written in Java would be. The file is deleted +outright. Only the *semantics that depend on collection happening* are rebuilt on +JVM primitives: + +| Lua concept | C mechanism | This implementation | +|---|---|---| +| Object lifetime | `lgc.c` tri-colour marking | **JVM GC** (G1 / ZGC) | +| Weak tables (`__mode = "k"/"v"/"kv"`) | weak links swept in `lgc.c` | `WeakReference` + `ReferenceQueue` | +| `__gc` finalizers | `luaC_checkfinalizer` / `GCTM` | `java.lang.ref.Cleaner` | +| Short-string interning | `lstring.c` table pinned against collection | weak intern pool; entries die when unreferenced | + +Everything else — lexer, parser, code generator, VM, metamethods, coroutines, +standard library — is a direct port. + +--- + +## Architecture + +The package layout mirrors the C sources one-to-one, so a reader holding +`lua-5.4.x` side by side can find their way around: + +| Lua 5.4 C source | Java | +|---|---| +| `lobject.h` / `lobject.c` | `LuaValue`, `LuaString`, `LuaTable`, `Proto`, `Closure`, `Upvalue` | +| `lstring.c` | `StringPool` | +| `ltable.c` | `LuaTable` | +| `lfunc.c` | `Proto`, `Closure`, `Upvalue` | +| `lstate.c` | `LuaState`, `CallInfo`, `GlobalState` | +| `lzio.c` | `Zio` | +| `llex.c` | `Lexer` | +| `lparser.c` | `Parser` (hand-written recursive descent, as upstream) | +| `lcode.c` / `lopcodes.h` | `FuncState`, `Codegen`, `OpCode` | +| `lvm.c` | `Interpreter` | +| `ldo.c` | call frames, protected calls, error propagation | +| `ltm.c` | `Meta` (metamethod dispatch) | +| `ldebug.c` | `Debug`, error messages, stack traces | +| `lapi.c` / `lauxlib.c` | operations on `LuaState`, `AuxLib` | +| `lundump.c` / `ldump.c` | bytecode load / dump | +| `lbaselib.c`, `lcorolib.c`, `loadlib.c`, `lstrlib.c`, `ltablib.c`, `lmathlib.c`, `liolib.c`, `loslib.c`, `lutf8lib.c`, `ldblib.c` | corresponding `lib.*` classes | +| `lgc.c` | **absent** — see above | + +The auxiliary layer (`lauxlib.c` and friends) comes along, because the standard +library is written against it. + +--- + +## Key Design Decisions + +These are the places where translating C to Java is not mechanical, and where a +wrong call costs correctness that is expensive to recover later. + +### 1. Lua strings are byte strings, so Java `String` cannot be used + +A Lua string may contain `\0`; `#s` counts **bytes**; `s:byte(i)` returns a byte. +Java `String` is UTF-16 and rejects lone surrogates. Any real-world Lua project +that touches binary data, UTF-8 text, or embedded `\0` will break if strings are +backed by `String`. Lua strings are therefore backed by `byte[]`, and every string +library function is implemented byte-wise. + +### 2. Values keep the integer/float subtype split + +Lua 5.4 distinguishes `1` (integer) from `1.0` (float): they compare equal, but +`math.type` differs, `//` differs from `/`, `1/2` is `0.5` while `1//2` is `0`, +and coercion to string yields `1` vs `1.0`. Values are a **mutable slot** type +(a `type` tag plus `long`, `double` and an `Object` payload) mirroring C's `TValue` +— this avoids boxing and lets the VM write stack slots in place. + +### 3. Tables keep the array part / hash part split + +Ported from `ltable.c`, including the migration logic that decides whether an +integer key is dense enough to live in the array part. Integer keys and string keys +are distinct entries: `t[1]` and `t["1"]` are different, as in Lua. + +### 4. Coroutines are separate stacks, not threads + +Lua coroutines can yield from arbitrarily deep stacks — inside `pcall`, inside a +metamethod, inside a `for` iterator. The plan was to give each one a Java virtual +thread. That turned out to be the wrong call, and phase 5 dropped it. + +A Lua coroutine is *cooperative*: exactly one runs at a time, and the suspension +point is stated explicitly by `coroutine.yield`. C models this with one stack per +`lua_State` plus `longjmp` out to `lua_resume`. Java has the same two ingredients +— one stack per `LuaState`, and an exception (`LuaYield`) that unwinds the Java +call stack. **Nothing needs a thread**, because all the Lua state lives on the +heap: unwinding the Java frames leaves the stack, the `CallInfo` chain and the +open upvalues exactly as they were, ready to continue on `resume`. + +Virtual threads were also *too permissive*. A C function that calls back into Lua +(`table.sort`'s comparator, `string.gsub`'s replacement function) cannot yield in +real Lua — it raises `attempt to yield across a C-call boundary`. On a virtual +thread it would simply work, and that difference is a bug, not a feature, for a +project whose goal is "run existing Lua code unchanged." + +This also drops the Java floor from 21 to 17, which is what makes the local build +work today. + +The machinery that replaced threads is a faithful port of `ldo.c`: +`lua_resume` / `unroll` / `finishCcall` / `finishpcallk` / `precover`, plus +`luaV_finishOp`, which completes the half-executed instruction that a yield +interrupted. C functions that can be interrupted (`pcall`, `xpcall`) register a +continuation — the Java equivalent of `lua_KFunction`. + +### 5. Errors use Java exceptions, not `setjmp` / `longjmp` + +`lua_error` becomes a thrown error carrying the Lua error value. `pcall` / `xpcall` +catch it, as does the coroutine boundary. Care is needed so that non-string error +values, error objects carrying `__tostring`, and errors crossing a coroutine +boundary all behave as upstream does. + +### 6. No JFlex / Bison + +The Lua reference parser is hand-written recursive descent. Porting it directly +removes the build-time code-generation step, along with the generated-parser drift +problem the previous architecture had. + +### 7. Metamethods: all of them, including the awkward rules + +`__index`, `__newindex`, `__call`, `__concat`, `__len`, `__eq`, `__lt`, `__le`, +`__unm`, `__add` … `__idiv`, `__close`, `__gc`, `__mode`, `__name`, `__metatable`, +`__pairs`, `__ipairs`. Including the parts that are easy to get wrong: `__le` +degrading to `not (b < a)` when only `__lt` exists; `__eq` firing only for +comparable types; `__len` on tables ignoring non-integer results; and `__index` +chain traversal. + +--- + +## Roadmap + +Each phase ends with tests green. Nothing advances on a red phase. + +| Phase | Content | Upstream reference | +|---|---|---| +| **0** | Foundations: `LuaValue`, `LuaString` + interning, `LuaTable`, `LuaState`, error type, `Zio` | `lobject`, `lstring`, `ltable`, `lstate`, `lzio` | +| **1** | Lexer + recursive-descent parser; emits directly as upstream does, with no separate tree | `llex`, `lparser` | +| **2** | Bytecode: full Lua 5.4 instruction set, code generator, constant folding, upvalue resolution | `lopcodes`, `lcode`, `lfunc` | +| **3** | Interpreter main loop: calls/returns, arithmetic & comparison with coercion, metamethod dispatch | `lvm`, `ldo`, `ltm` | +| **4** | Closures & open/closed upvalues, `goto`/labels, varargs, multiple assignment and returns | `lparser`, `lvm` | +| **5** | Coroutines: `coroutine.*`, yield/resume/unroll, yield & error across boundaries | `lcorolib`, `ldo`, `lvm` | +| **6** | Collection-dependent semantics: weak tables, `__gc`, weak string pool | *(replaces `lgc.c`)* | +| **7** | Standard library: `base`, `coroutine`, `package`, `string`, `table`, `math`, `io`, `os`, `utf8`, `debug` | `l*lib.c`, `loadlib.c` | +| **8** | `load` / `loadstring` / `dump`, `require` searchers, `package.path` / `cpath` | `lundump`, `ldump`, `loadlib` | +| **9** | Conformance: upstream test suite + differential testing against a real `lua5.4` binary | — | + +--- + +## How "it is actually Lua" gets verified + +A rewrite that merely *looks* like Lua is worthless. Four gates, in increasing +order of strength: + +1. **Upstream test suite.** The `lua-5.4.x/tests/*.lua` files (`alls.lua`, + `big.lua`, `strings.lua`, `coroutine.lua`, `pm.lua`, …) run unmodified. This is + the primary gate — upstream's own suite is far more adversarial than anything + written by hand here. +2. **Differential testing.** The same script runs under this implementation and + under a real `lua5.4` binary; stdout must match byte for byte. Wired into CI + with `lua5.4` installed. +3. **Differential fuzzing.** Generated programs compared against the reference + binary — this is what catches the "no crash but wrong answer" class. +4. **Coverage gate** (JaCoCo line ratio). + +A phase is done only when the gates that apply to it are green. + +--- + +## Status + +**Phase 10 — the upstream `lua-5.4.x/tests` suite runs green end to end.** 19 +files whose output is deterministic (`api`, `bitwise`, `calls`, `closure`, +`code`, `coroutine`, `db`, `errors`, `events`, `gengc`, `goto`, `nextvar`, `pm`, +`strings`, `tpack`, `utf8`, `vararg`, `verybig`, …) match a real `lua5.4` +**byte for byte**. Five more (`cstack`, `gc`, `literals`, `locals`, +`constructs`) print things that vary with the implementation or with a random +seed, so they are only asserted to run to completion and print `OK` — the reason +for each is listed under *Phase 10* below. 874 tests green, JaCoCo line coverage +0.96 against a 0.95 gate. (The upstream-suite gate needs `LUA54_TESTS` or +`target/lua54-tests`; without a test directory that whole class **skips itself**, +so a green run on a machine without it is 25 tests weaker than it looks.) + +**Phase 9 — complete, including `debug.sethook`.** The previous fakescript +language implementation has been removed from `master`; see *Migration* below. +Phases 0 through 9 are in: the compiler produces bytecode that is +**field-for-field identical** to a real `lua5.4`'s, and the VM executes it — +**stdout matches a real `lua5.4` byte for byte** across 158 + 121 + 60 + 32 + 25 ++ 16 + 21 + 36 differential scripts and 2100 generated programs, and error +messages match line for line. `string.dump`'s output is additionally compared as +**raw bytes**, not just as behaviour. All ten standard libraries are in (base, +coroutine, package, string, utf8, table, math, os, io, debug), `debug` included +in full — hooks and all. + +Phase 6 is the one phase where "no GC of our own" costs real ground. Weak tables, +`__gc` and the string pool are all rebuilt on JVM reachability, which is *not* the +same thing as Lua's mark-and-sweep; three places where it cannot be made to match +are listed below under *Known divergences*. + +Landed so far: + +| Piece | Upstream | Notes | +|---|---|---| +| `Lua` type tags | `lua.h`, `lobject.h` | integer/float subtypes kept as distinct variants | +| `LuaValue` | `TValue` | mutable slot, so the VM can write stack slots in place | +| `LuaString` | `TString` | `byte[]`-backed; `\0`, arbitrary and invalid-UTF-8 bytes all preserved | +| `StringPool` | `lstring.c` `strt` | interned weakly, so unused strings do not pin the heap | +| `LuaTable` | `ltable.c` | array part + hash part, `rehash`, `#t` borders, integral-float key normalisation | +| `LuaError` | `lua_error` | carries a Lua value; only this type is caught by `pcall` | +| `Zio` | `lzio.c` | byte input stream; `EOZ` is `-1`, bytes are unsigned | +| `LCType` | `lctype.c` | the `luai_ctype_` table copied byte for byte, so `lislalpha` treats `_` as a letter exactly as upstream does | +| `Numeral` | `lobject.c` `luaO_str2num` | int-first-then-float; strict decimal syntax, so `1d`/`inf`/`nan` are rejected the way C's `strtod` rejects them | +| `Lexer` | `llex.c` | full token set, all escapes incl. `\z` / `\xXX` / `\u{XXX}` / `\ddd`, long strings & comments, byte-exact error messages | +| `OpCode` | `lopcodes.h/.c` | all 83 opcodes; order, names and `luaP_opmodes` verified mechanically against upstream headers | +| `Proto` | `lobject.h`, `lfunc.c` | code / constants / nested protos / upvalues / locals / line info | +| `Arith` | `lobject.c`, `lvm.c` | the arithmetic used by constant folding — same code path as the VM will use | +| `ExpDesc`, `FuncState`, `DynData` | `lparser.h` | `expdesc`'s union flattened; block/goto/variable bookkeeping | +| `Codegen` | `lcode.c` | instruction emission, register allocation, jumps & patching, constant folding, `NEWTABLE`/`SETLIST` | +| `Parser` | `lparser.c` | hand-written recursive descent, emitting code as it parses | +| `UpVal` | `UpVal` | two-state (open = stack slot reference, closed = private copy) | +| `LClosure` | `LClosure` | proto + upvalue array | +| `CallInfo` | `CallInfo` | frame record; `CIST_FRESH` decides where a Lua→Lua return stops | +| `LuaState` | `ldo.c`, `lstate.c` | stack, call/return, `pcall`, upvalue open/close, all `luaG_*` errors | +| `LuaTm` | `ltm.c` | metamethod lookup & dispatch, `__le` fallback, `to-be-closed` | +| `Vm` | `lvm.c` | the interpreter main loop; all 83 opcodes | +| `BaseLib`, `MathLib` | `lbaselib.c`, `lmathlib.c`, part of `lstrlib.c` | minimal but differentially-verified | +| `Diff` (test) | — | shared differential driver: runs one script on both sides | +| `ConformanceTest`, `CodegenTest`, `ParserTest`, `RuntimeTest` | — | run a real `lua5.4` binary as the oracle | +| `ClosureTest` (test) | — | Phase 4 gate: closures, `goto`, varargs, multiple assignment | +| `CClosure` | `CClosure` | a C function bound to values — needed by `coroutine.wrap` | +| `LuaThread` value (`Lua.VTHREAD`) | `LUA_TTHREAD` | a `LuaState` reachable as a Lua value | +| `CoroutineLib` | `lcorolib.c` | `create` / `resume` / `yield` / `status` / `wrap` / `isyieldable` / `running` / `close` | +| `resume` / `unroll` / `finishCcall` / `finishpcallk` / `precover` | `ldo.c` | the yield/resume state machine, no threads | +| `LuaKFunction` | `lua_KFunction` | continuation for a C function interrupted by a yield | +| `LuaYield` | `luaD_throw(L, LUA_YIELD)` | unwinding exception; deliberately *not* caught by `pcall` | +| `Vm.finishOp` | `luaV_finishOp` | completes the half-executed instruction a yield interrupted | +| `CoroutineTest` (test) | — | Phase 5 gate: 60 differential scripts, incl. yield inside every metamethod | +| `FuzzTest` (test) | — | 1500 seeded programs + 600 more run inside a coroutine with random yields | +| `LuaTable.WeakRef` | `iscleared` in `lgc.c` | a weak payload *is* the stored form; keeps the identity hash, since `identityHashCode(null)` is 0 and would collapse every dead key onto bucket 0 | +| `LuaTable.purge` / `rewrap` | `cleartable` / `traverseweakvalue` | dead entries are dropped at the end of a collection round, and re-wrapped when `__mode` changes | +| `LuaState.clearDeadStackSlices` | `traversethread`'s atomic branch | nils `[top, stack_last)` on every thread, inside `collectgarbage` | +| `GcSentinel` | `GCTM` in `lgc.c` | sentinel whose `finalize` resurrects the table onto a pending queue; drained on the Lua thread | +| `LuaState.close` | `lua_close` → `callallpendingfinalizers` | runs `__gc` for every object that still has one, at exit | +| `WeakTableTest` (test) | — | Phase 6 gate: 20 differential scripts, split into a collection-dependent half and an always-true half | +| `GcTest` (test) | — | Phase 6 gate: 12 scripts — once-only, errors swallowed, late `__gc` ignored | +| `LuaUserdata` | `Udata` | payload + **per-object** metatable; `luaL_testudata` needs the latter | +| `Undump` | `lundump.c` | reads a precompiled chunk; header check, varint sizes, the `binary string` chunk-name rule | +| `Dump` | `ldump.c` | writes one; `string.dump`, `strip` | +| `LoadTest` (test) | — | Phase 8 gate: 25 scripts — modes, chunk names, `_ENV`, reader functions, `loadfile` / `dofile` | +| `DumpTest` (test) | — | Phase 8 gate: 16 — runtime stdout **and** `string.dump` output byte for byte | +| `StringPack` | part of `lstrlib.c` | `string.pack` / `unpack` / `packsize`; the only platform-dependent corner of the library | +| `PackTest` (test) | — | 21 scripts — integers, floats, strings, alignment, wide integers, errors, key set | +| `DebugLib` | `ldblib.c` + part of `ldebug.c` / `ldo.c` / `lauxlib.c` | `getinfo` / locals / upvalues / `traceback` / registry / metatables / user values / `setcstacklimit` / `sethook` / `gethook` | +| `LuaHook` | `lua.h` `lua_Hook` | the hook callback; `DebugLib`'s `hookf` is the one `sethook` installs | +| `LuaLibs` | `linit.c` | `luaL_openlibs`; the ten libraries in `loadedlibs` order | +| `DebugTest` (test) | — | Phase 9 gate: 36 scripts — levels, options, locals, upvalues, `traceback`, deep stacks, and 14 hook scripts | +| `LuaLibsTest` (test) | — | `openAll` really opens all ten, and they are the same tables the globals see | + +### The bytecode gate + +The compiler is validated by **differential comparison, not by self-declared +expectations**. `src/test/resources/fkdump.c` is a small dumper compiled against +the reference `lua5.4` sources; for every script it prints every instruction with +every field (`a`, `b`, `c`, `k`, `Bx`, `sBx`, `Ax`, `sB`, `sC`, `sJ`), plus the +line, the constant table and the upvalue table, recursively for nested functions. +`CodegenTest` compares that against `ProtoDump`'s rendering of our `Proto`, +requiring the two to be identical. + +Two corpora: ~50 hand-written scripts covering every construct, and the 33 files +of the **upstream `lua-5.4.7` test suite** (`target/lua54-tests`, fetched in CI), +compiled but not run. All of them match. + +Error messages are gated the same way: `ParserTest` writes broken sources to disk, +runs them through a real `lua5.4`, and compares the message byte for byte — +`'end' expected (to close 'if' at line 1)`, `label 'a' already defined on line 1`, +` at line 1 jumps into the scope of local 'x'`, and so on. The lexer half +was already gated like this: a table of deliberately-broken sources is compared +verbatim, including the `near ''` part whose quoting rules differ between +`` and everything else. + +Two decisions in this phase were settled by running the reference implementation +rather than by reading the manual: + +* an integral float used as a key **is normalised to an integer** on write + (`t[1.0]` yields `math.type(next(t)) == "integer"` and `#t == 1`); +* writing a **NaN** key raises `table index is NaN`, while *reading* one silently + returns `nil`. + +Both are locked by tests that run against real Lua 5.4 in `ConformanceTest`. + +### Three things the reference binary settled this phase + +Each of these looks like a bug and is not; all three are locked by tests. + +* **`>>` is a logical shift.** `intop(>>,x,-y)` casts to `lua_Unsigned` first, so + `-8 >> 2` is `4611686018427387902`, not `-2`. +* **Upstream's constant table contains duplicates.** `ls->h` — the + "constant → index" cache — is shared by the whole chunk, while each function + numbers its own constants from 0. A nested function overwrites cached indices + with its own small ones; back in the outer function the index no longer points + at the right constant, so the entry is rejected and a duplicate is appended. +* **Integral floats get a perturbed cache key.** Lua compares table keys with + `==`, so `100000` and `100000.0` are the *same* key; without + `luaK_numberK`'s `r + r*2^-52` trick they would evict each other from the cache + and a constant would be re-created on every use. + +### The runtime gate + +`RuntimeTest` is the Phase 3 gate. Every case is a script that runs **twice** — +once under this implementation, once under a real `lua5.4` — and the two stdout +streams must be equal. Error cases are gated on the message line, including the +`(local 'x')` / `(constant '10')` / `(method 'nope')` suffixes. + +The stack is modelled with one invariant that everything else depends on: + +> A stack slot `LuaValue` is **never replaced**. `LuaState` always does +> `stack[i].set(v)`, never `stack[i] = v`. + +That single rule is what makes "an open upvalue holds a reference to the stack +slot" mean the same thing in Java as "an open upvalue holds a pointer to the +stack slot" means in C. Break it once and closures silently stop seeing updates. + +Running it today: + +```java +LuaState L = new LuaState(); +BaseLib.open(L); +MathLib.open(L); +L.run("print('hello from Lua')"); +``` + +### Six things the reference binary settled this phase + +Each of these was guessed wrong from the manual or from intuition first; all six +are locked by differential tests. + +* **Arithmetic does not coerce strings, and `"10" + 1 == 11` does not come from + the VM.** `lvm.c`'s arithmetic macros use `tonumberns`/`tointegerns`, which + ignore strings. The coercion lives in `lstrlib.c`, which installs `__add` (and + friends) on the string metatable — first `lua_stringtonumber` both sides, then + `lua_arith`. Skip those metamethods and you get a plausible but wrong + `attempt to perform arithmetic on a string value`. +* **A C function's `n` return values are the top `n` pushed values.** `ipairsaux` + pushes `i`, then pushes `t[i]`, and returns `1` when `t[i]` is nil — so the + value seen by the generic `for` is that trailing **nil**, not `i`. Written the + intuitive way (`return i`), `for _, v in ipairs(t)` never terminates, because + the loop only stops when the *first* result is nil. +* **Every arithmetic opcode is followed by an `OP_MMBIN*`.** On success the VM + does `pc++` to skip it; on failure it falls through and the metamethod gets + dispatched. Miss one `pc++` and a working expression runs its metamethod too. +* **`OP_NEWTABLE`'s `EXTRAARG` is unconditional** (because `k == 0` ⟺ `Ax == 0`), + while `OP_SETLIST`'s is conditional. Reading it conditionally produces + `malformed opcode EXTRAARG` on every table constructor. +* **C's `%g` strips trailing zeros; Java's does not.** `1.5` printed as + `1.5000000000000` until `formatG` was written by hand. +* **`luaG_runerror` and `luaL_error` take their position differently.** The + former uses `isLua(ci)` on the *current* frame; the latter is + `luaL_where(L, 1)` and walks back one level to the caller. A library function + that uses the former reports no `src:line:` prefix at all, because its own + frame is a C frame. + +### The fuzzing gate + +`FuzzTest` generates 2100 programs from a small grammar and diffs each one +against the reference — 1500 plain, and 600 more whose main chunk runs inside a +`coroutine.create` with `coroutine.yield` sprinkled at random points and driven +by a bounded `resume` loop. Two constraints keep it from becoming a nuisance +generator: + +* **It must terminate.** Only bounded `for` / `ipairs` loops, no `while`, no + `repeat`, no `goto`; a call may only target a function that finished being + defined *earlier*, so the call graph is a DAG and recursion is impossible. +* **It must be reproducible.** Seeds increment from 0 — no `Math.random`. A + failure is reproducible from "seed 84". + +The generator knows nothing about how we *think* Lua behaves, which is the point: +it is the only gate that finds "does not crash, but the value is wrong". + +### Four things the reference binary settled this phase + +* **`luaF_newtbcupval` checks `__close` at assignment time, not at scope exit.** + `OP_TBC` runs right after the local receives its value, so + `local x = 5 print(x)` raises + `variable 'x' got a non-closable value` *before* printing anything. `nil` and + `false` are exempt (`l_isfalse`), so `local x = nil` is legal. +* **An error inside `__close` during unwinding *replaces* the original error.** + `error('orig')` followed by a `__close` that does `error('in close')` makes + `pcall` return `in close`. Keeping the original instead is wrong. +* **The code generator folds `OPR_MINUS` and `OPR_BNOT` only — never `OPR_LEN`.** + So `#7.5` is a *runtime* error, not a load-time one. Folding it would make a + script fail to compile that upstream compiles and only fails when run. +* **Two harness rules, learned the hard way.** When a script errors, the stdout + it already produced must still be compared (the reference's stdout and stderr + are captured together, so "print ten lines, then crash" has both in its + output); and the `lua.c` program-name prefix must be stripped *everywhere*, + not just at the start, because it appears mid-output once stdout precedes it. + +### Five things the reference binary settled this phase + +* **`pcall`, `xpcall`, metamethods and `for` iterators are yieldable; C-library + callbacks are not.** `table.sort`'s comparator and `string.gsub`'s replacement + function raise `attempt to yield across a C-call boundary`, because + `ltablib.c`/`lstrlib.c` call back through `lua_call` (`luaD_callnoyield`). + `__tostring` invoked by `print` is the same story. This is precisely the + behaviour virtual threads would have "fixed" — and broken. +* **The main thread is never yieldable.** `lstate.c`'s `lua_newstate` does + `incnny(L)` unconditionally, so `coroutine.isyieldable()` is `false` on the main + thread even with no C call in sight. A coroutine starts at zero, and every + `resume` resets it to zero (`lua_resume` copies only the low 16 bits of + `nCcalls` from the resumer). +* **`resume`'s arguments *are* the return values of `yield`.** `lua_resume`'s + `poscall(ci, nargs)` is what makes `coroutine.yield(1)` see `99` after + `resume(co, 99)` — the number of values moved back is the number passed in, + not the number yielded out. +* **`coroutine.wrap` reports its own name, not `create`'s.** `luaB_cowrap` calls + `luaB_cocreate` directly, and `luaL_argerror` derives the name from how the + *current* frame was called — so `coroutine.wrap(nil)` says + `bad argument #1 to 'coroutine.wrap'`. +* **`luaV_finishOp` has a branch per interruptible opcode, and `OP_CONCAT`'s is + not optional.** Concatenation proceeds in batches, so after a `__concat` yield + the metamethod result has to be moved back one slot, `top` pulled in by one, + and the remaining elements concatenated — possibly yielding again. Skipping it + silently yields the *operand table* instead of the concatenation result. + +### Five things the reference binary settled in Phase 6 + +* **The GC-visible stack ends at `L->top`, and that is load-bearing.** + `lgc.c: traversethread` marks only `[stack, top)` and then, in the atomic + phase, nils out `[top, stack_last)` — the source comment calls it "clear dead + stack slice". So a value sitting in a register *above* `top` is invisible to + the collector, and gets collected. Two shapes depend on this: a value pushed + by a C function such as `next` (the call returns, `top` drops back, the slot + still holds the key), and the table built by the last iteration of a loop + (`for i = 1, 5 do t[i] = {i} end` leaves `{5}` in a register while `L->top` + only reaches the call's arguments). Our `stack` is a Java array, so **every + slot is a live reference** — `LuaState.clearDeadStackSlices` reproduces that + clearance, and has to run *inside* `collectgarbage`, not on every `top` + change (see the next bullet). +* **…but clearing eagerly on every `top` change breaks two things.** C only + clears during the atomic phase. Doing it inline silently destroyed both + `luaT_callTMres`'s result (the destination register can *be* the slot being + popped) and the error object of `pcall` (`LuaError.value` aliases a live stack + slot, because `error('boom', 0)` throws `L.arg(0)` itself — so `pcall` started + returning `false nil`). Waiting until `collectgarbage` is both faithful and + exactly as precise. +* **A weak table's storage has to *be* weak, not merely carry a `WeakReference` + beside it.** The payload in the slot is the `WeakReference`; a strong payload + means nothing is ever released. That has a consequence: `LuaTable.get` must + hand back a *copy* for weak tables, so the write fast path + (`OP_SETTABLE`'s "slot found, write into it") cannot be used on them — + writing into a copy is a no-op, and `t.f = g` would leave `t.f` equal to `f`. +* **`__mode` is read dynamically, and re-wraps everything that is already in the + table.** Building a table, filling it, and *then* calling + `setmetatable(t, {__mode = 'k'})` still lets those entries be collected — + measured. Since our weakness is a storage form, changing the mode has to walk + the table and re-wrap in place, and a weak table must keep everything in the + hash part (array slots are plain `LuaValue`s and cannot hold a `WeakRef`). +* **`__gc` needs the object *alive*, which no JVM reference type can give you.** + Every "it's dead" notification (`WeakReference` cleared, `Cleaner`, + `PhantomReference`) arrives after the object is unreachable, and the reference + cannot be recovered. Only `finalize()` runs while `this` is still usable, and + it is allowed to *resurrect*. So a table with `__gc` gets a sentinel that only + it points to; when both go unreachable the sentinel's `finalize` pushes the + table onto a pending queue, and the next `collectgarbage` drains that queue on + the **Lua thread** — never on the JVM finalizer thread, where calling into a + `LuaState` would race and any exception would vanish without a trace. + +### Three known divergences from the reference + +These are not oversights; each is a place where the JVM has no primitive to +match, and each is documented rather than papered over. + +* **Ephemeron tables only handle the case where the value *is* the key.** + `__mode='k'` with `t[k] = k` clears the entry (matching the reference), and + the usual cache shape `cache[obj] = {fresh data}` keeps its value (also + matching). But `t[k] = {k}` — value *contains* key, key otherwise + unreachable — keeps the entry where Lua would drop it. The two halves are + mutually exclusive in Java: holding the value strongly keeps the key alive; + holding it weakly loses the value whenever the key is alive but the value is + not otherwise referenced, which is exactly the common cache. Data loss is + worse than retention, so we retain. +* **A weak entry whose key has a `__gc` is dropped one cycle early.** Lua marks + finalizable objects, so the entry survives the cycle that schedules the + finalizer. Ours keys off `WeakReference` clearing, which happens in the same + cycle. +* **Objects with `__gc` are only reclaimed when `collectgarbage` runs.** The + JVM never tells us an object is unreachable except by collecting it, so there + is no way to schedule a finalizer without an explicit cycle. + +### Phase 7 so far + +`string` (`StringLib`), `table` (`TableLib`), `math` (`MathLib`), `utf8` +(`Utf8Lib`), `os` (`OsLib`), `io` (`IoLib`) and `package` (`PackageLib`) are +ported, each with its own differential gate — `StringLibTest` (29), +`MathLibTest` (26), `TableLibTest` (18), `Utf8LibTest` (13), `OsLibTest` (24), +`IoLibTest` (35), `PackageLibTest` (22). + +#### Ten things the reference binary settled in Phase 7 + +* **A shared `lauxlib.c` layer has to come *before* any library.** Argument + checking (`checkString` / `checkInteger` / `argError` / …) is not scaffolding: + the message text is part of the observable contract, and six hand-rolled + copies would drift apart within a week. One detail is only right if the + parameter numbering is 1-based: `s:rep(nil)` reports `bad argument #1`, + because `luaL_argerror` decrements for a method call. +* **`math.max` / `math.min` use `lua_compare`, not a numeric comparison.** So + `math.max("a","b")` is `"b"`, and `math.max(1,"b")` raises + `attempt to compare number with string` via the failed `__lt` path. They also + preserve the operand's subtype: `math.max(1, 2.0)` is `2.0` (a float). +* **`math.fmod`'s float path is C's `fmod`, not Lua's `%`.** They differ in the + sign of the result: `math.fmod(-7.5, 2)` is `-1.5` (sign of the dividend) + while `-7.5 % 2` is `0.5` (sign of the divisor). Reusing the VM's `Arith.modf` + here is the easy mistake and it is wrong by a sign. +* **`math.log`'s base-2 and base-10 branches are genuinely different + computations.** C calls `log2` / `log10`; `log(x)/log(2)` differs from + `log2(x)` on 9 of 43 sampled inputs, and `log(x)/log(10)` from `log10(x)` on + 25 of 43. Java has no `log2`, so it is reconstructed as + `e + log(m)/ln2` from the mantissa decomposition `x = m * 2^e`, which lands + within 1 ulp on 190 of 192 inputs. +* **`table.insert` on an object with `__newindex` must call it.** `ltablib.c` + does not use `luaH_setint`; it uses `lua_seti`, which goes through + `luaV_finishset`. Writing raw leaves `table.insert(ud, v)` silently doing + nothing — no error, no write. +* **`os.execute` must spawn `sh`, not `/bin/sh`.** C's `system()` is + `execl("/bin/sh", "sh", "-c", cmd)` — **argv[0] is `sh`** — and the shell + prefixes its diagnostics with argv[0]. Spelling the full path turns + `sh: 42: command not found` into `/bin/sh: 42: command not found`, which the + differential gate sees immediately. +* **A userdata's metatable lives on the object, not on its type.** + `luaL_testudata` asks "is this userdata's metatable the one registered under + `FILE*`?", so file handles cannot share a per-type metatable with the + internal ones (the PRNG state, `gmatch`'s state) without those being mistaken + for files. `LuaUserdata` was added for this, and with it `__gc` — otherwise a + file left un-`close()`d by a script never gets closed at all. +* **`io` on a real file must be block-buffered, and `io.write` must not be.** + C stdio buffers file output: after `io.output(p):write("x")`, reading `p` + back gives *nothing*, because the bytes are still in the `FILE*`. Standard + streams are the opposite — they are line/unbuffered, and `io.write`'s output + has to reach whichever stream the host installed. So `IoLib` buffers + `RandomAccessFile` writes (flushing on `flush`, `seek`, any read, and close) + and writes straight through for stdin/stdout/stderr. +* **`require`'s searcher result is tested with `lua_isstring`, not "is a + string".** A searcher that returns the *number* `42` has its value + concatenated into the not-found message; only a non-string, non-number + result is treated as "this searcher said nothing" and drops the + `\n\t` prefix again. +* **Load-time errors run the chunk name through `luaO_chunkid` too.** The + module file is compiled as `"@" + path`, but `luaG_addinfo` strips the `@` + before printing, so a syntax error in a required module reads + `/tmp/m.lua:1: syntax error near 'is'` — not `@/tmp/m.lua:1: ...`. +* **Every library has to be registered in `_LOADED`, not just in `_G`.** + `coroutine` was the one that slipped through: `require("coroutine")` reported + `module 'coroutine' not found`, because the table was only in the globals. + +#### One divergence that is accepted, not fixed + +`sin` / `cos` / `tan` / `asin` / `acos` / `atan` / `exp` / `log` / `log10` / +`sinh` / `cosh` / `tanh` / `pow` are **not bit-identical** to the reference. +Java's `Math` is fdlibm; the reference binary links the platform libm (macOS +`libsystem_m` here). Both are correct to within 1 ulp of the true value, but +they round differently in the last place: of 43 typical inputs, `sin` differs on +1, `tan` on 12, `cosh` on 6. Closing that gap means shipping a libm of our own, +which neither fits the goal nor matches what Lua itself guarantees — Lua's own +results shift with the platform. The differential cases therefore compare these +functions to 14 significant digits (1 ulp is ~2.2e-16 relative). Everything +that *can* agree does agree: `sqrt`, `abs`, `floor`, `ceil`, `frexp`, `ldexp` +and the `log` branches above are bit-exact. + +#### Four divergences in `io` that are accepted, not fixed + +* **`file (%p)`.** `f_tostring` prints a pointer, so `print(io.stdout)` can + never match byte for byte. `IoLib` emits a same-shaped hex value and the + differential harness normalises `0x…` away. +* **`stdin` seeking.** The reference's stdin in the harness is redirected from + `/dev/null`, which *is* seekable (`lseek` returns 0); ours is a byte array, + which is not. `io.stdout:seek()` agrees (`nil, "Illegal seek", 29` on a + pipe); `io.stdin:seek()` is not compared, because the difference is in where + the stream comes from, not in what `io` does. +* **Writing to an update stream straight after reading it.** Where the bytes + land is C's read-ahead buffer, and ISO C leaves it undefined. Measured: 13 on + a 12-byte file, 4097 on a 6002-byte one, 5001 after `read(5000)`. We write at + the logical position. On the well-defined path (with an intervening `seek`) + the two agree. See *The `io.open` mode matrix* below. +* **The position reported after writing to an append file.** The reference gives + `base + n` while the data sits in the buffer and `EOF + n` once it is flushed + (the boundary is 4096); we always give `EOF + n`, which is the POSIX answer. + The bytes written are identical either way. See the same section. + +#### Two divergences in `package` that are accepted, not fixed + +`loadlib.c`'s fallback branch is the honest one here: there is no `dlopen` on +the JVM (reaching it would mean JNI or FFI — a different project), so the C +searcher behaves the way C does on a platform built without dynamic loading. + +* **`package.loadlib`** returns `nil, "dynamic libraries not enabled; check + your Lua installation", "absent"`. The reference binary is built with + `dlopen`, so it returns a `dlerror()` string and `"open"`. +* **`require` of a module whose `.so` actually exists** stops at "dynamic + libraries not enabled" instead of loading it. Modules that only exist as + `.lua`, and `package.preload` entries, are unaffected. + +### Phase 8 so far + +`load` / `loadfile` / `dofile` (`BaseLib`) and the binary-chunk pair +`lundump.c` / `ldump.c` (`Undump` / `Dump`) are in, with two differential +gates: `LoadTest` (25) and `DumpTest` (16). + +`DumpTest` compares at **two granularities**, and the second one is the real +gate for this phase: + +1. **Runtime.** Each script's stdout is compared byte for byte, as everywhere + else. +2. **Artefact.** `string.dump(f)` is written to a file by each side and the two + files are compared byte for byte. Runtime-only comparison would let "the + compiler emits something slightly wrong that still happens to run" through; + the artefact comparison pins down constant ordering, the line tables, local + and upvalue debug info, and `maxstacksize` as well. + +Eight shapes are checked that way — plain, nested, vararg, upvalue, `goto`, +loop, assorted constants, and a constant string longer than +`LUAI_MAXSHORTLEN` (which takes the non-interned long-string path on both the +dump and the undump side). + +#### Six things the reference binary settled in Phase 8 + +* **`luaU_undump` rewrites the chunk name before it ever reports an error.** + A leading `@` or `=` is dropped; a name that starts with the signature byte + (i.e. the chunk name *is* the binary chunk, which is what `load(s)` uses when + no second argument is given) becomes the literal `binary string`. So + `load("\27Lua")` reports `binary string: bad binary format (truncated + chunk)` — not an ESC-prefixed mess. +* **`f_parser` consumes the first byte on the binary path but hands it back on + the text path.** `luaU_undump`'s comment says so directly: "skip 1st char + (already read and checked)". Ungetting it before `undump` misaligns the whole + header by one byte and turns `truncated chunk` into + `not a binary chunk`. +* **`luaO_chunkid` has a third form, and it is the one `load` hits.** Only + `@x` and `=x` are stripped; anything else is wrapped as `[string "x"]`. + `load("x++")` reports `[string "x++"]:1: syntax error near '+'` — a + "strip the prefix" implementation that skips the wrapping reports `x++:1:`, + and every load-time error message is then wrong. +* **Chunk names are byte strings, and `luaO_chunkid` truncates by bytes.** + Decoding to a Java `String` first and truncating there gets the length wrong + on non-UTF-8 names. `chunkId` therefore has a `byte[]` core and the `String` + overload goes through it. +* **A prototype with no source gets *no* position at all — not even a line + number.** `luaG_addinfo` is `if (src) ... else return msg`. This is + observable: `string.dump(f, true)` strips the source, so + `pcall(load(string.dump(f, true)))` reports the bare `boom`. Falling back to + the chunk name not only adds a prefix, it splices the raw binary chunk into + the message (invalid UTF-8, so it decodes to replacement characters). +* **`lundump`'s `default: lua_assert(0)` is a no-op.** `lua_assert` compiles to + `((void)0)` in a default build, so a chunk whose constant type byte is `99` + loads fine and that constant is just `nil`. Raising an error there is + inventing a semantic of our own. Measured, not assumed: the hand-built chunk + loads as a `function` on the reference. + +One more, on the dump side: `dumpFunction` compares sources **by reference** +(`f->source == psource`), not by content. Short strings are interned so the two +agree, but a long source name is not interned and C writes it out again for +every nested function. Comparing by content would emit *less* than C does. + +### `string.pack` / `string.unpack` / `string.packsize` + +Ported as `StringPack` (the pack/unpack half of `lstrlib.c`), with `PackTest` +(21 scripts). This is the **only part of the standard library whose output +depends on the host platform**: `l` / `L` are C's `long`, `T` is `size_t`, `!`'s +default maximum alignment is `offsetof` of `LUAI_MAXALIGN`, and the default byte +order is the host's. Those are pinned to the platform the reference binary runs +on — LP64 + little-endian — because on a platform where `long` is 4 bytes the +reference itself emits different bytes. + +Two more things the reference binary settled here: + +* **`X` consumes the option after it.** `getdetails` calls `getoption` to learn + the next option's alignment, and that call advances the format string. So + `string.packsize("Xi4")` is `0`, and `string.pack("Xi4", 7)` is `""`. An + implementation that peeks without advancing returns `4` — which looks more + sensible and is wrong. +* **`str_pack`'s `lua_pushnil` marker is observable.** It exists to separate the + arguments from the string buffer, but it sits at index 2, so with no second + argument `string.pack("c2")` reports `string expected, got nil` rather than + `got no value`. + +### Phase 9 — the `debug` library, hooks included + +`ldblib.c` is in as `DebugLib`, together with the parts of `ldebug.c` that only +the introspection API needs (`lua_getstack` / `lua_getinfo` / `luaG_findlocal` / +`luaG_traceexec` / `luaG_tracecall`) and `lauxlib.c`'s `luaL_traceback`. +`DebugTest` (36 scripts) is the gate. All sixteen `debug` functions are in. + +`LuaLibs.openAll` (`linit.c`'s `luaL_openlibs`) also landed — before it existed, +"open the standard libraries" was ten calls that only the test driver knew the +order of. + +#### The hook machinery, and where its call points are + +`sethook` is not a library function that can be ported on its own: it is a hook +*dispatcher* whose call points are spread over the interpreter. Porting it meant +adding, in addition to `DebugLib.dHook` (`ldo.c: luaD_hook`): + +| Call point | Upstream | Fires | +|---|---|---| +| `LuaState.precallC` | `ldo.c` | `call` for C functions — **before** the C function itself runs | +| `DebugLib.dHookCall` from `Vm`'s `startfunc` and from `OP_VARARGPREP` | `ldo.c: luaD_hookcall` | `call` / `tail call` for Lua functions | +| `DebugLib.rethook` from `LuaState.poscall` | `ldo.c: rethook` | `return` for both kinds | +| `DebugLib.traceExec` from `Vm`'s `vmfetch` | `ldebug.c: luaG_traceexec` | `line` and `count` | + +plus `trap` on `CallInfo` and `hook` / `hookmask` / `hookcount` / `allowhook` / +`oldpc` on `LuaState`. The point of `trap` is that it is a *local* variable in +`luaV_execute`, so with hooks off the main loop does not even branch. + +#### Four more things the reference binary settled + +* **`lua_sethook` must call `settraps`.** The main loop's `trap` is a local that + is only re-read from the frame at a few points (`updatetrap`). Turning a hook + on *mid-execution* therefore does nothing at all unless every live Lua frame's + `trap` is set to 1 — which is what `settraps` does. Without it, + `debug.sethook(f, "l")` produces zero line events. +* **A hook error is *not* caught by the `pcall` it is reported for.** The `call` + hook for `pcall` fires inside `precallC`, *before* `pcall` establishes its + recovery point, so `pcall(function() ... end)` cannot catch an error raised by + a `call` hook on `pcall` itself. +* **`CIST_HOOKED` is tested before `isLua`** in `funcnamefromcall`. The frame + carrying the mark is often a **C** frame (the `call` hook for `pcall` is + reported on `pcall`'s own C frame), so testing `isLua` first never reaches it + — and the traceback line changes from `in hook '?'` to + `in function `. +* **Hooks can never yield.** `hookf` calls the user function with `lua_call`, + which goes through `luaD_callnoyield`, so the non-yieldable counter is non-zero + inside a hook. Measured: `coroutine.yield()` in a hook reports `attempt to + yield from outside a coroutine` on the main thread and `attempt to yield + across a C-call boundary` in a coroutine — in both cases the program or + coroutine dies. This is why the port does not implement `CIST_HOOKYIELD` at + all: it is unreachable from Lua. (`CIST_HOOKYIELD` exists upstream for *C* + hooks installed via the C API, which are called directly rather than through + `lua_call`.) + +#### One ordering bug found by the hook gate + +`allowhook` must be restored at the **protected-call boundary**, *after* the +message handler has run — not before. The handler runs at the throw point, where +`allowhook` is still 0 (that is what stops a traceback from re-entering the +hook). Restoring it earlier means the traceback triggers the hook again, which +raises again, which `applyErrMsg` turns into `error in error handling`. The +symptom: `debug.sethook(function() error("x") end, "c")` followed by an uncaught +error printed `error in error handling` instead of a real traceback. + +#### Five things the reference binary settled in Phase 9 + +* **Level 0 is the C frame of `getinfo` itself.** `lua_getstack(L, 0, &ar)` + succeeds and yields `what = "C", currentline = -1, name = "getinfo"`. The name + comes from `funcnamefromcall` reading the *caller's* frame, so it answers "what + was the caller calling", not "what is this frame called". Getting this off by + one shifts every `debug.getinfo(n)` result by a level. +* **`'f'` and `'L'` need `lua_rotate`, not a pop.** The result table is pushed + *after* `lua_getinfo`, so `lua_getinfo`'s own outputs sit *underneath* it. + `treatstackoption` is `lua_rotate(L, -2, 1)` followed by `lua_setfield(L, -2)`. + Doing `L.top--` instead pops the result table itself — the symptom is + `debug.getinfo(1)` returning something that prints as a function, and + `debug.getinfo(1).func` holding the table. +* **`pushfuncname` tries a *global* name first.** `error` is `global 'error'` as + far as `getobjname` is concerned (it is a `GETTABUP _ENV`), but a traceback + prints `function 'error'` — `pushglobalfuncname` looks the function up **by + value** in `_LOADED._G` and wins. Checking only `ar.namewhat` gets the + qualifier wrong on every C function that has a global name. +* **The message handler runs *before* the stack unwinds** — `luaG_errormsg` + calls it at the throw point, not from `luaD_pcall`'s recovery. This is the whole + reason `xpcall(f, debug.traceback)` shows the full traceback instead of just the + outer two frames. Java has no `longjmp`, but the *Lua* stack (`stack` / `top` / + `ci`) is still intact at the `catch`, so running the handler at the top of + `pcall`'s catch is equivalent. +* **`LEVELS1` / `LEVELS2` are 10 and 11**, not a round 10/10, and the head and + tail are *not* the same length. The "(skipping N levels)" count is off by one + otherwise, and only on stacks deep enough to trigger it. + +#### Two bugs found by the `debug` gate, neither of them in `debug` + +* **`OP_TESTSET`'s condition was inverted, which broke `and` / `or`.** Upstream + tests `l_isfalse(rb) == k`; this port tested `toBoolean() == (k != 0)` — and + those two are exact opposites over `k`'s two values. It is a *quiet* bug: + `TESTSET` only survives when the result register differs from `B` + (`patchTestReg` rewrites it to `OP_TEST` otherwise), so `if x then` was always + right while `x and y` was always wrong. The symptom is + `print(t and "yes")` printing the table, and — worse — `cond and a or b`, + the most common idiom in Lua, taking the wrong branch. **The bytecode gate + could not see it**: the emitted code was field-for-field identical to the + reference's. `RuntimeTest.logicalOperatorsReturnValues` now pins the returned + value, not just the truthiness. +* **`xpcall`'s handler ran after the call chain had already been restored.** + See the fourth bullet above; `xpcall(f, debug.traceback)`'s traceback was two + frames long instead of seven. + +#### One thing the reference binary settled that is a *driver* difference + +`lua.c` runs the script from inside `pmain`, a **C function**, so an extra C +frame sits under the main chunk. It shows up as the trailing `[C]: in ?` in every +traceback and as `debug.getinfo(3).what == "C"`. A host that calls +`luaL_dostring` directly has no such frame and sees neither. This is therefore +fixed in the differential driver (`Diff` now mirrors `lua.c`: `pmain` + +`docall` + `msghandler`), not in the engine — bending the engine to match would +have made `luaL_dostring` wrong instead. + +### Phase 10 — the upstream conformance suite, end to end + +The heaviest part of the fourth gate: `UpstreamSuiteTest` runs the upstream +`.lua` files unmodified and compares their output with the reference binary +**byte for byte**; `UpstreamSmokeTest` collects the ones that must not be +compared that way but still have to finish. + +| File | Why it cannot be compared byte for byte | +|---|---| +| `cstack.lua` | each case prints a `final count` = how much C stack one nesting level eats; Java and C necessarily differ | +| `gc.lua` | the whole file tests the incremental collector's `count` / `step` / `setpause`; this port has no such layer | +| `literals.lua` | the trailing locale section needs `os.setlocale("pt_BR")` to actually take effect; the JVM cannot | +| `locals.lua` | `tracegc` writes a progress dot from a finalizer with `io.stderr:write'.'`; when it runs is up to the JVM | +| `constructs.lua` | line 319 is `_ENV.GLOB1 = math.random(0, 1)`; two processes can never share a seed | +| `sort.lua` | it prints sort timings and a comparison count, and the input is generated with `math.random` | +| `math.lua` | it prints `random seeds`, the range of the random draws, and timings | + +#### What the reference binary settled in Phase 10 + +* **`__close` can still yield after an error.** When `yieldable(L)` holds, + `lua_pcallk` does **not** install a local recovery point: the error travels all + the way to `lua_resume`, and `precover` → `finishpcallk` → + `luaF_close(..., yy = 1)` finishes the job — so every `__close` may yield, and + a new error raised by a `__close` **replaces** the old one for the variables + still pending. In `locals.lua`'s "yielding inside closing metamethods", three + `` variables yield in turn; the middle one raises `error(err + 20)` + after yielding, so the third sees (and `pcall` finally returns) 30, not 10. + This port uses Java exceptions instead of `longjmp`, so the recovery point sits + in `pcall`'s `catch` and `yy` has to be passed down explicitly — `pcall` picks + it from `yieldable()`, which is exactly `k != NULL && yieldable(L)`. +* **When a yield interrupts the unwinding, somebody has to keep the error + object.** In C it is always on the stack (`luaD_seterrorobj` rewrites it before + every close, and `finishpcallk` reads `L->top - 1`). Here it lives in the + exception and vanishes with the Java stack, so it is parked in + `LuaState.closeErrPending` until the next `finishPcallK`. It must **yield to a + newer error**, or the third variable would still see the stale 10. +* **`luaB_error` has to be written in the C shape**, `lua_settop(L, 1)` + included: that call decides what sits one slot below the top, which is what + `luaD_seterrorobj`'s `default` branch reads. Without it, errors out of + `coroutine.wrap(...)` lose their `file:line:` prefix. +* **`luaL_where` and `luaG_addinfo` answer "no debug info" in opposite ways.** + The former needs `currentline > 0` and otherwise emits nothing; the latter + prints `?:-1:` anyway. Merging them breaks both `error("x")` and the + `^%?:%-1:` assertion in `errors.lua`. +* **`collectgarbage` validates its option first**, and `"generational"` / + `"incremental"` return the **previous mode as a string** + (`lbaselib.c: pushmode`), not an integer. +* **`luaD_shrinkstack`'s `stackinuse` is the max of `top` and every + `ci->top`**, not `top` alone, and `stackErr` is cleared only when a shrink + actually happens. +* **The main thread starts one slot up** (`stack_init` reserves it for + `base_ci.func`), `IoLib` must `lua_pop` after creating the std files, and + `Diff` must push `msghandler` onto the stack the way `lua.c: docall` does — + each is worth one level of recursion in `errors.lua`'s "expected stack overflow + after N calls". + +#### Four real bugs found while getting `sort.lua` / `math.lua` to finish + +Both files used to crash on entry; they only reach `OK` after these. None of them +is a "we differ" — all four were plain mistakes on our side: + +* **`tunpack`'s overflow test must compare as unsigned.** In C, `n` is + `lua_Unsigned`, so a wrapping `e - i` becomes a *huge* value; a Java `long` + wraps to a *negative* one, the signed comparison lets it through, and the loop + then pushes from `minI` to `maxI` until `table.unpack` dies with + `OutOfMemoryError`. +* **`top + n` overflows in `checkStack` / `checkStackOk`.** With an `n` of `2^31` + the sum goes negative, so the "over `MAXSTACK`" test stops firing — same + `OutOfMemoryError`. Now computed as `long`. +* **`tmove`'s test must not be simplified.** C asks `e < LUA_MAXINTEGER + f`, + *not* `e - f < LUA_MAXINTEGER`: the two agree only when nothing wraps, and + wrapping is exactly what is being tested. Simplified, + `table.move({}, minI, maxI, 1)` reports `#4 (destination wrap around)` where + the reference reports `#3 (too many elements to move)`. +* **`luaG_tointerror` carries a `varinfo` suffix.** For `math.huge << 1` the + reference says `number (field 'huge') has no integer representation`. +* **`tonumber` must not use `String.trim()`.** Java's `trim` strips "characters + at or below U+0020", which *includes `\0`*; C recognises only `SPACECHARS` + (`" \f\n\r\t\v"`) and additionally requires the parse pointer to land exactly + on `s + l`. So both `tonumber('1\0')` and `tonumber('1\0', 2)` silently + returned `1`. `b_str2int` is now written over bytes and re-checks the length. + +#### And one more: `table.sort` picks its pivot with *unsigned* arithmetic + +`sort.lua` lives in the smoke set — it prints timings, so its output can never be +compared byte for byte — and a later full run failed inside it with +`attempt to compare nil with number` out of `table.sort`. The file seeds nothing, +so `math.random()` hands it fresh data on every run, which makes this a *latent* +bug: it only surfaces when a partition comes out unbalanced enough for `auxsort` +to draw a new random pivot. + +* **`IdxT` is `unsigned int`, and so is the pivot seed.** C's `rnd % (r4 * 2)` is + an unsigned modulo, always in `[0, r4*2)`. Java's `int` is signed, and the seed + carries exactly the 32 bits C would have produced — negative half the time. A + signed modulo then drags `p` below `lo`, and at the extreme below `1`, where + `a[p]` is nil and `sort_comp` reports `attempt to compare nil with number`. + Now `Integer.remainderUnsigned`, matching C. +* **`rnd` is passed by value.** In C it is a parameter of `auxsort`, so + re-randomizing inside a recursive call never leaks back to the outer frame. It + used to be shared through an `int[]` here, which meant a seed drawn deep in one + subtree went on to steer every remaining partition of the whole sort. + +One run in twenty is a bad way to find a bug, so `choosePivot` now carries its own +deterministic test (`TableLibTest.choosePivotStaysInsideTheInterval`) that checks +the C `lua_assert` — `lo + r4 <= p <= up - r4` — directly, instead of hoping a +50 000-element sort trips over it. + +#### Phase 11: a precompiled chunk must load through *every* door + +`ldo.c: f_parser` reads one byte. If it is the first byte of `LUA_SIGNATURE`, the +chunk goes to `luaU_undump`; otherwise to `luaY_parser`. That single test is why a +caller never has to say whether it is handing over source or bytecode — and why +`load`, `loadfile`, `dofile` and `require` all accept precompiled chunks for free. + +The dispatch used to exist only inside `BaseLib`'s `load` / `loadfile`. `lua_load` +itself (`LuaState.load`) did not look at the signature, so **`require` of a +precompiled module failed**: + +``` +error loading module 'mod' from file '/tmp/mod.lua': + /tmp/mod.lua:1: unexpected symbol near '<\27>' +``` + +The reference loads it and returns the module. `PackageLib` reaches the loader +through `LuaState.loadFile`, which is why this showed up there and not in `load`. +The fix is at the `lua_load` layer, not a patch on `require`: + +* `LuaState.load` / `loadFile` now dispatch on `Lua.LUA_SIGNATURE0`; +* `loadFile` still runs the `lauxlib` preamble first (BOM, `#!`), because a + precompiled chunk never starts with `#` — order matters; +* the binary branch does **not** push the first byte back: `luaU_undump` says + "skip 1st char (already read and checked)", so `checkHeader` starts at byte two. + +`BinaryChunkTest` covers both directions: + +* **chunks produced by the real `luac`** (`ReferenceLua.compile`, located via + `LUAC54_BIN` or sitting next to the reference `lua`), run on both sides and + compared byte for byte. Self-compiled chunks would be grading our own compiler + with our own compiler, so the reference's `luac` is what produces them. The + corpus covers upvalues, varargs, `goto`, mixed table constructors, integer and + float edges, coroutines, a 300-entry constant table, and two scripts that *fail* + on purpose — the error messages carry line numbers and local-variable names, so + losing the debug info in `undump` turns them red; +* **chunks produced by our own `string.dump`** (`require`, `dofile`, `loadfile`, + the `mode` argument, the `binary string` chunk-name rule, and truncated or + tampered chunks, which must be load-time errors rather than crashes). + +#### And the fuzz corpus was 89% dead + +The generator had been emitting programs that do not compile: + +* table constructors wrote `{'key0' = v}`. Only a *name* can be a bare key, so a + string key needs `[k] = v` — the numeric case was bracketed, the string case was + not; +* `expression` emitted `(select('#', ...))` unconditionally, while a function + declares `...` only 40% of the time. Outside a vararg function that is + `cannot use '...' outside a vararg function`. + +Measured with the reference `luac`: **178 of 200 seeds failed to compile.** A +program that does not compile produces the *same* error on both sides, so the +comparison "passes" while proving nothing — the strongest gate had been quietly +vacuous over tens of thousands of seeds. Fixed (keys are always bracketed; the +generator tracks whether the function being built is vararg), and re-measured: +0 / 400. + +`FuzzTest.assertCompiles` now makes that state unrepresentable: it fails the seed +whose reference output has the shape of a compile error. The discriminator is +"the message ends in `near 'tok'`" rather than "contains `expected`", because +runtime errors are full of `expected` (`bad argument #1 to 'upper' (string +expected, got nil)`) and those are *supposed* to happen. + +#### Fuzzing the bytecode path too + +Ordinary fuzzing only ever takes the text path — `Dump` and `Undump` were touched +by a few dozen hand-written cases, which is exactly the blind spot the +`require`-a-precompiled-chunk bug lived in. `generatedChunksSurviveDumpAndUndump` +closes it: the generated body is wrapped in a vararg function, `string.dump`ed, +`load`ed back and run. That pushes thousands of *random* `Proto`s through +serialization — constant-table order and varint encoding, line info, local and +upvalue debug names, `maxstacksize`, nested protos. A sentinel print +(`dumped true loaded true`) is asserted on, so a silent `string.dump` failure +cannot masquerade as agreement. + +#### Five more things the reference binary settled + +The fuzz generator learned `table.*` and immediately paid for itself: five +divergences, none of which any hand-written case had hit. + +* **`l_str2int`'s accumulator is `lua_Unsigned`.** In `lobject.c` the running + value `a` is unsigned, and the overflow guard is `a >= MAXBY10`. With a signed + `long`, once `a` passes 2^63 it goes *negative* and the guard stops firing, so + the overflow escapes: `1 + (math.mininteger .. 0)` — the operand is + `"-92233720368547758080"` — wraps to exactly 0 and returns `1` instead of + falling through to the float path (`-9.2233720368548e+19`). Now + `Long.compareUnsigned`. +* **`luaG_runerror` adds `file:line:` where it is raised, not later.** `idiv` and + `mod` divide-by-zero go through `luaG_runerror`, which calls `luaG_addinfo` on + the spot. Ours threw a bare `LuaError` that only received its prefix at the + protection boundary, by which time the raising frame was gone — so + `coroutine.resume` returned the error as a *value* and it arrived without a + position: `false attempt to divide by zero` instead of + `false file:line: attempt to divide by zero`. `Arith.rawArith` now takes a + `LuaState` for exactly this reason. +* **`luaH_newkey` refuses nil values.** Its first line is + `if (ttisnil(value)) return; /* do not insert nil values */`. Assigning nil to a + key that does not exist creates no node and triggers no rehash. Creating the + node changes the computed *array-part size*, which is a direct input to `#t`: + `table.move({[1]=4.0}, 0, 2, 1)` leaves `#t == 2` in the reference and `0` + otherwise. The nil-key and NaN-key checks sit *before* that line, so + `t[nil] = nil` must still raise `table index is nil`. +* **`lauxlib.c: interror` tests `lua_isnumber`, not the type.** + `luaL_checkinteger` that cannot get an integer reports + `number has no integer representation` when `lua_isnumber(L, arg)` — true for a + string that parses as a number — and `number expected, got string` otherwise. + So `string.format("%d", "3.5")` is the former (`"3.5"` parses, 3.5 just is not + an integer) and `string.format("%d", "abc")` is the latter. The guard used to be + `v.isNumber()`, which made both the latter. +* **`%.14g` must round once, from the exact value.** Java's + `String.format("%.Nf")` does not round the *value*: it rounds + `Double.toString`'s shortest representation, i.e. twice. + `math.sqrt(math.maxinteger)` is exactly `3037000499.976049900054931640625`, + whose shortest form is `3037000499.97605`; the 15th digit is a 5, so the second + rounding carries and produces `3037000499.9761`. The reference prints + `3037000499.976`. `StringLib` already formatted through `BigDecimal` (whose + `new BigDecimal(double)` is the exact binary expansion); `luaO_tostring` had its + own copy built on `String.format`. Two copies of one C conversion is how they + drifted, so `Vm.formatG` now delegates to `StringLib.generalDigits`. + +That last one is worth stating plainly: measured over 4602 printed floats, the +old code was wrong on **12** — 0.26%. No hand-written case was ever going to find +it, and no amount of upstream-suite greenness would have either. It took the +fuzzer. `RuntimeTest.floatFormattingMatchesReferenceAcrossMagnitudes` now pins it +with a few thousand values, generated by an LCG *inside* the script so that the +random-number generator itself stays out of the comparison. + +#### And two more, from widening the corpus + +The generator then learned `utf8.*`, `gmatch`, `next`, `string.pack`/`unpack`, +`table.pack`, non-string error values, `math.ult`/`fmod`/`max`/`min`, and +`rawequal`/`rawlen`/`select`. Measured before trusting it: every new branch fires +in 6–29% of programs, so the corpus is actually alive. It immediately found two +more, both in `ltable.c` — the same file as three of the previous five. + +* **`luaH_getn` answers `limit - 1` before it ever binary-searches.** The manual + only asks `#t` to return *a* boundary, which makes "we found a valid boundary" + sound sufficient. It is not: real programs observe which one. `ltable.c` has a + fast path for the common case (`t[#t] = nil`, then ask for the length again): + + ```c + if (limit > 0 && isempty(&t->array[limit - 1])) { + if (limit >= 2 && !isempty(&t->array[limit - 2])) + return limit - 1; /* (1a) do not search */ + else { /* (1b) */ ... binsearch ... } + } + ``` + + An array of `[1, nil, 3, nil]`: binary search returns `1` (it finds the *first* + boundary), the reference returns `3`. `table.pack(1, nil, 3, nil)` leaves + `#t == 3`, not `1`. Across a systematic sweep of hole patterns — every single + hole up to `n = 32`, every hole pair up to `n = 19`, every triple up to + `n = 11`, all 32 `table.pack` nil combinations — **212 of 4968 results were + wrong (4.3%)**. + +* **`luaH_resize` must let the hash part shrink to *dummy*.** After + `rehash`, C's `reinsert` moves the old entries with `luaH_set`, so any integer + key `<= asize` is routed into the new array part and occupies **no hash slot**; + `luaH_resize(L, t, 4, 0)` therefore ends with a dummy (zero-slot) hash. This + port sized the hash to fit *all* the moved entries, so an array of 4 came with + 2 stale hash slots. That matters because `luaH_newkey` tests `isdummy(t)`: with + a non-dummy hash, the next insert just takes a free slot and **never rehashes**, + so the array part stops growing. `t = {}; t[2]=2; t[3]=3; t[4]=4; t[6]=6; + t[7]=7` gives the reference an array part of 8 and `#t == 7`; this port had 4 + and 4. The sweep above is what exposed it — the previous corpus only ever + inserted keys `1..n` in order, which never reaches this path. + +Both are pinned by `RuntimeTest.tableLengthBoundaryMatchesReference`, which builds +the hole patterns with loops inside the script (an LCG would put the generator +itself into the comparison) and reads `#t` twice per table, because C rewrites +`alimit` as a hint on the first call. + +#### One more, from running the corpus inside coroutines + +The same corpus was then also run through `coroutine.create`, which found a bug +that had nothing to do with coroutines: + +> `pcall(math.random, 5, 1)` inside a coroutine reported +> `bad argument #1 to '?' (interval is empty)`; the reference reports +> `bad argument #1 to 'math.random' (interval is empty)`. + +* **`_LOADED` lives at `global_State` level, not per-thread.** `lauxlib.c`'s + `pushglobalfuncname` reverse-looks-up the current function in + `package.loaded`, two levels deep, to turn `'?'` into a real name: + + ```c + static void pushglobalfuncname (lua_State *L, lua_Debug *ar) { + ... + lua_getfield(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE); + if (findfield(L, L.top - 1, 2)) { ... } + ``` + + `LUA_LOADED_TABLE` is a field of the *registry*, and the registry belongs to + `global_State` — every `lua_State` shares one. This port had made `loaded` a + per-`LuaState` table, so a coroutine's `package.loaded` was empty and every + diagnostic inside it degraded to `'?'`. The fix is one line in the + constructor: `loaded = main.loaded`. Note that this only shows up *inside a + coroutine* — the main thread populated its own table, which is why the whole + upstream suite and every earlier fuzz seed were silent about it. + +`RuntimeTest.libraryFunctionNamesResolveInsideCoroutines` pins it. + +#### The gate itself had a hole + +A systematic sweep of `string.format("%q", ...)` — every byte 0–255, in four +positions — was the first probe that wrote **non-UTF-8 bytes** to stdout. It found +nothing wrong with the engine, and something wrong with the harness: both sides +captured the reference's and this port's output into a Java `String` by decoding +UTF-8. Any invalid byte becomes `U+FFFD`, so `0x80` and `0xEF 0xBF 0xBD` compare +*equal*: + +``` +>>> b'"\x80"\n'.decode('utf-8', 'replace') == b'"\xef\xbf\xbd"\n'.decode('utf-8', 'replace') +True +>>> b'"\x80"\n'.decode('latin-1') == b'"\xef\xbf\xbd"\n'.decode('latin-1') +False +``` + +Measured rather than assumed: with `print` deliberately changed to round-trip +through a Java `String` (i.e. corrupting every non-UTF-8 byte), the new `%q` +sweep is **red under Latin-1 and green under UTF-8** — the old gate let a real +byte-level corruption through. The gate now decodes Latin-1, which is a +byte-for-char bijection, so comparisons are genuinely byte-exact. +`RuntimeTest.quoteFormatMatchesReferenceForEveryByte` is the pin. + +#### And one more, from sweeping operators against types + +The next probe was a full matrix: every operator × every pair of types, with no +metamethods, comparing the *error text*. 1651 lines. A second one did the same +with metamethods present — which of the two handlers runs, `__le` falling back to +`not (b < a)`, `__index`/`__newindex` in both table and function form, `__concat` +right-associativity, `__close` timing and error propagation (245 lines). The +second one was clean; the first found this: + +* **`luaG_tointerror` converts with `F2Ieq`, not "floor to integer".** + `lvm.h` defines `LUA_FLOORN2I` as `F2Ieq` by default — the name says *floor*, + the semantics are *"accepts only integral values"*: + + ```c + void luaG_tointerror (lua_State *L, const TValue *p1, const TValue *p2) { + lua_Integer temp; + if (!luaV_tointegerns(p1, &temp, LUA_FLOORN2I)) + p2 = p1; /* report the *first* operand if it cannot convert */ + luaG_runerror(L, "number%s has no integer representation", varinfo(L, p2)); + } + ``` + + Judging with a rounding mode makes `1.5` look convertible, so `1.5 & 3` + reported the **right** operand: `number (local 'b') has no integer + representation`, where the reference reports the left, `(local 'a')`. Ten lines + of the matrix were wrong. Hand-written cases only ever used `inf`/`nan`, where + both modes agree — which is why this survived every earlier gate. + +Both matrices are now permanent tests: +`RuntimeTest.operatorTypeErrorsMatchReference` and +`RuntimeTest.metamethodDispatchMatchesReference`. + +#### Two more, from sweeping the format specifiers + +`lstrlib.c: str_format` is one of the densest functions in the source, so the next +probe swept it: 22 verbs × 10 flag combinations × 5 widths × 9 precisions × 22 +value types — **20777 lines**. It found two things. + +* **The order of the two checks.** `str_format` does *not* validate the spec + first. For `d i u o x X` it calls `luaL_checkinteger` **before** `checkformat`, + and for `f e E g G` it calls `luaL_checknumber` first: + + ```c + case 'd': case 'i': flags = L_FMTFLAGSI; goto intcase; + ... + intcase: { + lua_Integer n = luaL_checkinteger(L, arg); + checkformat(L, form, flags, 1); + ``` + + (`c`, `a` and `s` are the other way round.) Validating the spec first meant + `("%.100d"):format("x")` reported `invalid conversion specification` where the + reference reports `number expected, got string` — **6620 lines wrong**. + +* **A precision turns off the `0` flag for integer verbs.** C99: *"For `d`, `i`, + `o`, `u`, `x`, and `X` conversions, if a precision is specified, the `0` flag + is ignored."* So `("%05.3d"):format(42)` is ` 042` — the precision pads to + `042`, then the width pads with **spaces** — not `00042`. 220 lines. Float verbs + have no such rule (`%08.3f` still zero-pads), so the fix is confined to + `emitInt`. + +`RuntimeTest.formatSpecifierMatrixMatchesReference` pins both; reverting either +one alone turns it red. + +#### And a second copy of `luaL_checkinteger` + +The next sweep was `os.date` specifiers, `os.time` argument checking, +`utf8.*` boundaries, and `tonumber` across bases 2–36 (854 lines). It found a +familiar shape of bug: **`luaL_checkinteger` had two implementations**, and the +older one in `BaseLib` was missing two things — the function-name prefix +(`bad argument #2 (...)` instead of `bad argument #2 to 'tonumber' (...)`) and +the "no integer representation" case, reporting `number expected, got number` +for `1.5`. Four call sites were affected: the base of `tonumber`, the index of +`select`, the level of `error`, and the index of the `ipairs` iterator. It now +delegates to `LuaAux.checkInteger`. + +That sweep also turned up a second instance of "not our bug, libc disagreeing": +**`os.date("!%z", t)`**. C's `strftime` reads the *process-global timezone*, +not the `tm` Lua hands it, and with the `!` (UTC) flag the two libc's answer +differently — macOS prints the local offset `+0800`, glibc prints `+0000`. The +test therefore only exercises `%z`/`%Z` **without** `!`, where both sides run on +the same machine and the answer is well-defined. + +`string.pack`/`unpack`/`packsize` also got a full sweep — every format verb +(including `i`/`I` widths 1–16, `c`/`s` counts, `X` and `!n` alignment, all four +byte orders) × values, plus invalid formats and out-of-range offsets, 706 lines. +It was clean; it is now a test so that "already agrees" stays agreed. + +#### One thing that is *not* a divergence: subnormal `%a` + +`quotefloat` formats through C's `%a`: + +```c +int nb = lua_number2strx(L, buff, MAX_ITEM, "%" LUA_NUMBER_FRMLEN "a", n); +``` + +C99 fixes one hexadecimal digit to the left of the point and says it "is `0` only +if the argument is denormalized" — which leaves *how* a subnormal is written +**unspecified**. glibc and Apple libc pick opposite answers: + +| value | glibc (CI: ubuntu) | Apple libc / musl | +| --- | --- | --- | +| `5e-324` | `0x0.0000000000001p-1022` | `0x1p-1074` | +| `3 * 5e-324` | `0x0.0000000000003p-1022` | `0x1.8p-1073` | + +This port matches **glibc**, because the gate that matters — CI — runs on +`ubuntu-latest`. `StringLibTest.subnormalQuoteFollowsGlibc` pins it with a plain +assertion *outside* the differential table, so that nobody "fixes" it by matching +a macOS reference binary and breaks Linux. (`Double.toHexString` already produces +the glibc form, so there is nothing to implement.) + +#### A second one that is *not* a divergence: `%a` rounding, and `inf`/`nan` padding + +Apple libc is simply **wrong** when `%a` is given a precision and the first dropped +hex digit is `8`. It is not round-to-nearest at all — its decision depends on the +*parity of the second dropped digit*: + +```text +("%.4a"):format(0x1.500082p0) -> 0x1.5000p+0 # 0x82 > 0x80, yet rounded down +("%.4a"):format(0x1.500081p0) -> 0x1.5001p+0 # ...and this one rounds up +``` + +glibc's rule, straight from `stdio-common/printf_fphex.c` plus +`include/rounding-mode.h`: + +```c +bool more_bits = ((next_digit_value & 7) != 0 || precision + 1 < numend - numstr); +do_round_away = round_away (negative, last_digit_value & 1, + next_digit_value >= 8, more_bits, mode); +/* FE_TONEAREST: half_bit && (last_digit_odd || more_bits) */ +``` + +i.e. round-to-nearest, ties-to-even — and when `precision == 0` the "last kept +digit" is the one left of the point, so `("%.0a"):format(1.5)` is `0x2p+0`, not +`0x1p+0`. `StringLibTest.hexFloatPrecisionRoundingFollowsGlibc` pins these with +plain assertions; the differential sweep skips every case whose first dropped digit +is `8` (the two sides skip exactly the same set — the predicate is computed from the +unrounded `%a`, which both agree on). + +Same file, one more: `inf` and `nan` are **not** zero-padded. The `special` branch +does `pad(buf, ' ', width)` unconditionally and never looks at `pad == '0'`, so +`("%020.5a"):format(math.huge)` is seventeen spaces followed by `inf`. + +#### One more, from sweeping the `table` library: `tmove` dropped a condition + +The `table` sweep — `insert` / `remove` / `move` / `concat` / `unpack` / `pack` / +`sort` × positions × sizes × offsets × value types, including same-table +overlapping `move` and cross-table `move`, **6277 lines** — produced 268 diff +lines, and every one of them had an unsettling shape: **the table's contents were +identical, only `#t` differed.** + +```text +< MV2-dump #0 -2=nil,-1=nil,0=nil,1=nil,2=1,3=nil,4=nil,... +> MV2-dump #2 -2=nil,-1=nil,0=nil,1=nil,2=1,3=nil,4=nil,... +``` + +The cause is a missing clause. C decides the copy direction like this: + +```c +if (t > e || t <= f || (tt != 1 && !lua_compare(L, 1, tt, LUA_OPEQ))) { + for (i = 0; i < n; i++) { ... } /* forward */ +} else { + for (i = n - 1; i >= 0; i--) { ... } /* backward */ +} +``` + +This port had only the first two clauses. The third one is not about positions at +all: **when the source and destination are different tables, the copy is always +forward**, regardless of how `t` and `f` relate. Dropping it sends cases that +should go forward down the backward path. + +Order does not change what ends up in the table — but it changes the *layout*, +which is what `#t` reads. `table.move({1}, -2, 1, -1, {1})` leaves the destination +with only `[2] = 1` either way; going forward clears `[1]` first and leaves +`alimit` at 1, so `luaH_getn`'s first case ("`array[alimit-1]` is nil ⇒ binary +search inside the array part only") fires and the answer is `0`. Going backward +sets `[2]` first, pushing `alimit` to 2, that case no longer applies, and the +answer becomes `2`. Hence the confusing symptom: the same sequence of writes +agrees when driven through `rawset` and disagrees when driven through +`table.move`, because only the internal layout differs. + +Two lessons went into the tests: + +* **Comparing table contents is not enough — `#t` has to be compared too**, or + layout bugs are invisible. +* The sweep is now three permanent tests: + `RuntimeTest.tableLibraryMatrixMatchesReference` (6277 lines, the one that + found it), `RuntimeTest.patternMatchingMatrixMatchesReference` (24819 lines of + pattern matching × subject × `init` × replacement form — **clean**, pinned so + that "already agrees" keeps agreeing), and + `RuntimeTest.tableLengthAcrossInsertionHistoriesMatchesReference` (3736 lines + that make *insertion history* itself the sweep dimension: every subset of + `1..n` × three insertion orders, fill-then-punch, `move`-built layout, + `insert`/`remove` alternation, negative/zero keys, bisection up to 40, + tail truncation — also clean). + +Reverting the one clause turns the first of them red. + +#### One more, from sweeping `debug` / `load` / `require`: `xpcall` skipped its check + +The next sweep — `debug.getinfo` (14 fields × levels 0–6), `getlocal`/`setlocal`, +the upvalue family, `traceback` (including inside a coroutine), `sethook` with 10 +masks plus the count hook, `load` (source / reader function / mode / env / binary +chunk), `package.path` / `cpath` / `config` / `searchers` and `require`'s odd +corners, `error` at levels 0–4, `xpcall`, `assert`, and error propagation through +coroutines, 289 lines — found **`xpcall` had lost its argument check**: + +```c +static int luaB_xpcall (lua_State *L) { + ... + luaL_checktype(L, 2, LUA_TFUNCTION); /* check error function */ +``` + +C checks that the message handler is a function *before* calling anything. This +port only checked that two arguments were present, so `xpcall(f, 5)` did not +raise — it called `f`, and when `f` errored it tried to use `5` as the handler: + +| | reference | this port, before | +| --- | --- | --- | +| `xpcall(f, 5)` | `false bad argument #2 to 'xpcall' (function expected, got number)` | `true false error in error handling` | +| `xpcall()` | `false bad argument #2 to 'xpcall' (function expected, got no value)` | `false bad argument #2 to 'xpcall' (value expected)` | + +The `io` sweep also went in — 32 open modes (including every invalid one), every +`read` format and byte counts 0–24, numeric reads, `seek`, `lines`, using a +closed file, `io.input`/`output`/`read`/`write`/`close`, append-mode `seek` +semantics, `r+` interleaving, a 5000-byte block, and the same file opened twice, +167 lines. It was clean, and it is worth having anyway: it is the only thing that +exercises the **errno layer** — a failed `io.open` must return +`"path: No such file or directory"` plus errno `2`, which C gets from `strerror` +and we have to match on our own. + +#### A second hole in the gate: tracebacks silently truncated the output + +The `debug` matrix was green on the first attempt — with the `xpcall` bug still in +the code. That was the gate failing, not the code passing. + +`Diff.assertMatches` normalizes by cutting everything from the first +`stack traceback:` onwards. That is deliberate: when a *script crashes*, the +message comes first and the traceback last, and our `ldebug.c` cannot be expected +to match it line for line. But when the traceback is **printed by the script** — +`debug.traceback()`'s return value — it lands in the *middle*, and everything the +script printed afterwards was thrown away. This script prints a traceback in its +fifth section and has 180 lines after it. + +The guard now rejects that case: if anything other than traceback lines (each +starts with a tab) follows the traceback, `assertMatches` fails and says to use +`assertFullMatches`, which does not strip. Thirteen existing tests turned out to +have tracebacks in their output; the refined check — content *after* the +traceback, not merely its presence — leaves all of them alone, because in those +the traceback really is last. + +This is the second time the gate has been found blind rather than the code wrong +(the first was UTF-8 decoding of captured output). A gate that quietly compares +less than it claims is worse than no gate: it reports green. + +#### One the fuzzer found, where the bug was in the *harness* + +At scale (16500 generated programs) the fuzzer produced a program whose stack +overflow was reported on **line 67** by the reference and **line 65** by this +port. The engine was fine; the harness was one stack slot short: + +```c + lua_pushcfunction(L, &pmain); /* to call 'pmain' in protected mode */ + lua_pushinteger(L, argc); /* 1st argument */ + lua_pushlightuserdata(L, argv); /* 2nd argument */ + status = lua_pcall(L, 2, 1, 0); /* do the call */ +``` + +`lua.c`'s `main` passes `pmain` **two** arguments, so the stack's initial `top` +is 3. `Diff.runLikeLuaC` passed one. One missing slot is carried all the way +down: the recursion that overflows the stack goes one level deeper, so the +innermost frame is a different function, so the reported line differs. + +The reason it took a while to see: it does **not** always differ. Each frame +costs `k + 1` slots (`k` = number of locals) and the depth is +`floor((limit - base) / (k + 1))`, so a base one slot off only changes the answer +when it happens to cross a division boundary. Measured across frame sizes: + +| locals `k` | 0 | 1 | 2 | 3 | 4 | 6 | 8 | 16 | 32 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| differed before | yes | yes | yes | no | no | yes | no | no | no | + +Trying one example would have looked fine. Fixing the harness makes all nine +match, and `RuntimeTest.stackOverflowDepthAndLineMatchReference` now sweeps them. + +(The other trap in that probe: `local function f() return f() end` is a **tail +call**, consumes no stack, and loops forever instead of overflowing — the +reference binary had to be killed after five minutes. The probe uses +`return 1 + f()`.) + +#### And three sweeps that came back clean + +Not every sweep finds something. `math` (every function × 30 boundary values +including ±inf, nan, −0.0, `maxinteger`, `mininteger`, `2^53`, `2^63`, `1/3`; both +return values of `modf`; `log`'s base; six kinds of argument error per function; +the seeded random sequence) plus the deterministic parts of `os`, **1423 lines**, +is clean. It stays in the suite for three reasons: + +* **The last bit of floating point.** `math.sin` and friends go through `libm`; + `Math.sin` need not agree bit for bit. Default `tostring` prints 14 significant + digits, which happens to hide it — the sweep proves it is hidden, rather than + never having been looked at. +* **`math.random`'s sequence** — xoshiro256\*\* pins it exactly, so a seeded run + must match number for number. +* **The `errno` / `strerror` layer** again, on `os.remove` / `os.rename`. + +Three things there cannot be tested at all: `os.clock` (elapsed time), a +`math.randomseed` with no argument (seeds from the clock), and `os.time` with no +argument (current time). + +One catch while landing it: `math.atan` and `math.atan2` are **the same** +`math_atan` in `lmathlib.c`, so which name shows up in the error message is +decided by whichever key `lauxlib.c: findfield` runs into first while walking +`package.loaded` — and that order comes from the string hash, which is seeded by +`luai_makeseed` (time-based). The reference binary is **nondeterministic** here: + +``` +$ for i in 1 2 3; do lua -e 'print(pcall(math.atan))'; done +false bad argument #1 to 'math.atan2' (number expected, got no value) +false bad argument #1 to 'math.atan2' (number expected, got no value) +false bad argument #1 to 'math.atan' (number expected, got no value) +``` + +Six `atan2` and two `atan` out of eight runs. We are deterministic (the hash is +not seeded) and that is a deliberate deviation. Sweeping the zero-argument error +name of **every** function in `math`, `string`, `table`, `os`, `io`, `coroutine` +and `debug`, eight runs each, `atan` is the only one that moves; the script now +normalises it before comparing. + +#### The `string` sweep: two bugs, and one thing the reference disagrees with itself about + +The non-pattern-matching half of the `string` library — `sub`, `byte`, `char`, +`rep`, `upper`, `lower`, `len`, `reverse`, `format`, `tonumber(base)`, `tostring` +(pattern semantics are covered separately by the 24819-line sweep) — **3921 +lines**, and it found two: + +**`__name` follows C string rules.** + +```c + int tt = luaL_getmetafield(L, idx, "__name"); + const char *kind = (tt == LUA_TSTRING) ? lua_tostring(L, -1) : ...; + lua_pushfstring(L, "%s: %p", kind, lua_topointer(L, idx)); +``` + +`lua_tostring` hands back a `const char *` and `"%s"` treats it as a C string, so +the name is cut at the first `\0`: with `__name = "A\0B"` the reference prints +just `A`, while we printed the whole `"A\0B"`. Lua strings are not +NUL-terminated, C strings are, and here C wins. Fixed with +`LuaString.toCString()`, for the places where the C code uses a value as a +`const char *`. + +**`%p` does not zero-pad.** The address was formatted `0x%016x`; the reference +prints `0x7ab30002c0`, with no leading zeros (neither macOS nor glibc pads). + +**And one thing that cannot be compared at all:** with the `+` or space flag on a +NaN, macOS libc prints no sign (`nan`) while glibc prints one (`+nan`). CI's +conformance job runs on ubuntu, this machine runs macOS, so the **reference +itself** differs between the two. The script strips NaN's sign before comparing; +the implementation stays glibc-compatible. + +#### The optional-argument sweep: two checks in the wrong order + +The previous sweep only tried zero or one argument. This one permutes the second +and third arguments too: for ~70 functions, every combination of `{nil, 1, "x"}` +from zero arguments up to that function's arity — **1866 lines**. It found two, +and both are the same mistake: **the order of the argument checks**. + +**`string.gsub`:** + +```c + int tr = lua_type(L, 3); /* just record the type */ + lua_Integer max_s = luaL_optinteger(L, 4, srcl + 1); + ... + luaL_argexpected(L, tr == LUA_TNUMBER || tr == LUA_TSTRING || + tr == LUA_TFUNCTION || tr == LUA_TTABLE, 3, + "string/function/table"); +``` + +The fourth argument is checked **first**. So `gsub(s, p, nil, "x")` reports +`bad argument #4 to 'gsub' (number expected, got string)`; we checked argument 3 +first and reported `bad argument #3 (string/function/table expected, got nil)`. + +**`tonumber`:** the three checks run `luaL_checkinteger(2)` → +`luaL_checktype(1, LUA_TSTRING)` → `luaL_argcheck(2 <= base <= 36, ...)` — the +range check comes **last**: + +| | reference | before the fix | +| --- | --- | --- | +| `tonumber(nil, 1)` | `bad argument #1 to 'tonumber' (string expected, got nil)` | `bad argument #2 (base out of range)` | +| `tonumber(1, 1)` | `bad argument #1 to 'tonumber' (string expected, got number)` | `bad argument #2 (base out of range)` | + +Three functions are deliberately absent: `os.time` and `os.date` read the clock +when their first argument is `nil`, `math.randomseed(nil)` seeds from the clock, +and `io.lines` with no argument reads stdin. + +#### The `package` / `require` / `load` sweep: two bugs, and one counter that has to be shared + +**311 lines.** `package`'s static structure, `searchpath`, every shape of +`require` (module returning `nil` / `false` / `true` / nothing, caching, a module +that raises, circular dependency, `preload` winning over disk, names with +slashes), and `load`'s `mode` / `env` / `chunkname` / reader functions. + +**`package.loadlib`'s third return value says *where* it failed, not *that* it +failed.** `ERRLIB` (the library could not be opened) gives `"open"`, `ERRFUNC` +(the symbol is not in it) gives `"absent"`: + +```c + lua_pushliteral(L, (stat == ERRLIB) ? "open" : "absent"); +``` + +We had it hard-coded to `"absent"`, copying the fallback branch for builds +without `dlopen` — but in that branch `lookforfunc` always returns `ERRLIB`, so +`"open"` is what it should have been all along. Code that uses the third value +to decide whether to keep looking picks the wrong branch. + +**A circular `require` does not report a bare `C stack overflow`.** It reports: + +``` +error loading module '_pk_b' from file './_pk_b.lua': + C stack overflow +``` + +because the parser and `luaD_call` **share one counter**: `#define enterlevel(ls) +luaE_incCstack(ls->L)`. So the 200th level is not reached at a `lua_call` — it is +reached in the middle of some `luaL_loadfile`, the error is caught by +`luaD_protectedparser`, `loadfile` returns a non-`OK` status, and `checkload` +wraps it. Counting call depth and expression depth separately gives the naked +message instead. The sweep pins the exact shape, module name included. + +Fixing that exposed the counter's other half: **the rollback**. `luaD_call` +increments `nCcalls` *before* the `try`-equivalent, so when `luaE_checkcstack` +raises, the increment is outside the `finally`; C gets it back from +`luaD_rawrunprotected`'s `L->nCcalls = oldnCcalls` at the `pcall` boundary, and +Java has no `longjmp` to fall back on. Without the rollback every C-stack +overflow leaks one level forever — measured with a `pcall`-recursion probe, the +quota walks 197 → 196 → 195 → 194 → 193, and upstream `errors.lua` starts +reporting `error in error handling` where the reference prints `OK`. Same for the +parser: the restore has to sit on `lua_load`, not on the binary/text dispatch +below it, or a failed `load` leaks two levels each time. + +**And one that cannot be compared:** `package.loadlib`'s failure *message* comes +from the platform `libc` (`dlopen(...)` on macOS, `cannot open shared object +file...` on glibc), and this implementation does not load dynamic libraries at +all — three different strings, so the sweep compares only that it is a string. + +#### The deep `io` sweep: `fseek` owes you `EINVAL` + +`io` was the thinnest of the ten standard libraries at 167 lines. This one adds +**124 lines** of output: sixteen legal `open` modes and nine illegal ones (which +*raise* rather than returning `nil` + message — `l_checkmode` wants `[rwa]%+?b?`), +every `read` format, how `write` renders integers versus floats, all three +`seek` origins and their out-of-range cases, `lines`, `setvbuf`, closing twice, +`io.output` / `io.input` switching, and binary safety with `\0` and invalid UTF-8. + +It found one thing: **a negative target position is `EINVAL`, not a generic I/O +error.** + +``` +reference: nil Invalid argument 22 -- seek("set", -1) +before: nil Input/output error 5 +``` + +`fseek` gives `EINVAL` for a negative resulting offset from *all three* origins +(`SET`, `CUR`, `END`), while Java's `RandomAccessFile.seek` throws *"Negative +seek offset"*, which falls through `errnoOf`'s catch-all into `EIO`. Seeking +**past** EOF is legal and returns the new position — easy to block by accident +while fixing this. + +One thing is deliberately absent: `print` writes to the C-level `stdout`, so it +is *not* redirected by `io.output` — a script that assumes otherwise produces two +different wrong answers. + +`fopen` on a **directory** used to be listed here as absent too, and that was a +mistake: it is a single POSIX rule (`open(dir, O_RDONLY)` succeeds, `O_WRONLY` +gives `EISDIR`, and the first read fails with `EISDIR`), it is the same on macOS +and glibc, and the JVM simply refused to model it. It is now modelled — see +*The directory bug* below. The one part that is genuinely not comparable is +`seek("end")` on a directory, which returns the directory's own size (64 here), +a filesystem fact rather than a Lua one. + +#### The `os.time` sweep: `lua_tointegerx` converts strings + +The 854-line sweep covered every `os.date` conversion specifier. This one covers +the half it did not: `os.time` with tables (leap-year carry, month/day/hour out +of range, defaults, ignored extra fields), `difftime`, `getenv`, `remove` / +`rename`, `tmpname`, `execute`'s exit codes, `setlocale`. + +**The fields of a date table are read with `lua_tointegerx`, which converts +strings.** + +```c + lua_Integer res = lua_tointegerx(L, -1, &isnum); /* cvt2num: strings too */ +``` + +So `os.time{year = "2000", month = 1, day = 1}` *is* the year 2000; we reported +`field 'year' is not an integer`. Library arguments go through the coercing +`luaL_check*` family, while the VM's arithmetic instructions deliberately do not +coerce — two different rules, easy to write as one. + +The sweep also pins down that `month` and `day` are **mandatory** (`field 'month' +missing in date table`; `year` defaults and `hour` defaults to 12), that a field +too large for the `int` range reports `field 'x' is out-of-bound`, and that extra +fields (`yday`, `wday`, …) are ignored. + +One thing is deliberately absent: extreme years such as `year = -100`, because +`mktime`'s representable range is libc-specific (macOS reports *"time result +cannot be represented in this installation"*). + +Removing a **directory** used to be absent too, on the grounds that `ENOTEMPTY` +is 66 on macOS and 39 on glibc — but "the number differs per OS" is a reason to +*key off the OS*, not to skip the case. It is now compared, with `ENOTEMPTY` +resolved from `os.name`; see *The errno shape table* below. (Skipping it had +hidden that the errno was wrong outright: it was hard-coded to 39 everywhere.) + +#### The weak-table / `__gc` / `` sweep: `isdecGCmodegen` + +This is the semantically riskiest corner of the whole project: **there is no +collector** (`lgc.c` is not ported), so weak tables are rebuilt on +`WeakReference` and `__gc` on `Cleaner`. It deserves more than twelve hand-written +tests. The sweep covers weak keys / values / both, keys that are still strongly +reachable, mutually-referencing weak values, values that are not collectable at +all (strings, numbers, booleans), every spelling of `__mode`, `__gc` on one and on +twenty objects, `__gc` raising, `__gc` added *after* `setmetatable` (which C +ignores — it only checks at that moment), and ``'s ordering and error +argument. + +It found one thing: **`collectgarbage("generational")` / `("incremental")` report +`isdecGCmodegen`, not `gckind`.** + +```c +#define isdecGCmodegen(g) (g->gckind == KGC_GEN || g->lastatomic != 0) +``` + +`lastatomic` is non-zero after the first real collection, and loading a script +allocates, so user code starts out looking *generational* even though `gckind` is +`KGC_INC`. Five consecutive switches in the reference: + +``` +1 generational 2 incremental 3 incremental 4 generational 5 incremental +``` + +Tracking only `gckind` gets the **first** one wrong. Both switches clear +`lastatomic`, so afterwards `gckind` alone decides. + +Two things cannot be compared: `collectgarbage("count")` (nothing to count +without a collector) and `collectgarbage("step")`'s return value — a single step +in the reference never finishes a cycle, so it is always `false`; with no +incremental collector one step *is* a full round, so we return `true`. +Deliberately: returning `false` would make the `repeat until +collectgarbage("step")` idiom spin forever. + +#### The `debug` × coroutine sweep: `_LOADED` lives in the registry + +Both sides had been swept separately (114 and 99 lines), but not the +**cross-product**: `getinfo` / `getlocal` / `traceback` / `sethook` *on a +coroutine*, hook **counts** (printed as a number instead of one line per event — +the count itself is a strong check on the line table and the call sequence, and it +pins the `tail call` hook event), registry / `uservalue` / `setmetatable`, and the +rest of the coroutine state machine. + +It found one thing: **`_LOADED` has to be a field of the registry.** + +```c +#define LUA_LOADED_TABLE "_LOADED" /* lauxlib.h */ +``` + +`package.loaded` is not stored somewhere else with a copy in the registry — it +*is* the registry field, so `debug.getregistry()["_LOADED"]` must return it. We +kept it in a `LuaState` field only, so `type(debug.getregistry()["_LOADED"])` +gave `nil` where the reference gives `table`. Now it is read back out of the +registry, so a script that swaps `_LOADED` swaps ours too. + +#### The error sweep: two things about `__name` + +208 lines on one axis that no earlier matrix had touched: `error(msg, level)` for +`level` from `-1` to `math.maxinteger`, error **objects** of every type (and what +`print` / `tostring` / `string.format("%s")` do to them), the `luaL_where` +position prefix in every syntactic position that can raise (table constructor, +numeric and generic `for`, all twelve metamethods, a `gsub` replacement, a `sort` +comparator, a coroutine, ``, a method, an `__index` chain, a tail call, a +`load`ed chunk with `=` / `@` / string names), the shape of `xpcall`, errors +inside hooks, and chunks dumped with `strip`. + +It found two things, both in `luaT_objtypename`: + +```c +const char *luaT_objtypename (lua_State *L, const TValue *o) { + Table *mt; + if ((ttistable(o) && (mt = hvalue(o)->metatable) != NULL) || + (ttisfulluserdata(o) && (mt = uvalue(o)->metatable) != NULL)) { + const TValue *name = luaH_getshortstr(mt, luaS_new(L, "__name")); + if (ttisstring(name)) /* is '__name' a string? */ + return getstr(tsvalue(name)); /* use it as type name */ + } + return ttypename(ttype(o)); /* else use standard type name */ +} +``` + +**`__name` is a C string.** `getstr()` hands back a `const char *` and +`luaG_typeerror` interpolates it with `%s`, so it is cut at the first `\0`: +`__name = "A\0B"` reports `attempt to perform arithmetic on a A value`. We +returned the whole byte array. (The same rule was already applied on +`luaL_tolstring`'s path — this was the second copy of the bug.) + +**Only tables and (full) userdata consult `__name`.** Everything else falls +straight to `ttypename`, *type metatable and all*: after +`debug.setmetatable(nil, {__name = "NilT"})`, `nil + 1` still reports +`... on a nil value`. Our lookup went through `luaT_gettmbyobj`, which also +returns the *type* metatable — so we reported `NilT`. + +Four more things the sweep pinned down, none of which were bugs: + +* **`debug.traceback` returns a non-string, non-`nil` message untouched** + (`ldblib.c: db_traceback`), so `xpcall(f, debug.traceback)` with a table error + object yields the *table*, not a traceback. And `lua_tostring` does **not** + honour `__tostring` — which is exactly why `lua.c`'s `msghandler` has to call + `luaL_callmeta(L, 1, "__tostring")` by hand. +* **The line hook fires before the first instruction of the line**, so + `print(pcall(f))` written on the same line does *not* protect the hook's error; + the call hook fires for `print` and `pcall` themselves. +* **`xpcall` drops the handler's second result**: `ldo.c: dothecall` calls it as + `luaD_callnoyield(L, L->top - 2, 1)` — `nresults` is hard-coded to 1. +* `coroutine.wrap`'s error carries **two** position prefixes; `pcall(error, "x")` + has **none** (a C function has no line); a library error **does** have one, + because `luaL_error`'s `luaL_where(L, 1)` asks for the *caller*. + +#### The NUL sweep: thirteen places C cuts a Lua string at `\0` + +One axis, one rule: a Lua string is a byte string, but every C API that takes a +`const char *` — `strlen`, `%s`, `strstr`, `strchr`, `strcmp` — stops at the +first NUL. 88 lines, and it found thirteen divergences, all of them the same bug +in thirteen different doorways: + +| cut at the first `\0` | chunk name (`luaS_new`), module name and `package.loaded`'s key, all four `searchpath` arguments, `io.open` / `io.lines` / `os.remove` / `os.rename` / `os.getenv` filename, `fopen`'s *mode*, `debug.getinfo`'s `what`, `debug.sethook`'s mask, `luaL_traceback`'s message, `luaL_checkoption` (`collectgarbage`), `string.pack`'s format, and `__name` | +| --- | --- | +| **not** cut | `string.format("%s")` (goes through `luaL_tolstring`, which carries the length), `error`'s message (`lua_concat`), `os.date` (it iterates `s + slen`), table keys, `gsub`, `upper` | +| **rejected** instead of cut | `%s` *with modifiers*, and `string.pack`'s `z` — `luaL_argcheck(L, l == strlen(s), arg, "string contains zeros")` | + +Two of them are worth calling out. + +**`os.remove("/x\0y")` crashed the whole process.** `Paths.get` throws +`InvalidPathException`, which is a `RuntimeException` — not a `LuaError`, so +`pcall` cannot catch it (only `LuaError` is) and it escapes all the way out. +Truncating to C-string semantics removes the case entirely; the `catch` now also +covers `RuntimeException` as a backstop. + +**`__name` was the second copy.** `luaL_typeerror` and `luaG_typeerror` both go +through `luaT_objtypename`, but `typeArgError` had its own `metafieldOf(v, +"__name")` lookup — which got *both* rules wrong (no truncation, and no +table/userdata restriction). It now calls `LuaTm.objTypeName` like the other two. + +The two `debug` ones are the subtlest: `debug.getinfo(f, "S\0ln")` must **not** +report `invalid option` (C stops scanning at the NUL, so it never sees `l` and +`n` at all), and `debug.sethook(f, "l\0c")` installs only the line hook while +`"c\0l"` installs only the call hook — the counts come out inverted. + +#### …and a second NUL round: `load` / `loadfile` / `io.output` + +Auditing every remaining `.toJavaString()` in `src/main` turned up three more +doorways — `load`'s *mode* (`checkmode` compares `const char *`), `loadfile`'s +filename, and the filename handed to `io.output` / `io.input`. So +`load(src, name, "t\0b")` has mode `"t"`, and `load(src, name, "b\0t")` tries to +undump a text chunk and reports `attempt to load a text chunk (mode is 'b')`. + +It also found one place where C and Java simply disagree about a path: +**`loadfile("")`.** C's `fopen("")` fails with `ENOENT`, giving +`cannot open : No such file or directory`; Java resolves the empty path to the +current directory and fails while *reading*, giving `Is a directory` — which we +turned into `Input/output error`. The empty path is the only case where the two +judge a path differently, so it gets `ENOENT` by C's rule. + +Directories were claimed here as "fine, must not be fixed" — and that claim was +wrong, which is the point worth keeping: **it was written from reading the C +source, not from running the reference.** `loadfile("/tmp")` does succeed at +`fopen` in C and fail at *read* — but this implementation was reporting +`cannot open /tmp: Input/output error`, i.e. the *other* branch, because Java's +`Files.readAllBytes` throws at open. Measuring it (see *The directory bug*) turned +one wrong sentence into a real bug across five entry points. + +#### A sweep that came back clean: `load`'s reader contract + +54 lines on `lua_load`'s `lua_Reader`, which nothing had swept: one shot versus +one byte at a time versus arbitrary chunk splits, **empty strings are *not* the +end** (`Zio` asks again; only `nil` terminates, so `{"", "", src}` compiles as +`src` while `{"", ""}` compiles as an *empty chunk*), a reader returning +`true` / a table / a function terminates the load, a reader that raises +propagates even when half the source is already consumed, a reader cannot +`yield` (`lua_load` uses `luaD_callnoyield`), and `load` — unlike `loadfile` — +does **not** eat a BOM or a `#!` line (that belongs to the loading layer, not the +lexer). + +It found nothing, which is itself the point: the reader is where a byte-stream +translation is most likely to lose or duplicate a byte. + +#### Two sweeps that came back clean: coroutines, and numeric `for` with coercion + +**Coroutines (99 lines):** which boundaries `yield` may cross, the semantics of +`close`, how errors propagate, the four states, and nested `resume`. The split, +measured and matching: + +| may `yield` | `pcall` / `xpcall`, numeric and generic `for` (custom iterators too), `gmatch`, `__index` / `__newindex` / `__add` / `__lt` / `__concat` / `__len` / `__call` / `__close` | +| --- | --- | +| may not | a `table.sort` comparator, a `string.gsub` replacement function, `__tostring` inside `print` — `attempt to yield across a C-call boundary` | + +It also pins down that a coroutine is **dead** (not suspended) after yielding +inside `__eq`, that closing yourself reports `cannot close a running coroutine`, +that `` variables are closed in **reverse order of declaration**, and that +an error raised inside `__close` replaces the original error. + +**Syntax and scope (121 lines):** sixty-odd fragments pushed through `load()`, so +**compile-time** errors are part of the comparison too — `goto` into a local's +scope, duplicate labels, assigning to a ``, `break` outside a loop, +malformed numerals and escapes, the various shapes of function names. Nothing a +generated program will ever produce. The runtime half covers "the right-hand side +is evaluated first" in multiple assignment, table-constructor evaluation order and +key overwriting, varargs with `nil` mixed in, malformed uses of `:`, upvalue +sharing, `repeat`'s condition seeing locals declared in the body, and operator +precedence and associativity. + +**`debug` and `utf8` (114 lines):** every `what` letter of `getinfo` alone and in +combination, out-of-range `getlocal` / `setlocal` / `getupvalue` / `setupvalue`, +all four `sethook` masks, the **shape** of `traceback` (not compared wholesale — +it contains chunk names and addresses), and the boundary cases of all five `utf8` +functions. + +**Numeric `for` and coercion (551 lines):** all four `forprep` / `forloop` +branches (integer up/down, float up/down), including `maxinteger` bounds, +strings as limits, a zero step, and float steps; plus `//`, `%`, bitwise +operations and string coercion. Three things worth remembering: in +`for i = 0, 1, 0.5` the first value is an **integer** `0` (with `1.0` as the +limit it is float throughout); bitwise operators do **not** coerce strings +(`"10" // 2` is 5 but `"10" & 1` errors); and `for i = 2^53, 2^53+2` / +`for i = 1e300, 1e300` never advance under float precision — the probe has a +20000-iteration guard so both sides bail out at the same place. + +#### The audit that found a whole missing function: `warn` + +This one is different from every sweep above: those look for *divergences inside +functions I already wrote*. Cross-checking `base_funcs[]` in `lbaselib.c` against +`BaseLib.open` line by line found that **`warn` was not implemented at all** — +with 828 tests green, because not one of them ever called it. A behaviour matrix +is blind to an absent function by construction. + +So there are now two things: the implementation, and a **permanent gate** that +compares the *symbol set* (190 lines: every global, every key of all nine +libraries, the string metatable, `package.loaded`, each printed as `name type`). +Values are not compared — they are addresses, and after normalisation nothing is +left, which would quietly turn the test into the empty case `assertFullMatches` +already rejects. + +The same audit caught **`_VERSION`**, a second whole gap. It is `"Lua 5.4"`, not +`"Lua 5.4.7"` (`LUA_VERSION` has no release number; `LUA_RELEASE` is what +`lua -v` prints). It matters more than it looks: real projects probe +`if _VERSION == "Lua 5.1" then …`, so a missing `_VERSION` takes the `else` +branch — silently wrong rather than loudly broken. + +The third hit, `arg`, is **not** a library gap: `lua.c` builds it in `createargtable` +after `luaL_openlibs`. Since the differential driver already imitates `lua.c`'s +shape (`pmain`, `msghandler`, the extra stack slot), it now imitates this too — +otherwise no script that reads `arg[1]` can ever be compared. + +Implementing `warn` meant porting `lauxlib.c`'s three-state machine, and the +probe caught three things that a `warn("@on")` test can never reach: + +| rule | symptom if missed | +| --- | --- | +| `checkcontrol` requires `tocont == 0` — so `warn("@on", "x")` prints `@onsecond` literally | "on" swallowed as a control message | +| only **OFF and ON** consult `checkcontrol`; `warnfcont` writes straight through — `warn("a", "@on")` prints `a@on` | a second `Lua warning: ` prefix inside one message | +| the message is a `const char *` and `lua_writestringerror` writes **bytes** | `warn("@on\0junk")` stops turning warnings on; `warn("\255")` comes out as `0xC3 0xBF` | + +That last pair is the NUL rule and the byte rule from earlier sections, arriving +together in one function — which is the reason to sweep by *contract* rather than +by example. + +#### What the symbol gate caught next: `f:write("")` on a read-only file + +The gate had a hole of its own: it compared the `io` table, so it never saw the +**seven file methods** — `close`, `flush`, `lines`, `read`, `seek`, `setvbuf`, +`write`. They live in `getmetatable(io.stdout).__index` (alongside `__gc`, +`__close`, `__name` = `FILE*`, `__tostring`), not in `io`. A symbol audit has to +follow `__index` to be worth anything; it now does. + +Probing those methods then turned up a real divergence: +**`fwrite(s, 1, 0, f)` returns 0, which equals `l`, so writing zero bytes always +succeeds** — `f:write("")` on a *read-only* file returns the file itself rather +than `nil, "Bad file descriptor", 9`. Our `write` checked `!writable` before +looking at the length. + +Only one cell of the grid exposes it, which is the argument for sweeping grids +rather than cases: + +| | writable file | read-only file | +| --- | --- | --- | +| `f:write("")` | file (both) | **file** / ~~`nil, msg, 9`~~ | +| `f:write("a")` | file (both) | `nil, msg, 9` (both) | + +Read is asymmetric and was already correct — and must stay that way: `f:read(0)` +does not go through `read_chars` but through `test_eof` (`getc` then `ungetc`), +so it *does* fail on a write-only stream, and `read` short-circuits on +`done >= len` before it ever checks `readable`. + +Also measured and matching: `flush`, `setvbuf` and `seek` all succeed on a +read-only stream; `io.stdout:close()` gives `nil, "cannot close standard file"`. + +#### Two sweeps over *every* library function at once + +The `f:write("")` bug was a **return-arity** bug, and it was found by hand. So +there are now two mechanical sweeps that apply one uniform input to all ~130 +library functions and compare success/failure, the *type* of every return value, +and the error text: + +* **zero arguments** (~120 lines) — the "missing argument" path; +* **one wrong-typed argument** (`{}`, `nil`, `1`; 359 lines) — the "type error" + path, which is a *different* C function in `lauxlib.c` (`luaL_check*` versus + `luaL_opt*` treat absence differently). + +The zero-argument sweep found nothing new. The wrong-argument sweep came back +clean too. They are kept because they pin ~370 call shapes at once: change any +error text, any result count, or drop one argument check and they go red, in +corners no hand-written matrix visits. + +Four things have to be excluded or normalised, each learned the hard way: + +| | why | +| --- | --- | +| `os.exit` | really kills the process | +| `io.*`, `dofile`, `loadfile` | touch the default streams or read stdin, which is not the same on both sides | +| `debug.debug()` | enters the interactive prompt (`lua_debug>`) and reads commands | +| `math.atan` / `math.atan2` | **the same C function** (`{"atan", math_atan}` and `{"atan2", math_atan}`), so which name `findfield` reports depends on `lua_next` order — and the string-hash seed is randomised per process. Six reference runs: three `atan`, three `atan2`. The reference disagrees *with itself*; the name is normalised before comparing. | + +Values are never printed, only `type()`: `os.time()`, `os.date()` and +`math.random()` differ on every run otherwise. + +#### The hook grid: masks × count × thread + +The existing hook tests were one mask per test; this one is the **full grid** — +eight masks, three counts, four bodies, plus per-thread installation — because +several semantics only show up in a cell: + +* `makemask` uses `strchr` and knows only `c` / `r` / `l`: `"x"` and `"CC"` are + **empty** masks (and it is case-sensitive). Reading only `"crl"` never reveals + this. +* **`lua_sethook` turns the hook off entirely when `mask == 0`** — so + `debug.sethook(f, "")` installs *nothing* and `debug.gethook()` returns `nil`, + rather than a live hook that never fires. Likewise `debug.sethook(f, "x")`. +* `count` joins `LUA_MASKCOUNT` only when `> 0`: `0` and `-1` produce no count + events, while `""` with count `1` is *count-only*. +* hooks are **per thread**: a line hook on the main thread never fires inside a + coroutine, and `debug.sethook(co, …)` is that coroutine's alone. +* `gethook`'s mask string **excludes** count (`unmakemask` emits only `c` / `r` / + `l`); count is the third return value. + +Two shape constraints matter as much as the assertions. Events are collected and +printed at the very end — with hooks on, every `print` produces its own +`call` / `return` pair and drowns the result. And the hook body spans several +lines and calls a function: if `allowhook` re-entry protection were broken, this +would be infinite recursion rather than a comparison failure. + +It came back clean. + +#### The integer / float subtype grid + +In 5.4 a "number" is one of two **subtypes**, and `math.type` can see the +difference. This is the single most likely thing to get wrong when porting 5.4: +a "everything is a double" implementation runs most scripts fine and diverges +quietly in `//`, `%`, `string.format("%d")` and `math.tointeger`. So every door +in the standard library that can hand back a number is swept, comparing only the +subtype — values are never printed (`os.time` and `math.random` differ per run). + +156 lines, in two halves: everyday shapes (the math library's +"integer in, integer out; float in, float out", four ways of writing `tonumber`, +the bitwise operators, every `string.unpack` width, `io`'s `seek`, `os.date`'s +fields) and the boundaries (`maxinteger + 1`, `mininteger // -1`, +`math.fmod(1, 0)` and `1 // 0` **raise** rather than returning nan, +`math.floor(-0.0)` is an integer, `math.tointeger(2^63)` is `nil`). + +Every line is wrapped in `pcall` — that is how the two raising cases were found. +Without it the script dies at the first one and the remaining hundred lines are +never compared, while the test still looks green. + +The first version of this sweep had a bug worth recording: merging the two probe +scripts left the second half calling the *first* half's helper, which took a +value instead of a thunk — so 72 of 156 lines silently read `tag nil` and the +test was green anyway. **A matrix whose lines are all `nil` proves nothing**; +check the reference output for uniformly-empty lines before believing a clean run. + +#### The subprocess sweep: `luaL_pushfail` is `nil`, not `false` + +`io.popen`, `io.tmpfile` and `setvbuf` were the thinnest part of the `io` library — +one or two cases each. Four scripts (read/write pipes, mode validation, `tmpfile`, +every `setvbuf` mode) widened that, and the very first run bit: + +``` +e3 true nil exit 3 <- reference +e3 true false exit 3 <- ours +``` + +`luaL_execresult` pushes `luaL_pushfail` when the child exited non-zero, and in +5.4 that macro is **literally `lua_pushnil`** +(`lauxlib.h: #define luaL_pushfail(L) lua_pushnil(L)`) — it has been since 5.4 +split "fail" from `false`. This was invisible for the whole project because +`os.execute` has its own copy of the logic and got it right; only `io.popen`'s +teardown goes through the shared `execResult`, so `popen` was the only door that +could show it. **Two implementations of the same helper means the bug hides in +the one nobody tested.** + +Three things here are measured as uncomparable, and each was measured rather than +assumed away: + +* **A child's stdout / stderr has to be redirected inside the command.** The + inherited descriptors do not end up in this implementation's capture buffer (a + Java child inherits the JVM's real stderr), so the two sides cannot line up. + Hence `cat > /dev/null` and `2>/dev/null`, not `cat`. +* **Closing an undrained pipe is a race.** The reference gets `nil, "signal", 13` + (`echo` died of SIGPIPE); we get `true, "exit", 0`. Which one you get depends on + whether the child finished writing first — it is not stable run to run and must + not be compared. +* **`setvbuf` with a negative or huge size is libc-defined.** C hands the size to + libc as a `size_t` and whether the buffer can be allocated is libc's call: on + the same machine `2^31` fails while `2^40` succeeds, and `"no"` with `-1` + succeeds while `"line"` with `-1` fails. Not modelable on the Java side. + +#### The success path: a third matrix, over every library function + +The two earlier sweeps over *every* library function asked two questions: does the +function **exist** (the symbol audit), and what does it do when you hand it the +**wrong** arguments (the arity and wrong-type sweeps). Both go through +`luaL_check*` / `luaL_opt*` — the checking branch. Neither touches the code that +runs *after* the checks pass. A `string.rep` that computes the wrong thing would +have stayed green under both. + +So there is now a third matrix: hand every library function a set of **valid** +arguments and compare the return **values**, not their types. 209 lines in three +scripts — `string` and `math` (68), `table`/`utf8`/`os`/`coroutine` (44), +`io`/`debug`/the globals (97). + +Values are described by a `d()` that keeps the comparison deterministic without +weakening it: a number carries its `math.type` subtype, a string carries its +length, a table is expanded two levels with keys sorted (so neither `pairs` order +nor an address can leak in). Only a handful of things compare type instead of +value — `os.time`, `os.clock`, `collectgarbage("count")`, and `tostring` of a +table. + +Two results worth having: seeded `math.random` matches (the xoshiro256\*\* +sequence is right, not just its range), and `getmetatable("")` dumps the entire +string metatable in one line — `__index` and all seventeen methods. + +**The matrix's sensitivity was measured, not assumed.** Breaking one line of +`string.upper` (shifting `'a'…'z'` to `'B'…'Z'`) turns the run red at +`s.upper [2] q(3)[Abc]`. A matrix that cannot fail is worse than no matrix, and +the only way to know is to break something and watch it go red. + +209 lines, byte-identical, first run. Which is also the suspicious outcome — see +the note above about matrices whose lines are uniformly `nil`. Here 205 of 209 +lines carry real values, so the matrix is not vacuous. + +#### The `io.open` mode matrix, and three things stdio decides + +`l_checkmode` accepts `[rwa]%+?b*` — and the tail is `strspn(mode, "b") == strlen(mode)`, +not "at most one `b`". So `"rbb"` is valid while `"rb+"` is not, and `"r b"`, `"br"`, +`"R"`, `"r\t"` all fail. 34 mode strings × (read / seek / write / position / close / +resulting file content) — 79 lines. + +It found three divergences. All three are **stdio's** call, not Lua's, and all three +were measured before being set aside: + +1. **On an update stream, writing straight after reading** — with no intervening + positioning call, which ISO C leaves undefined — lands wherever C's read-ahead + buffer left the physical offset. Measured: a 12-byte file writes at 13 (EOF); a + 6002-byte file writes at 4097 (`BUFSIZ` + 1); but `read(5000)` writes at exactly + 5001, because a large read bypasses the buffer. The matrix now puts an explicit + `seek` before every write, which sidesteps the UB path — and on the well-defined + path (`read → seek → write`) the two agree byte for byte. +2. **Where the position lands after writing to an append file.** `seek("set", 0)` + then write one byte: the reference reports **1** (`base + n`), we report 13 + (`EOF + n` — ours is the POSIX answer). Measuring further showed it is buffer- + defined too: `n ≤ 4095` gives `base + n`, `n ≥ 4096` gives `EOF + n`. The matrix + flushes before reading the position, which collapses it onto the one POSIX- + defined answer so the two sides are comparable. **The bytes written are identical + either way** — only what `seek("cur", 0)` reports differs. +3. **`seek` on a standard stream** depends on whether the real descriptor is + seekable: 0 when redirected to a file or `/dev/null`, `"Illegal seek"` on a pipe + or a tty. This implementation has no access to that descriptor, so it is not + compared. + +Two of the three are the same shape as the `setvbuf` sizes: C hands the decision to +libc and there is nothing sensible to model. The difference is that here the +*observable* is not just an errno but a file offset — which is why they are written +down rather than merely excluded. + +#### `require` against a real filesystem + +The `package` gate used to exercise only the pure-Lua half of the searcher chain, +by populating `package.preload` in memory. `PackageLib` was still the second- +thinnest class by coverage, so there is now a matrix that writes **real modules to +disk** and makes `require` find them: 57 lines covering what a module may return +(nil, false, true, a number, nothing, multiple values), a module that raises, one +with a syntax error, one that sets `package.loaded` itself, dotted names turning +into directories, the cache on a second `require`, a circular `require` (the +reference gives `C stack overflow`, not a sentinel), `package.loaded` pre-set to a +value / `false` / `nil`, `package.preload` given a function / a function returning +nil / a raising function / a non-function, `searchpath`'s `sep` and `rep`, and the +`searchers` chain — its length, each entry called directly, and the whole chain +replaced. + +One thing it pins down that would otherwise rot quietly: every +`module 'X' not found` message prints the **entire** candidate list, so the default +`package.path` has to match the reference binary's compiled-in `LUA_PATH_DEFAULT` +character for character. + +57 lines, byte-identical on the first run — so, again, sensitivity measured rather +than assumed: changing `searchpath`'s default `sep` from `"."` to `"-"` turns it +red at `sp_dots`. + +#### Reading the coverage report line by line + +Picking a target by class-level miss ratio only goes so far — after the mode and +`require` matrices the top holes barely moved, because a class's uncovered +instructions are mostly `catch (IOException e)` handlers that no Lua program can +reach. So the next pass went one level down: open +`target/site/jacoco//IoLib.java.html` (the XML report has no line detail, the +HTML does), collect the `class="nc"` line numbers, and ask of each one **"can a Lua +script reach this?"** + +Of 63 uncovered lines in `IoLib`, most are genuinely unreachable error handling. +The rest were reachable behaviours that had simply never been run — 39 lines: + +* `tostring` of a closed handle is `"file (closed)"` — *not* `io.type`'s + `"closed file"`; two different strings, and only one of them was being tested. +* `io.lines()` with **no argument**, and with an explicit `nil` — these go through + the registry's `IO_INPUT`, a different path from `io.lines(filename)`. +* Every shape of `f:read("n")`: exponents (`1e5`, and `1e999` giving `inf`), hex + (`0x10`, `0x1p4`, `0x.8p1`), a leading zero (`0777` — decimal, not octal), + truncated prefixes (`0x`, `1e` give nil), and an over-long digit string (nil). +* Closing the file *during* `lines` iteration: the iterator then raises + `"file is already closed"`. +* A large write (5000 bytes), which takes the branch that bypasses the write + buffer and goes straight to the file. + +All 39 came back byte-identical — those branches were **correct but never +executed**. Their value is as a regression net: change `read("n")`'s prefix +handling or the write-buffer threshold and this goes red immediately. + +#### The directory bug: one POSIX rule, five wrong answers + +Reading `PackageLib`'s uncovered lines and asking "can a Lua script reach this?" +led to `searcher_C`'s fallback and, from there, to a module name that collides +with a directory. That turned out to be **one rule mis-modelled in five places**: + +POSIX lets `open(dir, O_RDONLY)` succeed. `fopen(dir, "r")` therefore succeeds; +`fopen(dir, "w")` gives `EISDIR`; and the first `fgetc` / `fread` fails with +`EISDIR`. The JVM refuses to open a directory at all, so this implementation got +all five entry points wrong in one consistent way: + +| | reference | before | +| --- | --- | --- | +| `loadfile(dir)` | `nil, "cannot read

: Is a directory"` | `nil, "cannot open

: Input/output error"` | +| `io.open(dir, "r")` | a file handle | `nil, "

: No such file or directory", 2` | +| `io.open(dir, "w")` | `nil, "

: Is a directory", 21` | `nil, "

: No such file or directory", 2` | +| `package.searchpath` | finds it (returns the path) | `nil, "no file '

'"` | +| `require(name)` | `error loading module … cannot read …` | `module '…' not found` | + +The fix is one idea applied at four sites: `openInto` lets a read-only open of a +directory through (and reports `EISDIR` for anything writable), `LuaFile.getc` / +`read` fail with 21, `seek` / `tell` treat it as a zero-length file (a directory +descriptor *is* seekable — `f:seek("set", 5)` returns 5, and `f:seek("set", -1)` +is still `EINVAL`), `PackageLib.readable` accepts directories, and `loadfile` / +`searcher_Lua` take the `cannot read` branch instead of `cannot open`. + +18 lines, byte-identical after the fix. Two things about how this was found are +worth more than the bug: + +* The README previously said directories were "fine and must not be fixed" — + written from reading `lauxlib.c`, not from running the reference binary. It was + wrong. Measure, then write. +* The same sentence in the *code* comment (`BaseLib`) had asserted the behaviour + was already correct. A comment agreeing with the code is not evidence. + +#### The errno shape table: `os.remove` / `os.rename` + +`LuaAux` still had 15 uncovered lines, and most of them were `errnoOf` / `strError` +branches that no Lua script had ever reached — `ENOTEMPTY`, `EACCES`, and friends. +22 lines aimed at them found **five** divergences, four of them real bugs: + +* **`ENOTEMPTY`'s number is chosen by the OS, not by libc**: 39 on Linux, 66 on + macOS/BSD. It was hard-coded to 39, so `os.remove(nonempty_dir)` had the right + *message* and a number 27 off. It now comes from `os.name`. +* **`strError` must know both numbers.** Fixing only the number turned the line + into "errno 66 correct, message `Input/output error`" — the numeric constant and + its text have to move together. +* **`os.remove("")`** gave `EIO` where C gives `ENOENT` — the same "C and Java + judge a path differently" corner as `loadfile("")`. +* **`os.rename`'s whole shape table was wrong.** File → existing directory is + `EISDIR` (21); directory → existing file is `ENOTDIR` (20); directory → *empty* + directory **succeeds** (it replaces); file → existing file also replaces. Every + "target already exists" case was reporting `EEXIST`, because the code called + `Files.move` without `REPLACE_EXISTING`. + +(`os.remove("/etc")` used to be skipped as machine-dependent — it is `EPERM` +here because of SIP. It is no longer skipped: see *The errno shape table* below, +where the whole class of cases became **derived** instead of hard-coded, and +therefore portable.) + +The general lesson is the one from the directory bug again: **"the value differs +per platform" is a reason to derive it from the platform, not to leave the case +untested** — skipping it is what let a plain wrong constant survive. + +### Byte-by-byte mutation: naming every `error()` in `lundump.c` + +`Undump` had 11 branches no script had ever reached — truncated chunk, integer +overflow, bad format for a constant string, version / format / size mismatch. +Hand-picked bad samples only cover the positions you think of, so this sweep is +**byte-by-byte mutation** instead: every truncation length, every byte position +mutated to each of four values, varint-limit injections, and trailing garbage, +run over the plain dump, the stripped dump, and a dump with upvalues and nested +protos. 865 lines, 21 distinct outcomes, 542 lines that are not `ok`. All 11 +branches come out, two of them in places no hand-picked sample would have hit. + +Two traps first, because either one silently voids the whole sweep: + +* **`string.dump` output carries the source name, and the source name defaults to + the script path.** The reference runs `/tmp/x.lua`, ours runs + `target/rt/case.lua`, so the dumps differed by 7 bytes and every offset shifted + — the first run showed 40 "divergences" that were all this one cause. The probe + pins the chunk name to `"=X"`, after which the two dumps are byte-identical. +* **The varint-limit injection has to land after `nupvals`.** The header is 31 + bytes, not 30; byte 32 is the upvalue count. + +The sweep found exactly one real divergence, and it was **a branch we invented**: + +* **`loadSize`'s `v > Integer.MAX_VALUE → integer overflow` does not exist in C.** + The old comment asserted that "C would allocate and raise `LUA_ERRMEM`" — a + conclusion read off the C source, and measured to be wrong: with a source-name + length of `2^35-1` (varint `7f 7f 7f 7f ff`) the reference reports + `truncated chunk`, not a memory error. malloc of 34GB succeeds on 64-bit, so C + goes ahead and reads those 34GB and hits end-of-stream. The branch is gone; long + strings are now **accumulated while reading** — how much is buffered depends on + how many bytes the stream actually has, not on how many it claims, and hitting + the end still reports `truncated chunk`. + +The reverse check afterwards — swapping `loadUnsigned`'s `limit >>>= 7` for the +`Long.MAX_VALUE >>> 7` its own comment warns about — **turned up a second defect +in passing**: + +* **`loadCode`'s `new byte[n * 4]` can overflow to a negative number** and throw + `NegativeArraySizeException` — **a Java exception, which `pcall` cannot catch**, + so one malformed chunk takes down the host process. It also paid twice: C + allocates `n` instructions, this allocated `n` plus another `4n` bytes, i.e. + double whatever an untrusted input asked for. Reading instruction by instruction + from a reused buffer removes the overflow and allocates only `n`. The same + sabotage re-run afterwards: **no crash**, and 13 red lines — the matrix is + sensitive to errors this small. + +The lesson is the standing one: **a comment agreeing with the C source is not +evidence.** The more plausible that comment looked, the easier it was to read the +mismatch as a deliberate deviation — and a branch that exists nowhere in C stayed +quiet until a machine checked it. + +### JaCoCo can lie: the line that calls an always-throwing method + +Picking targets from uncovered lines has a trap, and this is where it surfaced: +**JaCoCo marks the line that calls an always-throwing method as not covered.** +The instruction after the call — the `pop` that discards the return value — is +never reached, so the whole line is recorded as `nc`. + +Measured: `Undump`'s ten `error()` branches fired **848 times** during the suite +(counted by adding a print inside `error()`), every line *inside* `error()` is +`fc`, and **all nine call sites are `nc`**. Following the report would have meant +writing more cases for code that was already covered. + +The test: **the line is an `xxxError(...)` call or starts with `throw`, and the +method it calls is itself covered** — that is a fake gap. Filtering on it makes +`Codegen`'s 15 lines, `Parser`'s 21 and `Undump`'s 10 **disappear entirely**; +what is left is real. + +### The table-key matrix: `rawequal(math.maxinteger, math.maxinteger + 1.0)` + +Of the real gaps, `LuaTable` had 16 lines, all paths that only run for narrow key +ranges: `hashFloat`'s `-0.0` normalisation, `landsInArray`'s float branch, rehash +when the hash part needs more slots. A handful of hand-picked keys never reaches +them, so this is a **key × key** square: 20 keys each written once, then read back +with all 20, plus 2048 integers on each side of `math.maxinteger` swept by step. + +It turned up exactly one real divergence, and it is **not in `LuaTable`**: + +* **`rawequal(math.maxinteger, math.maxinteger + 1.0)` should be `false`.** The + two numbers are genuinely different (`math.maxinteger` is 2^63−1, the right side + is the float 2^63), but `(double)(2^63−1)` rounds to `2^63`, so "cast both to + double and compare" calls them equal. Lua 5.4 compares two numbers of + *different variants* the other way: convert **both** with F2Ieq and compare + integers; if the float side does not convert, they are not equal. The fix is in + `LuaValue.numberEqual`, which backs `==`, `rawequal`, **and the table's key + comparison** — so `t[math.maxinteger]` and `t[2^63]` were previously the same + entry. +
The duplicate `floatToIntExact` in `LuaTable` is now folded into + `LuaValue`, so the two copies of that rule cannot drift apart. + +The two rules only diverge over roughly the thousand integers before +`math.maxinteger`, which is why the stepped sweep at the end of the script +matters: **without it the fix is worth 6 lines; with it, reverting turns 77 red.** + +`next`'s **order** depends on the hash function and need not match, so key sets +are always sorted before printing — the set is compared, not the order. + +### The `debug` frame-shape matrix: the hook frame is at level 3 + +After filtering out the fake gaps, `DebugLib` still had 22 lines, all paths that +only run when the stack has a particular shape: `currentLine` returning `-1` for +non-Lua frames, `funcNameFromCallInfo`'s `ci.hooked` branch, the +`getinfo`-with-a-*function* (rather than a level) path, and `traceback`'s +`(...tail calls...)`. Two hand-picked examples never reach them, so this is a +frame-shape matrix: top level, Lua frames, C frames (looking outward from +`table.sort` and `pcall` callbacks), inside and outside a coroutine, a tail-call +chain, and a long function under a line hook. 106 lines. + +Two things worth recording: + +* **The hook frame is at level 3.** Inside a hook, level 1 is the `pcall` wrapping + the call, level 2 is the printing helper, and **the hook's own frame is level + 3**. Stop at level 2 and the `namewhat = "hook"` branch is never reached — and + on the report it looks exactly like every other uncovered line. +* **One thing is not compared**: the `"(C temporary)"` of the outermost C frame is + whatever the host pushed when calling `pmain`. The reference pushes + `(argc, argv)`, so it reads back `2` and a userdata; this driver pushes the + script text. **That is the host's calling convention, not Lua semantics**, so + the script only goes up to level 2. + +Result: 106 lines match byte for byte, no real divergence, and three never-taken +paths are now covered — `getupvalue` on a C closure, `getinfo` with a function +plus a thread, and `traceback`'s tail-call marker. What remains uncovered is +defensive (`HOOKKEY` missing, hook not a function) plus one line that **only a +host can reach**: `debug.gethook`'s `"external hook"` — `sethook` from Lua always +installs `HOOKF`, so no script gets there. + +### The coroutine × C-boundary matrix: `xpcall`'s message handler cannot yield + +Every entry point that C drives from inside a callback has its own `nny` +(C's `nyci`) bookkeeping, and a single missing `+1` is invisible until something +yields at exactly that spot. So this is a matrix over **where** the yield +happens: `pcall`, `xpcall`, **`xpcall`'s message handler**, `table.sort`'s +comparator, `string.gsub`'s replacement function, `load`'s reader, eight +metamethods (including `__close`), all four kinds of generic-`for` iterator, a +debug hook, `coroutine.wrap`, a nested coroutine, `resume` on a dead and on an +errored coroutine, and a suspended coroutine resuming *itself*. Each row also +asks `coroutine.isyieldable()` separately, so "may I" and "what happens if I do" +are two measured answers rather than one. 67 lines. + +It found one real divergence, and it is worth stating precisely because the +wrong behaviour looks *more* capable, not less: + +``` +run xpcall_msgh true false error in error handling <- reference + status xpcall_msgh dead +run xpcall_msgh true in msgh <- this implementation + status xpcall_msgh suspended +``` + +`LuaState.applyErrMsg` was calling the handler with the yieldable `pcall`; +C uses `luaD_callnoyield`. The reason matters more than the symptom: being able +to yield inside the error-handling continuation means an +**"error half-delivered"** intermediate state can be suspended — and C has no +such state. Fixed with `callNoYield` plus a `catch (LuaError)` that stops +backstopping the handler's own failure, which is what produces +`"error in error handling"`. + +Two more things this matrix pinned down: + +* **`pairs` order over string keys is randomly seeded, so it cannot be + compared.** String keys go through `luaS_hash`, whose seed comes from + `lstate.c: luai_makeseed` — `time(NULL)` mixed with three ASLR addresses. + Running the same script against the reference five times gave `in pairs:a`, + `a`, `b`, `a`, `b`. Any probe that prints raw key order is a + **fake-red generator**; this one drains the coroutine to death and prints the + collected yields **sorted** instead. (This implementation has no seed at all, + so its order is fixed and deterministic — a deliberate, safe asymmetry: the + reference's own order is not observable either, since nothing can depend on + a value that changes every run.) +* **`load`'s reader must terminate.** A reader that never returns `nil` makes + `load` read forever — the first version of this probe **hung the reference + binary** and had to be `pkill`ed. + +Reverse verification: swapping `callNoYield` back to `call` turns exactly two +lines red, and the test fails. + +### The to-be-closed matrix: errors raised inside `__close` also reach the message handler + +`` is the only new syntax in 5.4 and it has the most edges, so this is a +matrix over **how the block is left** (falling off the end, nested, `return`, +`break`, `goto`, `error` with a string and with a table) crossed with **what +`__close` itself does** (log, raise, raise a non-string, return several values, +`yield`, be a number, be a callable table, be absent), then crossed again with +`pcall` / `xpcall` / coroutines / `coroutine.close`. 56 lines. + +It found two divergences with one root cause: **an error raised inside `__close` +must go through the message handler too.** + +``` +msgh orig | close a err=H | msgh E:a | xpcall -> false H <- reference +msgh orig | close a err=H | xpcall -> false E:a <- this implementation +``` + +`luaD_pcall` restores `L->errfunc` on its **last** line, and +`luaD_closeprotected` runs *before* that line — so the handler is still installed +for the whole time `__close` runs, and `error()` inside it reaches +`luaG_errormsg` just like any other error. The practical consequence is easy to +miss: **the final result is the handler's return value, not `__close`'s error.** +The same fix produces `"error in error handling"` in the case where the handler +itself raises — that string comes from the `__close` error making a second pass +through a handler that is already failing. + +The second divergence is the coroutine side of the same rule: +`finishpcallk` restores `L->errfunc` *after* `luaF_close`, so the handler must +run **before** the closing methods, not after. Getting this backwards shows up as +`close a err=after | msgh h2 after` instead of `msgh h2 after | close a err=H-h2` +— the closing method sees the raw, unhandled error. + +Moving that call earlier exposed a second, independent trap: the handler must be +called **before** `top` is pulled back to `fIdx + 1`. `luaG_errormsg` pushes its +call frame at the *current* top of the stack, where the protected function's +locals still live; pull `top` back first and it starts writing at `fIdx + 2`, +which is exactly the first local slot — the `` variable — so `__close` +gets overwritten with the handler itself and the next call reports +`attempt to call a nil value`. + +One combination is deliberately **not** in the matrix: a message handler that +raises. `luaG_errormsg` recurses there until the C stack overflows, producing +several hundred lines whose count varies with stack depth. The matrix pins the +*result* only. + +### The debug-hook matrix: whether a hook error is catchable depends on where the hook was set + +Hooks are the one place where this implementation interrupts the VM's normal +flow with a Java exception, so events are emitted from three different points +(`precall`, `poscall`, the instruction loop) with `allowhook`, `nCcalls` and +`ci` each in a different state. This is a matrix over the eight mask +combinations (`c` / `r` / `l` and their unions) crossed with `count` +(0, 1, 2, 3, 5), crossed with frame kind (Lua, C, tail call, coroutine), crossed +with what the hook itself does (disable itself, swap itself, raise, raise a +non-string, `yield`, read `debug.gethook`). 56 comparable lines. + +Result: byte for byte identical, **no real divergence**. It is still worth +keeping, because it is measured to be sensitive — renaming the `"tail call"` +event to `"tailcall"` turns 14 lines red. + +The interesting thing it pinned down is not a bug but a trap in writing such +probes: + +* **Whether a hook error can be caught depends on where `sethook` was called.** + The very next `call` event after `sethook` is for `pcall` itself — the `c` + mask counts C functions — and that fires *before* `pcall` has established its + protected region, so the error escapes and kills the script. The reference + reports `lua: hk1` with `[C]: in function 'pcall'` in the traceback and stops + there. To test *catching*, `sethook` has to run **inside** the `pcall`; the + escaping variant can only go **last** in the script, or everything after it is + silently uncompared while the test stays green. +* **Reading the stack from inside a hook needs level 3.** Level 2 is the + printing helper; level 3 is the frame with `namewhat = "hook"`. + +### The type-metatable matrix: `setmetatable`'s first argument must be a table + +`lua_setmetatable` dispatches three ways: tables and userdata store the +metatable on the object, everything else installs it in `global_State`'s `mt[]` +where **the whole type shares one**. This matrix walks all eight types through +`debug.setmetatable` / `debug.getmetatable`. 48 lines, byte for byte identical, +and it is sensitive — deleting one line of the userdata branch turns 6 lines red. + +The reason that branch had never been covered is worth stating: **`setmetatable` +from Lua requires its first argument to be a table** — +`lbaselib.c` starts with `luaL_checktype(L, 1, LUA_TTABLE)` — so userdata can +only be given a metatable through `debug.setmetatable`, and no script in the +suite ever did that. (The error message names the type via `__name`: +`bad argument #1 to 'setmetatable' (table expected, got FILE*)`.) + +Two things it pins down: + +* **Only tables and full userdata honour `__name`.** Give numbers, `nil`, + booleans, functions, threads or strings a type metatable with `__name` and the + runtime still says `number` / `nil` / `boolean` / … ; only the userdata becomes + `attempt to index a M_ud value`. +* **The string type metatable *is* the string library.** Replace it and + `("x"):upper()` stops working, so the probe has to save and restore it — and + it must save it *before* the section that installs an empty metatable on every + type, or it saves the wrong table and the script dies halfway through. + +### The `package` matrix: the module name is a `const char *` + +`PackageLib` was the thinnest class by coverage (0.89), so this is a matrix over +what `loadlib.c` actually does with a path: 18 template shapes (`""`, `";"`, +`"a;"`, `";;a;;"`, …) crossed with `?` substitution, `sep` / `dirsep` corner +cases, embedded NULs, argument checking, all four searchers called directly, and +`loadlib`. 156 lines, byte for byte identical, and sensitive — dropping the +`const char *` truncation turns 3 lines red. + +It found one real divergence and one wrong constant: + +* **`searcher_C` and `searcher_Croot` did not cut the module name at the first + NUL.** In C the name is `const char *name = luaL_checkstring(L, 1)`, so + `package.searchers[3]("a\0b")` searches for `a` — this implementation searched + for the whole string. Only reachable by calling a searcher directly (`require` + truncates first), which is why no earlier probe caught it. +* **`loadlib`'s third return value has *two* constants, not one.** `ERRLIB` + (library will not open) gives `LIB_FAIL`, which is `"open"` on a dlopen + build and `"absent"` on the fallback build; `ERRFUNC` (library opened, symbol + missing) gives a hard-coded `"init"` — the fallback branch redefines only the + former. Verified against the reference with a real `.dylib`: + `package.loadlib(real, "luaopen_nosuch")` → `nil, , "init"`. + This implementation's `lookForFunc` always returns `ERRLIB`, so the branch is + unreachable here, but the constant is now the one C uses. + +Two things the matrix deliberately does **not** compare: + +* **`dlerror()` text.** It embeds an address; `dlsym(0x…, luaopen_x): symbol not + found` differs on every run, so the C-searcher section compares only the part + of the message before `\n\t`. +* **`LUA_PATH` handling.** The `;;` → "insert the default path here" expansion is + real and easy to get wrong (only the *first* `;;` is replaced, and no extra + separators are added around the default). It cannot be driven from this harness — + the reference binary's environment is not controllable from JUnit — so it was + measured separately with `env LUA_PATH=…` over nine shapes + (`a;;b`, `;;`, `a;;`, `;;b`, `a;;b;;c`, `;;;`, `noMark`, empty, and + `LUA_PATH_5_4` taking priority): identical every time. + +**The matrix did not move coverage at all** — `PackageLib` is still 0.891 with +the same 117 missed instructions afterwards. Those lines were already being +*executed*; they were never *compared*. Coverage cannot see the difference, and +"coverage went up" is not the test for whether a gap has been closed. + +### The `io` corner matrix: `f:read(0)`, big writes, `ESPIPE`, directories + +`IoLib.LuaFile` was the thinnest class left (0.80), and every line in it is a +stdio rule rather than Lua semantics. This matrix covers `read(0)`, writes that +bypass the 8192-byte write buffer, `seek` on a pipe, reading a directory, and +`io.lines()` with no argument. 43 lines, byte for byte identical, and sensitive — +changing the directory `errno` from `EISDIR` (21) to `EIO` (5) turns 3 lines red. + +What each group pins down: + +* **`f:read(0)` returns `""` before EOF and `nil` at EOF.** `fread` asked for zero + bytes returns zero, which counts as "read something" — so the empty string is a + real result, not an error. The mirror case (writing `""` to a read-only file + succeeds) was already pinned earlier; these are the two halves of one rule. +* **Writes larger than the 8192-byte buffer skip the buffer entirely**, so + 10000 / 8192 / 8191 bytes are each written once and read back to check the + positions. +* **`seek` on a pipe is `ESPIPE` (29)**, not a generic I/O error. Only + `io.popen` files are used for this — see the warning below. +* **Directories: `fopen` succeeds, reading fails with `EISDIR` (21), and `seek` + follows yet another rule** — the position is computed normally and only a + negative target position is `EINVAL` (22). +* **`io.lines()` with no argument reads the registry's default input.** Close it + and the error is `attempt to use a closed file`, *not* `no input file`: the + registry entry is still there, it is merely closed. + +**Never `seek` `io.stdout` / `io.stderr` in a differential probe.** When stdout is +redirected to a *file*, that `seek("set", 0)` **succeeds**, the write pointer goes +back to 0, and every later line overwrites the output from the start — the +reference's output becomes a shuffled fragment of itself and looks exactly like a +bug in the implementation. It only reports `ESPIPE` when stdout is a *pipe*. That +depends on how the harness captures output, not on Lua. + +### The errno shape table: `errno` and `strerror` are inverse functions + +File operations do not fail along one axis ("exists or not"); the `errno` is +decided by the **shape of the target**. `io.open("file/y")` is `ENOTDIR` (20), +`io.open("/etc/hosts", "w")` is `EACCES` (13), `os.remove(".")` is `EINVAL` (22), +`os.remove("/")` is `EISDIR` (21), `os.remove("/etc")` is `EPERM` (1) here. This +implementation classified failures **by Java exception type** and fell back to +`ENOENT` / `EIO` for everything else — so **five of those were wrong**. + +The fix is to *invert* rather than to enumerate a second table: `errno` and +`strerror` are inverse functions, and Java's I/O exceptions carry the libc +`strerror` text verbatim — `FileNotFoundException` as +`" (Permission denied)"`, `FileSystemException.getReason()` directly as +`"Not a directory"`. So `LuaAux.errnoOf` now looks the text up in +`LuaAux.strError`'s table and gets the number back. 44 lines, byte for byte +identical, and sensitive — disabling the lookup turns 9 lines red. + +**This is also what makes the matrix portable.** `os.remove("/etc")` is `EPERM` +on macOS (SIP) and `ENOTEMPTY` on Linux — but *both* sides now derive their +answer from the same libc, so they agree on each platform instead of one side +carrying a platform-specific constant. That is the whole argument against +"this value varies per platform, so skip it": skipping it is what let a plain +wrong constant survive, and deriving it is what makes it testable. + +Two traps while writing the probe, both recorded in the test's javadoc: + +* **Fixtures go in a subdirectory and are deleted at the end** — not scattered + over the repository root. +* **Never exercise `os.remove` against a real system file.** `os.remove("/etc/hosts")` + would *succeed* and delete it if the suite ever ran as root. The `EACCES` case + uses a `chmod 555` directory the test creates itself. + +#### The `utf8` sweep: one literal written twice, and one line nobody can reach + +`Utf8Lib` was at 0.930, and unlike `IoLib`/`PackageLib` its holes are pure logic — +no platform behaviour, no `IOException` handlers. Reading the uncovered lines found +a literal written **twice**: `open` built the `utf8.charpattern` byte array inline +while `charPattern()` sat next to it with no callers at all. Two copies of the same +constant is a drift that nothing can detect — the coverage report just shows both +blocks `nc`. Merging them (and having the matrix actually read +`utf8.charpattern`) puts the pattern under test. + +The 72-line matrix covers `utf8.char` at every boundary (0, `0x7F`, `0x80`, +`0x7FF`, `0x800`, `0xFFFF`, `0x10000`, `0x10FFFF`, `0xD800`, `0x110000`, +`MAXUTF`, `-1`, `math.mininteger` / `math.maxinteger`, non-integer, string), the +**separate** `value out of range` branch that only arguments 2..n take, every +`utf8.codepoint` bounds shape (`i > j` returning nothing at all, 0, past the end, +negative, empty string, `lax` true/false, float / string / boolean arguments), and +`charpattern` **used** rather than inspected — `gmatch` over `"a中b\u{1F600}"` +yields 4 characters, over `"\xff\xfe"` yields 0. + +Byte-identical on the first run. `Utf8Lib` went 0.930 → 0.995 with one line left: +`throw L.libError("string slice too long")`, which needs a string of at least two +gigabytes. It is recorded as unreachable rather than chased. + +#### A syntax-error matrix: every error string in `lparser.c` and `llex.c` + +`Parser` was the second-biggest hole (169 uncovered instructions) and, unlike +`IoLib`/`PackageLib`, its gaps are pure logic. Reading the uncovered lines showed +they were almost entirely **error messages** — and every single one of them was +`nc`, meaning no test had ever produced that message at all. + +So the matrix is not a hand-picked list; it is an **enumeration of error sites in +the C source**: `error_expected` and both shapes of `check_match`, `errorlimit` +for locals/upvalues/constructor items, `attempt to assign to const variable`, +`jumpscopeerror` / `undefgoto` / duplicate labels, ` or '...' expected`, +`function arguments expected`, `unexpected symbol`, `unknown attribute`, +`multiple to-be-closed variables`, `cannot use '...' outside a vararg function`, +`'=' or 'in' expected`, `control structure too long`, plus the whole lexical set +(unfinished string / long string / long comment, malformed number, hexadecimal +digit, `missing '{'`, UTF-8 value too large, `missing '}'`, decimal escape too +large, invalid escape sequence, invalid long string delimiter). 172 lines. + +Three things it pins down that would otherwise rot: + +* **`` and `` are printed *without* quotes**, while `'end'` and `','` + are printed with them — `luaX_token2str` returns the bare string for + `token >= TK_EOS`. And the token after `near` goes through `txtToken`, which + uses the *raw source text* for strings and numbers, so the message can contain + nested quotes: `hexadecimal digit expected near ''\x''`. +* **C stack overflow is *returned*, not raised.** Deep nesting trips + `luaE_incCstack`, but `luaD_protectedparser` catches it, so `load` hands back + `nil + "C stack overflow"`. Only `load`'s own argument check + (`bad argument #2 to 'load'`) actually raises. The probe wraps everything in + `pcall` so the two are distinguishable. +* **`control structure too long` needs a loop body of more than `MAXARG_Bx`** + (262143) instructions, so that case is a generated 1 MB source — 0.02 s on the + reference, 0.6 s here, cheap enough to keep. + +Byte-identical on the first run, and sensitive: quoting every token name turns 11 +lines red, dropping `check_match`'s same-line shortcut turns 13 red, and +`errorlimit`'s `"main function"` → `"main chunk"` turns 1 red. + +#### …and a hole in the coverage report, not in the code + +After installing it, `Parser`'s coverage barely moved — 169 → 167, while the +uncovered-line list was **character for character identical**. That is not +possible if 172 lines of new error paths really ran, and they did: instrumenting +one of them with a `println` fires, and the byte-identical output can only have +come from those `throw` sites. + +What gives it away is the line *above*: + +```html +if (!lh.v.isVar()) { + throw ls.syntaxError("syntax error"); +``` + +JaCoCo knows the guarding branch was taken in both directions, and the line +number table proves `1315` has instructions of its own — yet the line reads +`nc`. `Parser` has **19 `throw` lines and not one of them is `fc`** (17 `nc`, +2 `pc`), while the rest of the project is 136 `fc` / 41 `nc`. + +The consequence for target selection: **an uncovered `throw` line is not +evidence of an uncovered path.** Half of `Parser`'s apparent hole is this +artifact, and chasing it would have burned an afternoon proving correct code +correct. What actually tells you the path runs is the *comparison*, not the +coverage number — which is the same lesson as "coverage going up ≠ the gap +closed", from the other side. + +#### The `os.date` shape matrix: `*t` is a `strcmp`, not a prefix + +`OsLib` was at 0.952, and the existing `FORMAT` group only exercised the `!` +(UTC) half. The new 337-line matrix adds the other half: **every conversion +specifier in local time**, the same set at five timestamps (`0`, the 2038 +boundary, 1900, `-1`, and year 1), the error shapes for invalid specifiers, and +the corners of `os.time` / `os.difftime` / `os.setlocale` / `os.getenv`. + +It found **one real bug**: `*t` was a *prefix* test, but C has +`strcmp(s, "*t") == 0`. So `os.date("*t x")` should fall through to `strftime`, +where `*`, `t`, the space and the `x` are all literals, and return the string +`"*t x"` — this implementation returned a table instead. + +Fixing it surfaced a second detail, the same trap as everywhere else in this +codebase: `strcmp` **stops at the first NUL**, so `os.date("*t\0x")` is *still* +the table form in C. Comparing `f.length` against 2 is not enough; the string +has to be truncated at `\0` first. + +Sensitivity: deleting the `E` / `O` two-character group from +`LUA_STRFTIMEOPTIONS` turns 38 lines red, reverting `*t` to a prefix test turns +1 red, and using `f.length` instead of the NUL-truncated length turns 2 red. + +Two things stay out of the matrix on purpose: **`!%z`** (macOS's `strftime` +reports the *local* offset even under `gmtime`, glibc reports `+0000`; this +implementation follows POSIX and can only agree with one of them) and +**`os.date` / `os.time` with no argument** (wall clock). + +#### The `name` / `namewhat` grid: a function's name is reconstructed, not stored + +A function's `name` is not a property of the function — it is **reconstructed from +the call site** (`funcnamefromcall` → `funcnamefromcode` → `getobjname`), by +looking at whether the caller's instruction was `OP_GETFIELD`, `OP_GETTABUP`, +`OP_GETUPVAL`, `OP_MOVE` or `OP_SELF` and then digging the name out of the +constant table or a register. So the same function has a different `name` and +`namewhat` at every syntactic position, which is exactly what a grid is for: 77 +lines over nine `namewhat` values — `global`, `local`, `field`, `method`, +`upvalue`, `metamethod`, `for iterator`, `""`, and `nil`. + +Shapes that would not survive being guessed: + +* **`t[1]()` is named `"integer index"`, not `"?"`.** Integer keys are their own + branch in `getobjname`; only float and boolean keys fall through to `"?"`. +* **`"?"` still reports `namewhat = "field"`** — the name may be unrecoverable + while the *calling convention* is not. The two are independent. +* **`metamethod` names are the event name minus the `__`** (`index`, `newindex`, + `add`, `len`, `close`, …), while `__call` goes through a normal call and + reports `name="a1", namewhat="local"`. +* When nothing is recoverable, `name` is `nil` but `namewhat` is the **empty + string**, not `nil`. + +First run byte-identical (bar the script path), so sensitivity was measured: +`"integer index"` → `"?"` turns 5 lines red, and changing the case of +`namewhat`'s `"field"` turns 1 red. + +### `__gc` is deliberately not chased further + +There is no `lgc.c` here; `__gc` is reconstructed with `Cleaner`, so **when** it +runs depends on the JVM's collection timing, while the reference's timing depends +on its own incremental collector. Neither is guaranteed, so byte-for-byte +comparison is not stable in principle — even the "add `__gc` after setting the +metatable" rule measured differently for tables and for userdata. The handful of +reproducible shapes already in `GcTest` (runs once, errors are swallowed, adding +`__gc` late does not count) is the whole target; no more matrices are built for +it, and an apparent divergence there is not evidence of a bug. + +Nothing in the language or the ten standard libraries, and every upstream file +whose output is deterministic now matches byte for byte. The remaining work is +verification breadth: more differential scripts and a wider fuzz corpus. The only +upstream files still off the list are `all.lua` / `main.lua` (the driver: it needs +the `T` module built from `ltests.c` and writes `time.txt`), `attrib.lua` / +`files.lua` / `big.lua` (they need the C modules in `libs/` and the globals +`all.lua` sets up — the reference binary fails on them standalone too), +`heavy.lua` (minutes), and `bwcoercion.lua` (no output standalone). + +### The library-argument grid: every function, every wrong argument + +`bad argument #1 to 'string.rep' (string expected, got boolean)` is the message +real projects hit constantly, and every branch that produces it lives in +`lauxlib.c`. The grid enumerates the functions **at runtime** (`_G` plus the nine +library tables) rather than from a hand-written list — a hand-written list cannot +miss a function silently, and "silently" is exactly what happened when `warn` was +unimplemented for a long stretch with 828 green tests. 108 functions × `{0, 1, 2}` +arguments × 10 values = **2396 lines**, all byte-identical. + +Three judgments it pins down: + +* **A missing argument reports `no value`, not `nil`.** `luaL_typename` on an + out-of-range index gives `no value`. Breaking it turns **212** lines red. +* **`lua_type` on an out-of-range index is `LUA_TNONE`**, not `LUA_TNIL`, so a + parameter declared "nil or table" does not silently accept absence: + `setmetatable({})` must report `nil or table expected, got no value`. Only one + line in the grid covers it — which is one line more than before. +* **Method calls subtract one from the argument index** (`arg--` in + `luaL_argerror`), and if that lands on 0 the message changes to + `calling 'x' on bad self (...)`. A direct call never reaches either branch, so + the grid adds a second, **literal** block of `(""):f(v)` / `(1):f(v)` calls + (377 lines) — `OP_SELF` only comes from a syntactic method call, it cannot be + assembled at runtime. Sensitivity 75 and 22. + +Two normalisations are deliberate: `math.atan` and `math.atan2` are the same C +function and `findfield` picks one based on the string-hash seed, which changes +every run; addresses become `PTR`. The functions on the blacklist are excluded +for side effects, not for difficulty — `print` and `io.write` write to stdout, +`os.date` / `os.time` / `os.clock` read the clock, `math.random` is random, +`io.read` / `io.lines` read stdin, `debug.sethook` leaves a hook installed, +`debug.setmetatable` mutates a global type metatable, and `collectgarbage`'s +`step` / `count` are the documented deviations. + +There is a third layer on top of the same grid: **what a valid call returns**. The +first version printed `OK` on success and threw the return values away, which +hides an entire class of bug — return-value *type*. `math.abs(-1)` returns the +integer `1`, not the float `1.0`, and `tostring` prints those as `1` and `1.0`. +Routing `math.abs`'s integer branch through the float path leaves the +argument-error grid **completely green** and turns 6 lines red here; conversely, +breaking `math.type`'s return value turns 2 lines red here and none there. Of the +2729 output lines, 959 are successful calls covering 139 distinct return shapes. +Two functions are excluded because of their *return values*: `os.tmpname` is +random, and `debug.traceback`'s return value contains `stack traceback:`, which +the harness would cut at — silently skipping everything after it. + +### `debug.debug`: the one function that reads stdin + +`ldblib.c: db_debug` is a loop, and all three of its rules were wrong here before +this grid existed: + +* it writes `"lua_debug> "` to **stderr before every read** (no newline). This was + missing entirely, so the reference's output had eleven bytes we never produced; +* it **loops** until EOF or a line that is exactly `"cont\n"`, executing each line + as a chunk named `=(debug command)`. The Java version read one line and + returned; +* a command that fails prints `luaL_tolstring(L, -1)` to stderr and the loop + **continues**. + +Testing any of this needs stdin, which the differential harness did not have — it +redirects the reference's stdin to `/dev/null` and leaves `LuaState#in` as an +empty stream, so only the "print one prompt, then EOF" shape was reachable. +`Diff.assertMatchesWithStdin` feeds the same bytes to both sides, which brings the +loop body, the error branch, `cont`, a line with no trailing newline, and the +250-byte `fgets` buffer split (a long line is chopped into several chunks, each +executed separately) into CI. All six cases now match byte for byte. + +One thing worth recording about ordering: C's `print` goes through +`lua_writeline`, which calls `fflush(stdout)`, so stdout and stderr interleave in +program order even when both are captured into one pipe — not "all stdout at +exit". + +### The `varinfo` matrix: the name in an error message is reconstructed, not recorded + +`attempt to index a nil value (local 'x')` — the parenthesised tail comes from +`ldebug.c: varinfo`, and none of it is stored anywhere. `varinfo` first asks "is +this value an upvalue of the running closure?", then "which register holds it?", +then hands that register to `getobjname`, which guesses from **the last +instruction that wrote it**. The grid (**112 lines**) pins every kind: `local`, +`upvalue`, `constant`, `global`, `field`, `method`, `(for iterator 'for +iterator')`, plus the two failure shapes `field 'integer index'` (`OP_GETI`, so +the kind is `field` and the *name* is the literal string `integer index`) and +`field '?'` (a key it cannot recover). The empty tail is part of the answer too: +`attempt to call a number value` must **not** grow a `(constant '42')`, because +`kname` names *string* constants only — two rows pin that. + +The valuable half of this matrix is what it found by **not** being sensitive. The +first 21 cases were written on the intuition "a conditional assignment makes the +name disappear": `local x = nil; if c then x = nil end; return x.y`. All 21 answer +`(local 'x')`, and rewriting `filterpc` as an unconditional `return pc` left all +21 green. The reason is in `basicgetobjname`: it calls `luaF_getlocalname` +**first**, and any register that is a live local at the failing pc returns +`"local"` on the spot — `findsetreg`, its jump-target filter and all, is never +entered. Symbolic execution only ever speaks about **temporaries**. + +What does reach it is `and` / `or`: in `(c and t.a or t.b).z` both branches write +the *same* temporary and both writes sit before the jump target, so the last write +is judged "may not have executed" and discarded, and the tail collapses to the +empty string. Adding those turned the very same sabotage from 0 red lines into +**234**. + +Sensitivity, measured by breaking one line and reverting: `filterpc` 234, +`instack` 435, `upvalname`'s `"?"` 211, `kname` string-only 131, `OP_MOVE`'s +`b < a` 22, `OP_LOADNIL`'s register range 10, `"integer index"` 3, `isenv`'s +`global` 3, `method` 2. + +One branch is recorded as unreachable rather than quietly assumed covered. +Instrumenting `findsetreg` over the grid and the 33 upstream files shows which +opcode it lands on: `LOADNIL` 1080, `GETTABUP` 494, "not found" 326, `GETUPVAL` +199, `MOVE` 52, … and `TFORCALL` **zero**. That branch (`change = reg >= a + 2`) +cannot be observed from an error message: `TFORCALL` sits at the end of the loop +body, the registers it touches are live locals inside the loop, and outside the +loop they are always rewritten before anything reads them. It is transcribed +verbatim from C; no case in 1032 lines reaches it, and that is a measured fact. + +### The file-method matrix: six methods × six modes × every argument kind + +`IoLib.LuaFile` was the least-covered class in the tree (0.758). The grid that +targets it has nine blocks and prints **1262 lines**: the six file methods +(`flush lines read seek setvbuf write`) over the six open modes over 21 argument +values, then the same again on a *closed* handle; a **two-argument** `seek` +matrix; write-then-seek; `read`'s multi-format forms and `lines` iteration; +`write`'s multi-argument forms; a directory handle; a **second handle opened on +the same path**; a failed `read("n")` followed by `read(1)`; and `popen`'s close. + +Two of those blocks exist because the obvious one is blind: + +* `f:seek(-1)` never reaches the offset code at all — argument 2 is the + *option*, so a number just means `invalid option`. Only `f:seek(w, off)` + exercises negative offsets, and it is worth **46 red lines**. +* `seek` flushes the write buffer before reporting the position, so nothing + distinguishes a `flush()` that flushes from one that does not. Opening a + *second* handle and reading the file with it does: **4 red lines**. + +Two real divergences came out of it: + +* **`f:read(-1)` raised `NegativeArraySizeException`.** C writes + `size_t l = (size_t)luaL_checkinteger(L, n)` — a negative count becomes an + astronomical unsigned value, `luaL_prepbuffsize` fails to allocate, and + `resizebox` raises the ordinary Lua error `"not enough memory"` + (`false not enough memory` under `pcall`). `new byte[(int) n]` is not the + translation of that. `read_chars` now reads in chunks until the stream is dry + or the count is met — which is closer to `fread` anyway, since `fread` keeps + going on a short read — and only refuses when the size is one no system could + allocate. The exact refusal boundary is platform-dependent (the reference + binary cuts off around 2^46 on macOS, elsewhere with overcommit), so it is + deliberately **not** in the differential table; the deterministic input is the + negative one. +* **A successful `fseek` discards the `ungetc` pushback.** `f:read("n")` + failing on `"three"` pushes `'t'` back; the following `f:seek("set", 0)` then + `f:read(3)` gives `"1 2"` in the reference and `"t1 "` in ours until `seek` + cleared it. + +Measured sensitivity: `seek`'s negative-position `EINVAL` 46, `read`'s directory +check *crashes the whole grid*, append not returning to EOF 5, `flush` not +flushing the write buffer 4, `seek` not clearing the pushback 3, `seek`'s +directory base 2, `write`'s zero-byte special case 1. + +And one honest zero: **`tell`'s `+ wlen` is 0** — `f:seek` calls `flushW` before +`tell`, so the buffer is always empty there. Coverage went up; the branch is +still unobserved. Blocks 8 and 9 are likewise coverage-only: removing the +`done >= len` short-circuit, or not closing `popen`'s `in`, leaves the grid +green. They exist so JaCoCo has less to lie about, not because they pin a +behaviour — most of what is left uncovered in `LuaFile` is `IOException` catch +blocks, which need fault injection, not more scripts. + +### The `os.time` / `os.date` grid: the two functions the argument grid had to skip + +`os.time` and `os.date` were **blacklisted** from the library-argument grid because +with no arguments they read the clock. With explicit arguments they are perfectly +deterministic, so an entire area went unwatched. This grid is **1152 lines**: +seven `os.time` fields × 23 values, the missing-field defaults (`hour` defaults to +12, `year`/`month`/`day` have **no** default), `setallfields` writing the +normalised fields back into the caller's table, 60 `os.date` formats × 15 +timestamps, `os.difftime`, `os.getenv`, `os.setlocale`. + +It found one real divergence: **`os.time{year=2147483647}` returned garbage.** +The reference gives `67767976201968000`; we gave `-6019000092870206`. The cause +is that `GregorianCalendar` holds **milliseconds**, so at that magnitude +`getTimeInMillis()` overflows and wraps to a negative number. It now goes through +`OsLib.civilToEpoch` (proleptic-Gregorian arithmetic in plain `long`), and the two +agree — and `67767976201968000` is exactly `86400 * days_from_civil` minus the +zone offset, i.e. **pure arithmetic**, so both libcs produce it. + +Two things are measured as out of scope rather than guessed at: + +* **Anything before 1900.** Apple libc's `mktime` refuses it (measured: `year = + 1900` works, `1899` does not); glibc does not. Neither implementation is "right", + so the probe prints `SKIP` for that band. +* **`os.time(nil)` / `os.time()`** — `lua_isnoneornil` means the clock. + +Measured sensitivity: the conversion-specifier error text 54, `civilToEpoch`'s +month base (`tmMon` is 0-based) 29, `isdst` always 0 / `isdst = 1` not shifting +an hour 24, `setAllFields`'s field names 22, falling back to `Calendar` (overflow) +3. And one honest zero: **`os.date`'s `MAX_SEC` guard is 0** — `2^60` falls outside +it either way, so this grid cannot see that line. + +--- + +## Migration from the old fakescript language + +This is a **breaking rewrite**. The old fakescript syntax is gone entirely: +`package`, `include`, `struct`, `const`, `->`-style `for`, `array()`, `map()`, +`fake` coroutines, `:=`, the Java binding annotations, and the profiler. + +Existing fakescript scripts must be rewritten as Lua — which is the point: after +this change, what runs is Lua 5.4, and scripts are portable to and from any other +Lua 5.4 implementation. Users who still need the old language should pin to the +last commit before the rewrite. + +--- + +## Build + +```bash +./mvnw clean test # requires JDK 21+ +``` + +With no `lua5.4` binary on the machine, the differential tests skip themselves +(a single `assumeTrue` guard in `Diff`) and the coverage gate is skipped with +them — the differential suites are most of the line coverage, so a gate that +runs while they are all skipped would only ever be red for the wrong reason. +Set `LUA54_BIN`, or build the reference into `target/lua54/src/lua`, to get the +real gate: + +```bash +LUA54_BIN=/path/to/lua5.4 ./mvnw clean test +``` + +CI builds the reference from source and exports `LUA54_BIN` and `FKDUMP_BIN`, so +this only affects local builds without a reference binary. + +--- + +## License + +MIT diff --git a/README_CN.md b/README_CN.md new file mode 100644 index 0000000..6581922 --- /dev/null +++ b/README_CN.md @@ -0,0 +1,2690 @@ +# fakescript-java + +[![License](https://img.shields.io/github/license/esrrhs/fakescript_java)](https://github.com/esrrhs/fakescript_java) +[![Language](https://img.shields.io/github/languages/top/esrrhs/fakescript_java)](https://github.com/esrrhs/fakescript_java) +[![Maven Central](https://img.shields.io/maven-central/v/com.github.esrrhs/fakescript-java)](https://central.sonatype.com/artifact/com.github.esrrhs/fakescript-java) +[![Build Status](https://github.com/esrrhs/fakescript_java/actions/workflows/maven.yml/badge.svg?branch=master)](https://github.com/esrrhs/fakescript_java/actions) + +**纯 Java 实现的 Lua 5.4 —— 不自己实现 GC,内存回收全部交给 JVM。** + +[English](./README.md) + +--- + +## 设计目标 + +> 让现有的 Lua 项目**一行代码都不用改**就能跑在 JVM 上。 + +本项目是 **Lua 5.4 参考实现(`lua-5.4.x` C 源码)到 Java 的逐模块翻译**。 +它不是一门"借鉴了 Lua 的"语言——它就是 Lua 5.4,只是把内存管理那一层摘掉、 +交给了宿主运行时。 + +| | | +|---|---| +| **目标** | 完整的 Lua 5.4 语言语义 + 完整的标准库 | +| **方法** | 逐模块直译 C 参考实现 | +| **GC** | **不实现**。对象生命周期完全交给 JVM(G1/ZGC 本身就是并发收集器) | +| **运行环境** | Java 17+(协程是独立的栈,不用线程,见下) | +| **兼容范围** | 纯 Lua 项目;C 扩展模块(`.so` / `luaopen_*`)本阶段不做 | + +### 为什么不写 GC + +C 实现的 `lgc.c` 是最复杂的部分之一:三色增量标记、分代模式、写屏障、终结器、弱表。 +它做的事,JVM 已经做得更好——并发、且调优过。所以这个文件整体删除, +**只有那些"依赖回收动作"的语义**用 JVM 原语重建: + +| Lua 概念 | C 实现机制 | 本实现 | +|---|---|---| +| 对象生命周期 | `lgc.c` 三色标记 | **JVM GC**(G1 / ZGC) | +| 弱表(`__mode = "k"/"v"/"kv"`) | `lgc.c` 里清扫弱链 | `WeakReference` + `ReferenceQueue` | +| `__gc` 终结器 | `luaC_checkfinalizer` / `GCTM` | `java.lang.ref.Cleaner` | +| 短字符串驻留 | `lstring.c` 的驻留表需抵抗回收 | 弱引用驻留池,无人引用即释放 | + +其余部分——词法、语法、代码生成、虚拟机、元方法、协程、标准库——都是直译。 + +--- + +## 架构 + +包结构一比一对应 C 源码,拿着 `lua-5.4.x` 对照即可定位: + +| Lua 5.4 C 源码 | Java | +|---|---| +| `lobject.h` / `lobject.c` | `LuaValue`、`LuaString`、`LuaTable`、`Proto`、`Closure`、`Upvalue` | +| `lstring.c` | `StringPool` | +| `ltable.c` | `LuaTable` | +| `lfunc.c` | `Proto`、`Closure`、`Upvalue` | +| `lstate.c` | `LuaState`、`CallInfo`、`GlobalState` | +| `lzio.c` | `Zio` | +| `llex.c` | `Lexer` | +| `lparser.c` | `Parser`(与上游一致的手写递归下降) | +| `lcode.c` / `lopcodes.h` | `FuncState`、`Codegen`、`OpCode` | +| `lvm.c` | `Interpreter` | +| `ldo.c` | 调用帧、保护调用、错误传播 | +| `ltm.c` | `Meta`(元方法派发) | +| `ldebug.c` | `Debug`、错误信息、栈回溯 | +| `lapi.c` / `lauxlib.c` | `LuaState` 上的操作、`AuxLib` | +| `lundump.c` / `ldump.c` | 字节码载入 / 转储 | +| `lbaselib.c`、`lcorolib.c`、`loadlib.c`、`lstrlib.c`、`ltablib.c`、`lmathlib.c`、`liolib.c`、`loslib.c`、`lutf8lib.c`、`ldblib.c` | 对应的 `lib.*` 类 | +| `lgc.c` | **不存在** —— 见上 | + +`lauxlib.c` 等辅助层一并搬过来,因为标准库是照着它们写的。 + +--- + +## 关键设计决策 + +以下几处是"把 C 翻译成 Java"不能机械照搬的地方,选错了后面要花很大代价才补得回来。 + +### 1. Lua 字符串是字节串,所以不能用 Java `String` + +Lua 字符串可以包含 `\0`;`#s` 数的是**字节**;`s:byte(i)` 返回一个字节。 +Java 的 `String` 是 UTF-16,且不接受孤立代理项。任何处理二进制数据、UTF-8 文本 +或含 `\0` 的真实 Lua 项目,只要字符串底层用 `String` 就会出错。 +因此 Lua 字符串底层用 `byte[]`,所有字符串库函数按字节实现。 + +### 2. 值保留整数/浮点子类型区分 + +Lua 5.4 区分 `1`(integer)与 `1.0`(float):二者比较相等,但 `math.type` 不同, +`//` 与 `/` 不同,`1/2` 是 `0.5` 而 `1//2` 是 `0`,转字符串分别是 `1` 与 `1.0`。 +值采用**可变槽**结构(`type` 标签 + `long` + `double` + `Object` 载荷), +对应 C 的 `TValue`:既避免装箱,也让 VM 能原地写栈槽。 + +### 3. 表保留数组部分/哈希部分的分离 + +照搬 `ltable.c`,包括"这个整数键是否足够密集、能否放进数组部分"的迁移逻辑。 +整数键与字符串键是不同的项:`t[1]` 与 `t["1"]` 是两个不同的条目,与 Lua 一致。 + +### 4. 协程是独立的栈,不是线程 + +Lua 协程可以在任意深度的栈上 yield——`pcall` 内部、元方法内部、`for` 迭代器内部。 +原计划是每个协程给一条 Java 虚拟线程。阶段 5 实测下来这是个错的选择,已经改掉。 + +Lua 协程是**协作式**的:同一时刻只有一个在跑,挂起点由 `coroutine.yield` 显式 +指出。C 版的实现就是「每个 `lua_State` 一条独立栈 + `longjmp` 跳回 +`lua_resume`」。Java 有同样的两样东西——每个 `LuaState` 一条独立栈,加上一个 +把 Java 调用栈弹掉的异常(`LuaYield`)。**根本不需要线程**:Lua 那边的状态全在 +堆上,弹掉 Java 帧之后,栈、`CallInfo` 链、开放 upvalue 都原样留着,`resume` +时接着跑就行。 + +虚拟线程还有个问题是**过于宽松**:C 函数回调进 Lua 的地方(`table.sort` 的比较 +函数、`string.gsub` 的替换函数、被 `print` 调用的 `__tostring`)在真实 Lua 里 +不能 yield,会报 `attempt to yield across a C-call boundary`;换成虚拟线程它就 +「能跑」了。对一个目标是「一行不改跑现有 Lua 代码」的项目来说,这个差异是 bug +不是特性。 + +顺带的好处是 Java 下限从 21 降回 17——本机现在的构建就是 17。 + +取代线程的是 `ldo.c` 的忠实移植:`lua_resume` / `unroll` / `finishCcall` / +`finishpcallk` / `precover`,外加 `luaV_finishOp`(把被 yield 打断的那半条指令 +补完)。会被打断的 C 函数(`pcall`、`xpcall`)注册一个续体,也就是 Java 版的 +`lua_KFunction`。 + +### 5. 错误用 Java 异常,不用 `setjmp` / `longjmp` + +`lua_error` 变成抛出携带 Lua 错误值的异常。`pcall` / `xpcall` 捕获它,协程边界 +同样处理。需要小心处理:非字符串的错误值、带 `__tostring` 的错误对象、 +以及跨越协程边界传播的错误——都要与上游行为一致。 + +### 6. 不再用 JFlex / Bison + +Lua 参考实现的解析器是手写递归下降。直接搬过来,就去掉了构建期代码生成这一步, +以及旧架构里"生成物漂移"的问题。 + +### 7. 元方法:全部实现,包括那些别扭的规则 + +`__index`、`__newindex`、`__call`、`__concat`、`__len`、`__eq`、`__lt`、`__le`、 +`__unm`、`__add` …… `__idiv`、`__close`、`__gc`、`__mode`、`__name`、`__metatable`、 +`__pairs`、`__ipairs`。包括容易做错的部分:只有 `__lt` 时 `__le` 退化为 +`not (b < a)`;`__eq` 只在类型可比时触发;表上的 `__len` 忽略非整数结果; +`__index` 的链式查找。 + +--- + +## 实施路线 + +每一阶段结束都必须测试全绿;红着不许往下走。 + +| 阶段 | 内容 | 上游对应 | +|---|---|---| +| **0** | 地基:`LuaValue`、`LuaString` 与驻留、`LuaTable`、`LuaState`、错误类型、`Zio` | `lobject`、`lstring`、`ltable`、`lstate`、`lzio` | +| **1** | 词法 + 递归下降语法分析,按上游方式直接产出而不单独建树 | `llex`、`lparser` | +| **2** | 字节码:完整 Lua 5.4 指令集、代码生成、常量折叠、upvalue 解析 | `lopcodes`、`lcode`、`lfunc` | +| **3** | 解释器主循环:调用/返回、带类型转换的算术与比较、元方法派发 | `lvm`、`ldo`、`ltm` | +| **4** | 闭包与 open/closed upvalue、`goto`/label、变长参数、多重赋值与多返回值 | `lparser`、`lvm` | +| **5** | 协程:`coroutine.*`、yield/resume/unroll、yield 与错误跨边界 | `lcorolib`、`ldo`、`lvm` | +| **6** | 依赖回收的语义:弱表、`__gc`、弱字符串驻留池 | *(取代 `lgc.c`)* | +| **7** | 标准库:`base`、`coroutine`、`package`、`string`、`table`、`math`、`io`、`os`、`utf8`、`debug` | `l*lib.c`、`loadlib.c` | +| **8** | `load` / `loadstring` / `dump`、`require` 搜索器、`package.path` / `cpath` | `lundump`、`ldump`、`loadlib` | +| **9** | 一致性验证:上游测试套件 + 与真实 `lua5.4` 二进制的差分测试 | — | + +--- + +## 怎么证明"它真的是 Lua" + +一个只是**看起来像** Lua 的重写是没有价值的。四道闸门,强度递增: + +1. **上游测试套件**。直接跑 `lua-5.4.x/tests/*.lua`(`alls.lua`、`big.lua`、 + `strings.lua`、`coroutine.lua`、`pm.lua` 等),不做任何修改。这是主闸门—— + 上游自己的测试集比这里手写的任何用例都刁钻。 +2. **差分测试**。同一份脚本分别在本实现与真实的 `lua5.4` 二进制下运行, + stdout 必须逐字节相同。CI 里装 `lua5.4` 后自动跑。 +3. **差分模糊测试**。生成程序与参考二进制对比——这一条专门抓"不崩但算错"那类缺陷。 +4. **覆盖率门禁**(JaCoCo 行覆盖率)。 + +一个阶段只有在适用于它的闸门全绿之后才算完成。 + +--- + +## 现状 + +**阶段 10(上游 `lua-5.4.x/tests` 端到端跑绿)已完成。** 19 个「独立可跑且输出 +确定」的用例文件(`api`、`bitwise`、`calls`、`closure`、`code`、`coroutine`、 +`db`、`errors`、`events`、`gengc`、`goto`、`nextvar`、`pm`、`strings`、`tpack`、 +`utf8`、`vararg`、`verybig` 等)与真实 `lua5.4` 的输出**逐字节一致**; +`cstack` / `gc` / `literals` / `locals` / `constructs` 五个因为打印的是随实现或 +随随机数而变的东西,只断言「跑完并打印 `OK`」——理由逐个列在 *阶段 10* 一节里。 +全量 874 个用例绿,JaCoCo 行覆盖 0.96(门禁 0.95)。(上游套件这道闸门要 +`LUA54_TESTS` 或 `target/lua54-tests`;没有用例目录时整类自动跳过, +所以那种机器上「绿」其实比看起来少 25 个用例。) + +**阶段 9(`debug` 库,含 `sethook`)已完成。** 旧的 fakescript 语言实现已从 +`master` 移除,见下文 *迁移说明*。阶段 0~9 已落地:编译器(词法 → 语法 → 代码生成) +产出的字节码与真实 `lua5.4` **逐字段一致**,VM 已经能把它跑起来——158 + 121 + 60 ++ 32 + 25 + 16 + 21 + 36 段差分脚本与 2100 个生成程序的 stdout 与真实 `lua5.4` +**逐字节一致**,错误信息逐行一致。`string.dump` 的产物另外按原始字节比过, +不只是比行为。十个标准库(base、coroutine、package、string、utf8、table、math、 +os、io、debug)全部落地,`debug` 是完整的——钩子也在内。 + +阶段 6 是「不自己实现 GC」这个决定真正付出代价的一处。弱表、`__gc`、字符串池 +全部重建在 JVM 的可达性之上,而那与 Lua 的标记-清除**不是一回事**;有三处 +怎么也对不齐,列在下面的 *已知的不一致* 里。 + +已落地: + +| 部件 | 上游对应 | 说明 | +|---|---|---| +| `Lua` 类型标签 | `lua.h`、`lobject.h` | 保留整数/浮点两个变体子类型 | +| `LuaValue` | `TValue` | 可变槽,让 VM 能原地写栈槽 | +| `LuaString` | `TString` | 底层 `byte[]`;`\0`、任意字节与非法 UTF-8 都原样保存 | +| `StringPool` | `lstring.c` 的 `strt` | 弱引用驻留,不再引用的字符串不会钉住堆 | +| `LuaTable` | `ltable.c` | 数组部分 + 哈希部分、`rehash`、`#t` 边界、整值浮点键规范化 | +| `LuaError` | `lua_error` | 携带 Lua 值;只有这个类型会被 `pcall` 捕获 | +| `Zio` | `lzio.c` | 字节输入流;`EOZ` 是 -1,字节按无符号取 | +| `LCType` | `lctype.c` | 逐字节照搬 `luai_ctype_` 表,因此 `lislalpha` 与上游一样把 `_` 当字母 | +| `Numeral` | `lobject.c` 的 `luaO_str2num` | 先整数后浮点;十进制用严格正则,把 `1d`/`inf`/`nan` 按 C `strtod` 的方式拒掉 | +| `Lexer` | `llex.c` | 完整 token 集、`\z`/`\xXX`/`\u{XXX}`/`\ddd` 全部转义、长字符串与长注释、逐字节一致的错误信息 | +| `OpCode` | `lopcodes.h/.c` | 83 条指令;顺序、名字与 `luaP_opmodes` 对着上游头文件机械校验过 | +| `Proto` | `lobject.h`、`lfunc.c` | 指令、常量、嵌套原型、upvalue、局部变量、行号信息 | +| `Arith` | `lobject.c`、`lvm.c` | 常量折叠用的算术——与将来 VM 用的是同一套 | +| `ExpDesc`、`FuncState`、`DynData` | `lparser.h` | `expdesc` 的 union 摊平;块/goto/变量的簿记 | +| `Codegen` | `lcode.c` | 发指令、分配寄存器、跳转与回填、常量折叠、`NEWTABLE`/`SETLIST` | +| `Parser` | `lparser.c` | 手写递归下降,边解析边发指令 | +| `UpVal` | `UpVal` | 两态:开放时持有栈槽引用,关闭时持有私有副本 | +| `LClosure` | `LClosure` | 原型 + upvalue 数组 | +| `CallInfo` | `CallInfo` | 帧记录;`CIST_FRESH` 决定 Lua→Lua 返回在哪一层停 | +| `LuaState` | `ldo.c`、`lstate.c` | 栈、调用/返回、`pcall`、upvalue 开关闭、全部 `luaG_*` 错误 | +| `LuaTm` | `ltm.c` | 元方法查找与派发、`__le` 退化、to-be-closed | +| `Vm` | `lvm.c` | 解释器主循环,83 条指令全覆盖 | +| `BaseLib`、`MathLib` | `lbaselib.c`、`lmathlib.c` 与部分 `lstrlib.c` | 最小集合,但每条都对过差分 | +| `Diff`(测试) | — | 差分驱动的公共部分:一段脚本两边各跑一遍 | +| `ConformanceTest`、`CodegenTest`、`ParserTest`、`RuntimeTest` | — | 以真实的 `lua5.4` 二进制作为裁判 | +| `ClosureTest`(测试) | — | 阶段 4 闸门:闭包、goto、变长参数、多重赋值 | +| `CClosure` | `CClosure` | 绑定了值的 C 函数,`coroutine.wrap` 需要它 | +| 线程值(`Lua.VTHREAD`) | `LUA_TTHREAD` | 一个能被当作 Lua 值使用的 `LuaState` | +| `CoroutineLib` | `lcorolib.c` | `create` / `resume` / `yield` / `status` / `wrap` / `isyieldable` / `running` / `close` | +| `resume` / `unroll` / `finishCcall` / `finishpcallk` / `precover` | `ldo.c` | yield/resume 状态机,不用线程 | +| `LuaKFunction` | `lua_KFunction` | 被 yield 打断的 C 函数的续体 | +| `LuaYield` | `luaD_throw(L, LUA_YIELD)` | 用于展开的异常;刻意**不**被 `pcall` 捕获 | +| `Vm.finishOp` | `luaV_finishOp` | 把被 yield 打断的半条指令补完 | +| `CoroutineTest`(测试) | — | 阶段 5 闸门:60 段差分脚本,含在每个元方法里 yield | +| `FuzzTest`(测试) | — | 1500 个生成程序,另有 600 个跑在协程里并随机撒 yield | +| `LuaTable.WeakRef` | `lgc.c` 的 `iscleared` | 弱引用**就是**存储形式;额外记下身份哈希,因为 `identityHashCode(null)` 恒为 0,会把所有失效键一起塌到 0 号桶 | +| `LuaTable.purge` / `rewrap` | `cleartable` / `traverseweakvalue` | 失效条目在一轮收集结束时摘掉;`__mode` 变化时整体重新包装 | +| `LuaState.clearDeadStackSlices` | `traversethread` 的 atomic 分支 | 把所有线程栈的 `[top, stack_last)` 置 nil,只在 `collectgarbage` 里做 | +| `GcSentinel` | `lgc.c` 的 `GCTM` | 只被目标表引用的哨兵;它的 `finalize` 把表复活到待终结队列,由 **Lua 线程**排空 | +| `LuaState.close` | `lua_close` → `callallpendingfinalizers` | 退出时把还有 `__gc` 的对象全跑一遍 | +| `WeakTableTest`(测试) | — | 阶段 6 闸门:20 段差分脚本,拆成「依赖收集时机」与「恒成立」两半 | +| `GcTest`(测试) | — | 阶段 6 闸门:12 段脚本——只跑一次、错误静默、后置 `__gc` 不生效 | +| `LuaUserdata` | `Udata` | 载荷 + **每对象**元表;`luaL_testudata` 要的正是后者 | +| `Undump` | `lundump.c` | 读预编译 chunk:头部校验、变长长度、`binary string` 源名规则 | +| `Dump` | `ldump.c` | 写预编译 chunk:`string.dump` 与 `strip` | +| `LoadTest`(测试) | — | 阶段 8 闸门:25 段脚本——模式、chunk 名、`_ENV`、读函数、`loadfile` / `dofile` | +| `DumpTest`(测试) | — | 阶段 8 闸门:16——运行期 stdout 与 `string.dump` 产物都逐字节比 | +| `StringPack` | `lstrlib.c` 的一部分 | `string.pack` / `unpack` / `packsize`;标准库里唯一平台相关的一角 | +| `PackTest`(测试) | — | 21 段脚本——整数、浮点、字符串、对齐、宽整数、报错、键集合 | +| `DebugLib` | `ldblib.c` + `ldebug.c` / `ldo.c` / `lauxlib.c` 的一部分 | `getinfo` / 局部变量 / upvalue / `traceback` / 注册表 / 元表 / 用户值 / `setcstacklimit` / `sethook` / `gethook` | +| `LuaHook` | `lua.h` 的 `lua_Hook` | 钩子回调;`DebugLib` 的 `hookf` 就是 `sethook` 装的那个 | +| `LuaLibs` | `linit.c` | `luaL_openlibs`;按 `loadedlibs` 的顺序开十个库 | +| `DebugTest`(测试) | — | 阶段 9 闸门:36 段脚本——层号、选项、局部变量、upvalue、`traceback`、深栈,外加 14 段钩子用例 | +| `LuaLibsTest`(测试) | — | `openAll` 真的开全了十个,而且库表就是元表指的那张 | + +### 字节码这道闸门 + +编译器是**用差分比对验证的,不是靠自称的预期值**。`src/test/resources/fkdump.c` +是挂在参考 `lua5.4` 源码上的一个小转储器:对每份脚本,它把每条指令的 +**全部字段**(`a`、`b`、`c`、`k`、`Bx`、`sBx`、`Ax`、`sB`、`sC`、`sJ`)、行号、 +常量表、upvalue 表递归地打出来。`CodegenTest` 拿它和 `ProtoDump` 渲染出的 +`Proto` 逐行比对,要求完全相同。 + +语料有两份:约 50 个覆盖各种语法结构的小脚本,以及**上游 `lua-5.4.7` 测试套件** +的 33 个文件(CI 里下载到 `target/lua54-tests`),只编译不运行。全部一致。 + +错误信息同样走差分:`ParserTest` 把写坏的源码落盘交给真实 `lua5.4` 跑, +再逐字节比对消息——`'end' expected (to close 'if' at line 1)`、 +`label 'a' already defined on line 1`、` at line 1 jumps into the scope of local 'x'` +等等。 + +词法层是**用差分比对验证的,不是靠自称的预期值**:一批故意写坏的源码先落盘 +交给真实 `lua5.4` 跑,再把同一份字节喂给 `Lexer`,逐字比对错误信息(含 +`near ''` 那一段——`` 与其他 token 的加引号规则并不一样)。 +另一批用例反向验证:参考实现能加载的源码,这里也不许报错。 + +本阶段有两条语义是**跑参考实现实测**定下来的,而不是照手册推的: + +* 整值浮点作键时,写入会**规范化成整数键** + (`t[1.0]` 之后 `math.type(next(t))` 是 `"integer"`,`#t` 是 1); +* 写 **NaN** 键报 `table index is NaN`,而**读** NaN 键静默返回 `nil`。 + +两条都由 `ConformanceTest` 里跑真实 Lua 5.4 的用例锁住。 + +### 本阶段由参考实现裁定的三件事 + +每一条看起来都像 bug,其实不是;三条都有测试锁住。 + +* **`>>` 是逻辑右移**。`intop(>>,x,-y)` 先把操作数转成 `lua_Unsigned`, + 所以 `-8 >> 2` 是 `4611686018427387902`,不是 `-2`。 +* **上游的常量表里确实有重复项**。`ls->h`(「常量 → 下标」缓存)由整份源码共享, + 而每个函数的常量各自从 0 开始编号。内层函数会把缓存里的下标改成自己的小下标; + 回到外层时那个下标已经指向别的常量,校验失败,于是再追加一份重复的。 +* **整值浮点要用扰动过的缓存键**。Lua 的表用 `==` 判键,`100000` 与 `100000.0` + 是同一个键;没有 `luaK_numberK` 里那个 `r + r*2^-52`,两者会互相把对方从缓存 + 里挤掉,每用一次就新建一份常量。 + +### 运行期这道闸门 + +`RuntimeTest` 是阶段 3 的闸门。每条用例都是一段脚本**跑两遍**——本实现一遍、 +真实 `lua5.4` 一遍——两份 stdout 必须相等。出错的用例比对消息行,含 +`(local 'x')` / `(constant '10')` / `(method 'nope')` 这类后缀。 + +栈靠一条不变量撑着,其它一切都以它为前提: + +> 栈槽 `LuaValue` **永不被替换**。`LuaState` 永远做 `stack[i].set(v)`, +> 绝不做 `stack[i] = v`。 + +只有守住这一条,「开放 upvalue 持有栈槽的引用」在 Java 里才等价于 C 里的 +「开放 upvalue 持有栈槽的指针」。破一次,闭包就会静默地看不到更新。 + +现在就可以这样跑: + +```java +LuaState L = new LuaState(); +BaseLib.open(L); +MathLib.open(L); +L.run("print('hello from Lua')"); +``` + +### 本阶段由参考实现裁定的六件事 + +每一条最初都凭手册或直觉写错了,六条全部由差分测试锁住。 + +* **算术不做字符串转换,`"10" + 1 == 11` 也不来自 VM。** `lvm.c` 的算术宏用的是 + `tonumberns` / `tointegerns`,两者都不认字符串。转换在 `lstrlib.c`:它给字符串 + 元表挂上 `__add` 等元方法,先把两侧 `lua_stringtonumber`,再 `lua_arith`。 + 少了这一组,就会得到一个看起来合理但与参考实现不同的 + `attempt to perform arithmetic on a string value`。 +* **C 函数的 n 个返回值是「栈顶最后 n 个」。** `ipairsaux` 先压 `i`、再压 `t[i]`, + 当 `t[i]` 是 nil 时 `return 1`——泛型 for 拿到的正是最后那个 **nil**,不是 `i`。 + 按直觉写成「返回 i」,`for _, v in ipairs(t)` 永远不会终止,因为循环只在 + **第一个**返回值为 nil 时退出。 +* **每条算术指令后面必定跟一条 `OP_MMBIN*`。** 成功就 `pc++` 跳过它, + 失败就自然落下去派发元方法。漏一个 `pc++`,正确的表达式也会多跑一次元方法。 +* **`OP_NEWTABLE` 的 `EXTRAARG` 是无条件的**(因为 `k == 0` ⟺ `Ax == 0`), + 而 `OP_SETLIST` 的是有条件的。按条件读前者,每个表构造都会报 + `malformed opcode EXTRAARG`。 +* **C 的 `%g` 会去掉末尾的零,Java 的不会。** 在手写 `formatG` 之前, + `1.5` 被打印成 `1.5000000000000`。 +* **`luaG_runerror` 与 `luaL_error` 取位置的方式不同。** 前者看**当前**帧的 + `isLua(ci)`;后者是 `luaL_where(L, 1)`,会往回跳一层到调用者。库函数用错了 + 前者,就完全没有 `src:line:` 前缀——因为它自己那帧是 C 帧。 + +### 差分 fuzz 这道闸门 + +`FuzzTest` 用一个小文法生成 2100 段程序,逐段与参考实现比对:1500 段常规程序, +另有 600 段把主 chunk 塞进 `coroutine.create`、随机撒 `coroutine.yield`,再用一个 +有界的 `resume` 循环驱动。两条约束保证它 +不会变成一个麻烦制造机: + +* **必须终止。** 只生成有界的 `for` / `ipairs`,不生成 `while`、`repeat`、 + `goto`;函数调用只允许指向**更早定义完**的函数,于是调用图是 DAG, + 递归与死循环都不可能。 +* **必须可复现。** 种子从 0 递增,不用 `Math.random`。失败时凭「第几个种子」 + 就能精确复现。 + +生成器不知道我们认为 Lua 该怎样——这正是它的价值:它是唯一能抓住 +「不崩、但答案错」的闸门。 + +### 本阶段由参考实现裁定的四件事 + +* **`luaF_newtbcupval` 在赋值当下就检查 `__close`,不是在作用域结束时。** + `OP_TBC` 紧跟在局部变量拿到值之后,所以 `local x = 5 print(x)` + 会在打印之前就抛 `variable 'x' got a non-closable value`。`nil` 与 `false` + 豁免(`l_isfalse`),所以 `local x = nil` 是合法的。 +* **展开过程中 `__close` 里的错误会顶掉原错误。** `error('orig')` 之后 + `__close` 里再 `error('in close')`,`pcall` 拿到的是 `in close`。 + 沿用原错误是错的。 +* **代码生成只折叠 `OPR_MINUS` 与 `OPR_BNOT`,从不折叠 `OPR_LEN`。** + 所以 `#7.5` 是**运行期**错误,不是加载期错误。折叠它会让一段上游能编译、 + 只在运行时才失败的脚本变成编译不过。 +* **两条闸门自身的规则,都是踩过才知道。** 脚本出错时它已经打出的 stdout + **仍然要参与比对**(参考实现的 stdout 与 stderr 是一起捕获的,「先打十行 + 再崩」的输出里两者都有);而 `lua.c` 加的程序名前缀要**全文抹掉**, + 不能只抹开头——一旦 stdout 在前面,它就出现在输出中间了。 + +### 本阶段被参考实现裁定的五件事 + +* **`pcall`、`xpcall`、元方法、`for` 迭代器都可以 yield;标准库的回调不可以。** + `table.sort` 的比较函数、`string.gsub` 的替换函数、被 `print` 调用的 + `__tostring` 都会报 `attempt to yield across a C-call boundary`——因为 + `ltablib.c` / `lstrlib.c` / `luaL_callmeta` 是经 `lua_call` + (`luaD_callnoyield`)回调进 Lua 的。这恰恰是虚拟线程会「修好」、 + 而实际上是改错的行为。 +* **主线程永远不可 yield。** `lstate.c` 的 `lua_newstate` 无条件执行 + `incnny(L)`,所以主协程上 `coroutine.isyieldable()` 是 `false`,哪怕一个 C + 调用都还没发生。协程起始为 0,每次 `resume` 也重置为 0(`lua_resume` 只从 + resume 方拷 `nCcalls` 的低 16 位)。 +* **`resume` 的参数就是 `yield` 的返回值。** `lua_resume` 里那句 + `poscall(ci, nargs)` 决定了 `resume(co, 99)` 之后 `coroutine.yield(1)` 看到的 + 是 `99`——搬回去的值的个数是传进来的个数,不是 yield 出去的个数。 +* **`coroutine.wrap` 报错用的是自己的名字,不是 `create` 的。** `luaB_cowrap` + 直接调 `luaB_cocreate`,而 `luaL_argerror` 的函数名是从「当前帧是被谁调用的」 + 倒推出来的,所以 `coroutine.wrap(nil)` 报的是 + `bad argument #1 to 'coroutine.wrap'`。 +* **`luaV_finishOp` 每个可中断操作码都有一个分支,`OP_CONCAT` 那个不能省。** + 拼接是一批一批做的,`__concat` 里 yield 之后要把元方法结果往回挪一格、把 + `top` 收一格,再把剩下的元素拼完——可能又 yield 一次。省掉它的表现是 + 拿到**操作数那张表**而不是拼接结果。 + +### 阶段 6:参考实现裁定的五件事 + +* **GC 可见的栈到 `L->top` 为止,这一点是承重的。** `lgc.c: traversethread` + 只 mark `[stack, top)`,然后在 atomic 阶段把 `[top, stack_last)` 整段置 nil + ——源码注释就叫 “clear dead stack slice”。所以**落在 `top` 之上的寄存器里的 + 值对收集器不可见**,会被收掉。两种形态依赖这一点:C 函数压出来的值(比如 + `next` 取回的键,调用结束 `top` 回落,槽还攥着它),以及循环最后一次迭代 + 建的表(`for i = 1, 5 do t[i] = {i} end` 之后 `{5}` 还留在寄存器里,而 + `L->top` 只到调用参数处)。我们这边 `stack` 是条 Java 数组,**每个槽都是活 + 引用**,所以 `LuaState.clearDeadStackSlices` 必须把这一步补回来——而且得在 + `collectgarbage` 里做,不能在每次改 `top` 时做(见下一条)。 +* **……但在每次改 `top` 时急切地清,会撞坏两处。** C 只在 atomic 阶段清。 + 照抄到内联位置,会静默毁掉 `luaT_callTMres` 的结果(目标寄存器**就是**弹出 + 的那一格),以及 `pcall` 的错误对象(`LuaError.value` 直接别名了活栈槽—— + `error('boom', 0)` 抛的就是 `L.arg(0)` 本身——于是 `pcall` 变成返回 + `false nil`)。等到 `collectgarbage` 再清既忠实,也同样精确。 +* **弱表的存储必须「本身是弱的」,不是在旁边挂一个 `WeakReference`。** 槽里的 + 载荷就是那个 `WeakReference`;载荷若是强的,就什么也释放不掉。这带来一个 + 后果:弱表上 `LuaTable.get` 只能交出**副本**,于是写路径那条快速分支 + (`OP_SETTABLE` 的「槽已存在,直接写进去」)不能用在弱表上——往副本里写等于 + 没写,症状是 `t.f = g` 之后 `t.f` 还是 `f`。 +* **`__mode` 是动态读的,而且要把已在表里的条目重新包装一遍。** 先建表、塞好 + 条目、再 `setmetatable(t, {__mode = 'k'})`,那些条目照样会被回收——实测过。 + 既然弱是本实现的存储形式,模式一变就得遍历全表重新包装;而且弱表只能把所有 + 东西放在哈希部分(数组槽是普通 `LuaValue`,放不了 `WeakRef`)。 +* **`__gc` 要求对象还「活着」,这是 JVM 所有引用类型都给不了的。** 每一种 + 「它死了」的通知(`WeakReference` 被清、`Cleaner`、`PhantomReference`)都发生 + 在对象已经不可达之后,引用再也取不回来。只有 `finalize()` 是 `this` 还能用 + 的时候被调到,而且**允许复活**。于是挂了 `__gc` 的表会得到一个只有它引用的 + 哨兵:两者一起不可达时,哨兵的 `finalize` 把表塞进待终结队列,下一次 + `collectgarbage` 在 **Lua 线程**上排空——绝不能在 JVM 终结线程上调 Lua 函数, + 那里既会和 `LuaState` 抢数据,抛出的异常也会被 JVM 连日志都不留地吞掉。 + +### 已知的三处与参考实现不一致 + +这些不是疏漏,每一处都是 JVM 没有对应原语的地方,如实记着而不是糊过去。 + +* **ephemeron 只做对了「值就是键」这一种。** `__mode='k'` 下 `t[k] = k` 会摘掉 + 条目(与参考实现一致),常见的缓存形态 `cache[obj] = {新数据}` 会保住值(也 + 一致)。但 `t[k] = {k}`——值**包含**键、键又没有别的引用——本实现会留着, + 而 Lua 会摘掉。两半在 Java 里互斥:值存强的会把键养活,值存弱的又会在「键 + 活着但值没有别的引用」时把值弄丢,而那正是最常见的缓存用法。丢数据比漏摘 + 条目严重,所以选了漏摘。 +* **键挂了 `__gc` 的弱条目会早一轮被摘掉。** Lua 会 mark 带终结器的对象,于是 + 那一轮条目还在;本实现靠 `WeakReference` 被清来判死活,同一轮就摘了。 +* **挂了 `__gc` 的对象只在 `collectgarbage` 时才被回收。** JVM 除了真收掉它, + 没有别的办法告诉我们「它不可达了」,所以没有显式收集周期就排不上终结器。 + +### 阶段 7 到目前为止 + +`string`(`StringLib`)、`table`(`TableLib`)、`math`(`MathLib`)、 +`utf8`(`Utf8Lib`)、`os`(`OsLib`)、`io`(`IoLib`)、`package` +(`PackageLib`)已经搬完,各带一道差分闸门:`StringLibTest`(29)、 +`MathLibTest`(26)、`TableLibTest`(18)、`Utf8LibTest`(13)、 +`OsLibTest`(24)、`IoLibTest`(35)、`PackageLibTest`(22)。 + +### 阶段 7 里由参考实现裁定的十件事 + +* **共享的 `lauxlib.c` 那一层必须**先于**任何库落地。** 参数检查 + (`checkString` / `checkInteger` / `argError` / …)不是脚手架:错误文本本身 + 就是可观测契约的一部分,六个库各抄一遍,一周内就会彼此走样。其中一处只有 + 在「参数从 1 起编号」时才对:`s:rep(nil)` 报的是 `bad argument #1`,因为 + `luaL_argerror` 对方法调用会减一。 +* **`math.max` / `math.min` 用的是 `lua_compare`,不是数字比较。** 所以 + `math.max("a","b")` 是 `"b"`,`math.max(1,"b")` 报的是 + `attempt to compare number with string`(走 `__lt` 失败那条路)。它们还保留 + 操作数的子类型:`math.max(1, 2.0)` 是 `2.0`(float)。 +* **`math.fmod` 的浮点路径是 C 的 `fmod`,不是 Lua 的 `%`。** 两者结果符号 + 不同:`math.fmod(-7.5, 2)` 是 `-1.5`(取被除数符号),而 `-7.5 % 2` 是 + `0.5`(取除数符号)。顺手复用 VM 的 `Arith.modf` 是最容易犯的错,且正好 + 差一个符号。 +* **`math.log` 的 base 2 / base 10 两条分支确实是不同的计算。** C 调的是 + `log2` / `log10`:`log(x)/log(2)` 与 `log2(x)` 在 43 个样本里有 9 个不同, + `log(x)/log(10)` 与 `log10(x)` 更是差 25 个。Java 没有 `log2`,这里用尾数 + 分解 `x = m * 2^e` 还原成 `e + log(m)/ln2`,192 个样本里 190 个落在 1 ulp 内。 +* **`table.insert` 落在带 `__newindex` 的对象上必须真的调它。** `ltablib.c` + 用的不是 `luaH_setint` 而是 `lua_seti`,后者走 `luaV_finishset`。写成裸写 + 会让 `table.insert(ud, v)` 静默地什么都不做——不报错,也不写入。 +* **`os.execute` 起的进程名必须是 `sh`,不能是 `/bin/sh`。** C 的 `system()` + 是 `execl("/bin/sh", "sh", "-c", cmd)`——**argv[0] 是 `sh`**——而 shell 的 + 诊断信息是用 argv[0] 打头的。写成全路径会把 `sh: 42: command not found` + 变成 `/bin/sh: 42: command not found`,差分闸门立刻就能看到。 +* **userdata 的元表在对象上,不在类型上。** `luaL_testudata` 问的是「这个 + userdata 的元表是不是注册在 `FILE*` 名下那一张」,所以文件句柄不能跟内部 + userdata(随机数状态、`gmatch` 的状态)共用一张按类型分的元表,否则后者 + 会被当成文件。为此补了 `LuaUserdata`,顺带补了它的 `__gc`——不然脚本里忘了 + `f:close()` 的文件永远不会关。 +* **真实文件上的 `io` 必须块缓冲,而 `io.write` 不能缓冲。** C 的 stdio 对 + 文件是缓冲的:`io.output(p):write("x")` 之后把 `p` 读回来是**空的**,那几 + 个字节还在 `FILE*` 里。标准流正好相反——它们是行/无缓冲的,而且 `io.write` + 的输出必须落到宿主装的那条流上。所以 `IoLib` 对 `RandomAccessFile` 的写做 + 缓冲(`flush`、`seek`、任何读、以及关闭时落盘),对 `stdin` / `stdout` / + `stderr` 则直接写穿。 +* **`require` 判断搜索器结果用的是 `lua_isstring`,不是「是不是字符串」。** + 返回**数字** `42` 的搜索器,那个 `42` 会被拼进「找不到模块」的消息里; + 只有既非字符串又非数字的结果才被当成「这个搜索器没说什么」,并把刚加的 + `\n\t` 前缀退掉。 +* **加载期错误的源名也要过 `luaO_chunkid`。** 模块文件是按 `"@" + 路径` 编 + 译的,但 `luaG_addinfo` 打印前会把 `@` 剥掉,所以 require 一个语法错误的 + 模块报的是 `/tmp/m.lua:1: syntax error near 'is'`,不是 `@/tmp/m.lua:1: ...`。 +* **每个库都要同时登记进 `_LOADED`,不能只进 `_G`。** 漏掉的是 `coroutine`: + 它当时只在全局表里,于是 `require("coroutine")` 报 + `module 'coroutine' not found`。 + +### 一处承认下来、不去修的偏离 + +`sin` / `cos` / `tan` / `asin` / `acos` / `atan` / `exp` / `log` / `log10` / +`sinh` / `cosh` / `tanh` / `pow` 与参考实现**不是逐位相同**。Java 的 `Math` +是 fdlibm,参考二进制链的是平台 libm(本机是 macOS 的 `libsystem_m`)。两者 +对真值的误差都在 1 ulp 之内,但最后一位的舍入不同:43 个典型输入里 `sin` +差 1 个、`tan` 差 12 个、`cosh` 差 6 个。要消掉就得自带一份 libm,那既超出 +目标,也不符合 Lua 自己给出的保证——Lua 的结果本来就随平台漂。所以差分用例 +对这几个函数只比到 14 位有效数字(1 ulp 约 2.2e-16 相对误差)。能对齐的地 +方都对齐了:`sqrt`、`abs`、`floor`、`ceil`、`frexp`、`ldexp` 以及上面那两条 +`log` 分支都是逐位一致的。 + +### `io` 里四处承认下来、不去修的偏离 + +* **`file (%p)`。** `f_tostring` 打的是指针,`print(io.stdout)` 无论如何也不 + 可能逐字节相同。`IoLib` 给一个同形状的十六进制值,差分比对把 `0x…` 抹掉。 +* **`stdin` 的定位。** 参考实现在测试里的 stdin 被定向到 `/dev/null`,那是 + **可定位**的字符设备(`lseek` 返回 0);本实现这边是字节数组,定位不了。 + `io.stdout:seek()` 两边一致(管道上是 `nil, "Illegal seek", 29`); + `io.stdin:seek()` 不比对——差的是流的来源,不是 `io` 的行为。 +* **更新模式上「读完不定位直接写」。** 字节落到哪里由 C 的预读缓冲定,ISO C + 里是未定义行为。实测:12 字节的文件落到 13、6002 字节的落到 4097、 + `read(5000)` 之后落到 5001。本实现写在逻辑位置上。良定义那条路(中间有 + `seek`)两边一致。见下文 *`io.open` 的模式矩阵*。 +* **append 档写完之后的位置。** 数据在缓冲里时参考给 `base+n`,压掉 + 缓冲后给 `EOF+n`(分界是 4096);本实现一律给 `EOF+n`(这一边符合 POSIX)。 + **写进去的内容两边始终一致**。见同节。 + +### `package` 里两处承认下来、不去修的偏离 + +这里如实走 `loadlib.c` 的兜底分支:JVM 上没有 `dlopen`(真要接得走 JNI 或 +FFI,那是另一个工程),所以 C 搜索器的行为就是 C 版在「未启用动态库」的 +平台上的行为。 + +* **`package.loadlib`** 返回 `nil, "dynamic libraries not enabled; check your + Lua installation", "absent"`。参考二进制是用 `dlopen` 编出来的,那边给的 + 是 `dlerror()` 的文本和 `"open"`。 +* **`require` 一个真能找到 `.so` 的模块**会停在「动态库未启用」这一步, + 而不是真的加载它。只有 `.lua` 的模块与 `package.preload` 里的条目不受影响。 + +### 阶段 8 到目前为止 + +`load` / `loadfile` / `dofile`(`BaseLib`)与二进制 chunk 那对 +`lundump.c` / `ldump.c`(`Undump` / `Dump`)已经落地,各带一道差分闸门: +`LoadTest`(25)与 `DumpTest`(16)。 + +`DumpTest` 用**两种粒度**比,第二种才是这一阶段的硬闸门: + +1. **运行期**:脚本的 stdout 逐字节比,与其它闸门一致; +2. **产物**:两边各自把 `string.dump(f)` 写成文件,再比这两个文件的字节。 + +只比第一种的话,「编译产物略有偏差、但恰好能跑」是漏得过去的。比产物则把 +常量顺序、行号表、局部变量与 upvalue 的调试信息、`maxstacksize` 一起钉住了。 +目前比了八种形状:普通、内嵌函数、变长参数、带 upvalue、`goto`、循环、各类 +常量,以及一个超过 `LUAI_MAXSHORTLEN` 的常量字符串(dump 与 undump 两侧都走 +不驻留的长字符串那条路)。 + +### 阶段 8 里由参考实现裁定的六件事 + +* **`luaU_undump` 在报错之前会先改写源名。** 开头的 `@` 或 `=` 要去掉;如果 + 源名以签名首字节开头(也就是源名本身就是那段二进制 chunk—— + `load(s)` 不给第二个参数时正是这样),就整段换成字面量 `binary string`。 + 于是 `load("\27Lua")` 报的是 + `binary string: bad binary format (truncated chunk)`,不是一串 ESC 开头的乱码。 +* **二进制那条路 `f_parser` 吃掉首字节,文本那条路要还回去。** + `luaU_undump` 的注释写得直白:"skip 1st char (already read and checked)"。 + 在 undump 之前回退,整个头部会错开一个字节,`truncated chunk` 就变成了 + `not a binary chunk`。 +* **`luaO_chunkid` 有第三种形态,而 `load` 碰到的正是它。** 只有 `@x` 与 + `=x` 是剥前缀,其余一律包成 `[string "x"]`。`load("x++")` 报的是 + `[string "x++"]:1: syntax error near '+'`——只剥不包的实现会报 `x++:1:`, + 于是每一条加载期错误都是错的。 +* **源名是字节串,`luaO_chunkid` 按字节截。** 先解码成 Java `String` + 再截,遇到非 UTF-8 的名字长度就对不上。所以 `chunkId` 的核心写成 `byte[]` + 版,`String` 那个重载走它。 +* **没有源名的原型,位置整段都不给,连行号也不给。** + `luaG_addinfo` 是 `if (src) ... else 原样返回 msg`。这一点是可观测的: + `string.dump(f, true)` 会剥掉源名,于是 + `pcall(load(string.dump(f, true)))` 报的是裸的 `boom`。退化到「用 chunk 名 + 兜底」不但多一截前缀,还会把那段二进制 chunk 塞进消息里——它是非法 UTF-8, + 解码出来就是替换字符。 +* **`lundump` 里的 `default: lua_assert(0)` 是空操作。** + `lua_assert` 在默认构建下编成 `((void)0)`,所以常量类型字节写成 `99` 的 + chunk 能正常加载,那个常量就停在 `nil` 上。在那儿报错是我们自己发明语义。 + 这一条是量出来的(手工拼的 chunk 在参考实现上加载成一个 `function`), + 不是推出来的。 + +dump 那一侧还有一条:`dumpFunction` 比源名用的是引用 +(`f->source == psource`)而不是内容。短字符串驻留之后两者等价,但长字符串 +不驻留,C 会为每个内嵌函数各写一遍源名;按内容比会写得比 C 少。 + +### `string.pack` / `string.unpack` / `string.packsize` + +作为 `StringPack`(`lstrlib.c` 的 pack/unpack 那半段)落地,配 `PackTest` +(21 段)。这是标准库里**唯一一处输出取决于宿主平台**的部分:`l` / `L` 是 +C 的 `long`,`T` 是 `size_t`,`!` 的默认最大对齐是 `LUAI_MAXALIGN` 的偏移, +默认字节序取宿主的。这些按参考二进制所在的平台(LP64 + 小端)钉死—— +在 `long` 只有 4 字节的平台上,参考实现自己给出的字节也不一样。 + +这里还有两处由参考实现裁定的: + +* **`X` 会吃掉它后面那一项。** `getdetails` 调 `getoption` 是为了拿到后一项的 + 对齐要求,而那次调用同时把格式串往前推了一格。于是 + `string.packsize("Xi4")` 是 `0`,`string.pack("Xi4", 7)` 得到空串。 + 「先看一眼再决定」的实现会给出 `4`——看着更合理,但它是错的。 +* **`str_pack` 那个 `lua_pushnil` 标记是可观测的。** 它本来只是把参数与字符串 + 缓冲隔开,但它站在 2 号位置,所以不传第二个参数时 + `string.pack("c2")` 报的是 `string expected, got nil` 而不是 + `got no value`。 + +### 阶段 9 —— `debug` 库,钩子也在内 + +`ldblib.c` 落地为 `DebugLib`,顺带把 `ldebug.c` 里只有内省接口才用到的那部分 +(`lua_getstack` / `lua_getinfo` / `luaG_findlocal` / `luaG_traceexec` / +`luaG_tracecall`)与 `lauxlib.c` 的 `luaL_traceback` 一起搬进来。闸门是 +`DebugTest`(36 段脚本)。十六个函数全部落地。 + +`LuaLibs.openAll`(`linit.c` 的 `luaL_openlibs`)也一并落地——之前「开标准库」 +是十次调用,只有测试驱动知道顺序。 + +#### 钩子这套机制,以及它的调用点都在哪儿 + +`sethook` 不是一个能单独搬的库函数:它只是一个分发器,真正的调用点散在 +解释器各处。搬它意味着除了 `DebugLib.dHook`(`ldo.c: luaD_hook`)之外还要补: + +| 调用点 | 上游 | 触发 | +|---|---|---| +| `LuaState.precallC` | `ldo.c` | C 函数的 `call`——在 C 函数本身之前 | +| `DebugLib.dHookCall`,由 `Vm` 的 `startfunc` 与 `OP_VARARGPREP` 调 | `ldo.c: luaD_hookcall` | Lua 函数的 `call` / `tail call` | +| `DebugLib.rethook`,由 `LuaState.poscall` 调 | `ldo.c: rethook` | 两类函数的 `return` | +| `DebugLib.traceExec`,由 `Vm` 的 `vmfetch` 调 | `ldebug.c: luaG_traceexec` | `line` 与 `count` | + +外加 `CallInfo.trap` 与 `LuaState` 上的 `hook` / `hookmask` / `hookcount` / +`allowhook` / `oldpc`。`trap` 的意义在于它是 `luaV_execute` 里的一个局部 +变量——不开钩子时主循环连分支都不走。 + +#### 又四件由参考实现裁定的事 + +* **`lua_sethook` 必须调 `settraps`。** 主循环里的 `trap` 是局部变量,只在少数 + 几个点(`updatetrap`)才从帧上重读。所以「执行到一半把钩子打开」在别的帧上一 + 点用都没有,除非把每个活着的 Lua 帧的 `trap` 都置成 1——这正是 `settraps` 做的。 + 少了它,`debug.sethook(f, "l")` 一个行事件都不会来。 +* **钩子里的错误,不会被它所报告的那个 `pcall` 抓住。** `pcall` 的 `call` + 钩子是在 `precallC` 里、在 `pcall` 建好恢复点之前报的,所以 + `pcall(function() ... end)` 抓不到「钩子在 `pcall` 自己身上抛的错」。 +* **`funcnamefromcall` 里 `CIST_HOOKED` 要判在 `isLua` 之前。** 带这个标记 + 的那一帧常常是 C 帧(`pcall` 的调用钩子就打在 `pcall` 自己的 C 帧上), + 先判 `isLua` 永远走不到这一支——回溯里那一行会从 `in hook '?'` 变成 + `in function <文件:行>`。 +* **钩子永远不能 yield。** `hookf` 用 `lua_call` 调用户函数,而 `lua_call` 走 + `luaD_callnoyield`,「不可 yield」的层数在钩子里非零。实测:钩子里 + `coroutine.yield()` 在主线程上报 `attempt to yield from outside a coroutine`、 + 在协程里报 `attempt to yield across a C-call boundary`,两种情况都让程序/协程 + 死掉。这就是为什么本实现根本没搬 `CIST_HOOKYIELD`——从 Lua 里走不到 + (上游那个标记是给「由 C API 直接装的 C 钩子」用的,那种钩子不走 `lua_call`)。 + +#### 由钩子这道闸门抓出来的一个顺序 bug + +`allowhook` 必须在保护调用边界上恢复,而且是在消息处理函数跑完之后, +不是在它之前。处理函数是在抛出点跑的,那时 `allowhook` 还是 0(正是这一点让回溯 +不会重新进钩子)。提前恢复成 1,回溯就会再触发一次钩子、再抛一次错,最后被 +`applyErrMsg` 兜成 `error in error handling`。症状是 +`debug.sethook(function() error("x") end, "c")` 之后那个未捕获错误打出来的是 +`error in error handling`,而不是真正的回溯。 + +#### 阶段 9 里由参考实现裁定的五件事 + +* **层号 0 是 `getinfo` 自己的 C 帧。** `lua_getstack(L, 0, &ar)` 会成功, + 给出 `what = "C", currentline = -1, name = "getinfo"`。名字来自 + `funcnamefromcall` 读调用者那一帧,所以它回答的是「调用者当时在调谁」, + 不是「这一帧叫什么」。搞错一处,所有 `debug.getinfo(n)` 整体错一层。 +* **`'f'` 与 `'L'` 要 `lua_rotate`,不能弹。** 结果表是 `lua_getinfo` 之后 + 才压的,所以 `lua_getinfo` 自己的产物在它底下。 + `treatstackoption` 是 `lua_rotate(L, -2, 1)` 再 `lua_setfield(L, -2)`。 + 写成 `L.top--` 弹掉的是结果表本人——症状是 `debug.getinfo(1)` 返回一个 + 打印出来像函数的东西,而 `debug.getinfo(1).func` 里存着那张表。 +* **`pushfuncname` 先试全局名。** `error` 在 `getobjname` 眼里是 + `global 'error'`(一条 `GETTABUP _ENV`),但回溯打的是 `function 'error'`—— + `pushglobalfuncname` 在 `_LOADED._G` 里按值反查到了它,并且优先。 + 只看 `ar.namewhat`,每个有全局名的 C 函数的修饰语都会错。 +* **消息处理函数在栈还没退的时候跑** —— `luaG_errormsg` 是在抛出点调它的, + 不是在 `luaD_pcall` 的恢复里。这就是 `xpcall(f, debug.traceback)` 能打出完整 + 回溯、而不是只剩最外面两层的原因。Java 没有 `longjmp`,但 `catch` 到的那一刻 + Lua 栈(`stack` / `top` / `ci`)还是完好的,所以把处理函数放在 + `pcall` 的 `catch` 最前面跑是等价的。 +* **`LEVELS1` / `LEVELS2` 是 10 和 11**,不是整整齐齐的 10/10,头尾也不等长。 + 否则「(skipping N levels)」那个数差 1——而且只在深到会触发它的栈上才暴露。 + +#### 由 `debug` 这道闸门抓出来的两个 bug,都不在 `debug` 里 + +* **`OP_TESTSET` 的判条件写反了,于是 `and` / `or` 是错的。** 上游判 + `l_isfalse(rb) == k`,这里判成了 `toBoolean() == (k != 0)`——这两者在 `k` + 的两个取值上恰好相反。它静默:`TESTSET` 只在「结果寄存器 ≠ B」时才 + 保留下来(`patchTestReg` 否则把它改写成 `OP_TEST`),于是 + `if x then` 一直是对的,`x and y` 一直是错的。症状是 + `print(t and "yes")` 打印出那张表;更糟的是 `cond and a or b`——Lua 里最常用的 + 惯用法——整体取错分支。**字节码闸门看不见它**:两边的字节码逐字段相同。 + `RuntimeTest.logicalOperatorsReturnValues` 现在钉住的是返回的哪个值, + 不只是真假。 +* **`xpcall` 的处理函数跑在调用链已经恢复之后。** 见上面第四条: + `xpcall(f, debug.traceback)` 的回溯只有两层,应该是七层。 + +#### 一处由参考实现裁出来、但属于「驱动方式」而不属于引擎的差异 + +`lua.c` 是在 `pmain`——一个 C 函数——里面跑脚本的,所以主 chunk 底下多一个 +C 帧。它表现为每次回溯末尾那行 `[C]: in ?`,以及 +`debug.getinfo(3).what == "C"`。宿主直接 `luaL_dostring` 时没有这一帧,两者都 +看不到。所以这是修在差分驱动上的(`Diff` 现在照 `lua.c` 搭:`pmain` + `docall` ++ `msghandler`),不是修在引擎上——为了让引擎凑上它,反而会让 `luaL_dostring` +变成错的。 + +### 阶段 10 —— 把 `lua-5.4.x/tests` 端到端跑绿 + +第四道闸门里最重的一块:`UpstreamSuiteTest` 原样跑上游那套 `.lua`,输出与参考 +二进制逐字节比;`UpstreamSmokeTest` 收那些「不该逐字节比、但必须跑完」的。 + +分到后者的四个文件,理由各自不同,都写在测试类的注释里: + +| 文件 | 为什么不能逐字节比 | +|---|---| +| `cstack.lua` | 每个用例打印的 `final count` 是「一层嵌套吃掉多少 C 栈」,Java 与 C 必然不同 | +| `gc.lua` | 整篇在测增量收集器的 `count` / `step` / `setpause`,本实现没有这一层 | +| `literals.lua` | 末尾要 `os.setlocale("pt_BR")` 真的生效,JVM 设不上 | +| `locals.lua` | `tracegc` 用 `io.stderr:write'.'` 在终结器里写进度点,终结时机由 JVM 定 | +| `constructs.lua` | `_ENV.GLOB1 = math.random(0, 1)`(第 319 行),两个进程的种子不可能一样 | +| `sort.lua` | 打印排序耗时与比较次数,而待排数据是 `math.random` 生成的 | +| `math.lua` | 打印 `random seeds`、随机数的取值范围与耗时 | + +#### 本阶段由参考实现裁定的事 + +* **出错之后 `__close` 里还能 yield。** `lua_pcallk` 在 `yieldable(L)` 时 + 不建本地恢复点:错误一路冒到 `lua_resume`,由 `precover` → + `finishpcallk` → `luaF_close(..., yy = 1)` 收尾,所以每个 `__close` 都能 + yield,而 `__close` 自己抛的新错误要顶掉旧的、接着传给下一个还没关的 + 变量。上游 `locals.lua` 那段「yielding inside closing metamethods」里,三个 + `` 变量依次 yield,中间那个 yield 完再 `error(err + 20)`,于是第三个 + 看到的错误对象和 `pcall` 最终拿到的是 30 而不是 10。 + 本实现用 Java 异常代替长跳转,恢复点落在 `pcall` 的 `catch` 里,因此 + `yy` 得显式传下去:`pcall` 按 `yieldable()` 决定,正好对上 + `k != NULL && yieldable(L)`。 +* **yield 打断收尾之后,错误对象得有人接着保管。** C 版它一直在栈上 + (`luaD_seterrorobj` 每关一个变量就重新写一次),`finishpcallk` 从 + `L->top - 1` 取即可;本实现的错误对象住在异常里,yield 一抛就跟着 Java 栈 + 没了,只能暂存在 `LuaState.closeErrPending` 上,等下一次 `finishPcallK` 来取。 + 它必须让位给新错误——否则 `y` 的 `__close` 先 yield 再 `error(30)` 时, + `z` 看到的还是旧的 10。 +* **`luaB_error` 必须照 C 的形状写**,连那句 `lua_settop(L, 1)` 都不能省:它决定 + 「栈顶下面一格是什么」,而 `luaD_seterrorobj` 的 `default` 分支读的就是那格。 + 少了它,`coroutine.wrap(...)` 抛出的错误会丢掉 `file:line:` 前缀。 +* **`luaL_where` 与 `luaG_addinfo` 对「没有调试信息」给的是相反的答案**。前者要 + `currentline > 0` 否则返回空串,后者照样打 `?:-1:`。合成一个会同时弄坏 + `error("x")` 与 `errors.lua` 里那条 `^%?:%-1:` 断言。 +* **`collectgarbage` 要先校验选项**,且 `"generational"` / `"incremental"` + 返回的是切换前的模式名(`lbaselib.c: pushmode`),不是整数。 +* **`luaD_shrinkstack` 的 `stackinuse` 取 `top` 与所有 `ci->top` 的最大值**,不是 + 只看 `top`;`stackErr` 也只在真的缩了栈时才清。 +* **主线程要留一格起手**(`stack_init` 给 `base_ci.func` 预留),`IoLib` 建标准 + 文件后要 `lua_pop`,`Diff` 要照 `lua.c: docall` 把 `msghandler` 压栈—— + 三处各差一格时,`errors.lua` 的「expected stack overflow after N calls」 + 就会偏一。 + +#### 把 `sort.lua` / `math.lua` 跑完时又挖出来的四个真缺陷 + +这两个文件原来一进来就崩,修完才跑得到 `OK`。四个都不是「不一致」,是本实现 +写错了: + +* **`tunpack` 的越界判断要按无符号比。** C 版 `n` 是 `lua_Unsigned`, + `e - i` 溢出时回绕成一个巨大的数;Java 的 `long` 回绕成负数,照有符号 + 比就漏过去,然后循环从 `minI` 一路 push 到 `maxI`,把 `table.unpack` 撑成 + `OutOfMemoryError`。 +* **`checkStack` / `checkStackOk` 里的 `top + n` 会整型溢出。** `n` 一下子来个 + `2^31` 时和变成负数,「超过 `MAXSTACK`」的判断反而失效——同样是一路 + `OutOfMemoryError`。改成 `long` 算。 +* **`tmove` 的判据不能化简。** C 版是 `e < LUA_MAXINTEGER + f`,不是 + `e - f < LUA_MAXINTEGER`:两者在无溢出时等价,但这里判的就是溢出。 + 化简后 `table.move({}, minI, maxI, 1)` 报的是 `#4 (destination wrap around)`, + 参考实现报 `#3 (too many elements to move)`。 +* **`luaG_tointerror` 要带 `varinfo` 后缀。** `math.huge << 1` 参考实现给的是 + `number (field 'huge') has no integer representation`。 +* **`tonumber` 不能用 `String.trim()`。** Java 的 `trim` 去掉的是 + 「小于等于 U+0020 的字符」,连 `\0` 一起去掉;C 版只认 + `SPACECHARS`(`" \f\n\r\t\v"`),并且要求解析指针正好走到 `s + l`。于是 + `tonumber('1\0')` 与 `tonumber('1\0', 2)` 双双被算成 `1`。 + `b_str2int` 现在整个改在字节上做,并回到长度上判「吃完了没有」。 + +#### 还有一个:`table.sort` 选枢轴要用无符号算术 + +`sort.lua` 在冒烟组里(它打印耗时,输出天生不可逐字节比),后来一次全量跑挂在 +`table.sort` 抛出的 `attempt to compare nil with number`。这个文件不播种, +每次 `math.random()` 给的都是新数据,所以这是个潜伏缺陷:只有当某次分区 +严重失衡、`auxsort` 去换新的随机种子时才会露出来。 + +* **`IdxT` 是 `unsigned int`,枢轴种子也是。** C 的 `rnd % (r4 * 2)` 是无符号 + 取模,恒在 `[0, r4*2)`;Java 的 `int` 有符号,而种子恰好就是 C 会生成的那 + 32 个比特——一半概率为负。有符号取模会把 `p` 拖到 `lo` 以下,极端时拖到 `1` + 以下:`a[p]` 是 nil,`sort_comp` 就报 + `attempt to compare nil with number`。现在用 `Integer.remainderUnsigned`。 +* **`rnd` 是按值传的。** C 里它是 `auxsort` 的形参,递归调用里重新随机不会漏回 + 外层帧。早先这里用 `int[]` 共享,于是在某个子树深处抽到的种子会一路影响整个 + 排序剩下的所有分区。 + +「二十次跑挂一次」是很糟的找 bug 方式,所以 `choosePivot` 现在有一条自己的 +确定性用例(`TableLibTest.choosePivotStaysInsideTheInterval`),直接验 C 版那条 +`lua_assert`——`lo + r4 <= p <= up - r4`——而不再指望五万元素的排序碰巧踩中。 + +#### 阶段 11:预编译 chunk 要从每一道门进得来 + +`ldo.c: f_parser` 先读一个字节:是 `LUA_SIGNATURE` 的首字节就走 `luaU_undump`, +否则才进 `luaY_parser`。就这一步,调用方完全不必声明自己给的是源码还是 +字节码——`load` / `loadfile` / `dofile` / `require` 因此全都天然支持预编译 chunk。 + +这个分派原先只存在于 `BaseLib` 的 `load` / `loadfile` 里,`lua_load` 本身 +(`LuaState.load`)不看签名,于是 **`require` 一个预编译模块会失败**: + +``` +error loading module 'mod' from file '/tmp/mod.lua': + /tmp/mod.lua:1: unexpected symbol near '<\27>' +``` + +参考实现能加载并正常返回模块。`PackageLib` 走的是 `LuaState.loadFile`,所以这个 +缺陷出现在 `require` 而不是 `load`。修法落在 `lua_load` 那一层,不是给 `require` +打补丁: + +* `LuaState.load` / `loadFile` 现在按 `Lua.LUA_SIGNATURE0` 分派; +* `loadFile` 仍然先过 `lauxlib` 那段前奏(BOM、`#!`)——预编译 chunk 的首字节 + 不会是 `#`,所以顺序不能反; +* 二进制分支不回退首字节:`luaU_undump` 的注释是 + 「skip 1st char (already read and checked)」,`checkHeader` 从第二个字节比。 + +`BinaryChunkTest` 两个方向都验: + +* **真实 `luac` 编出来的 chunk**(`ReferenceLua.compile`,按 `LUAC54_BIN` 或 + 参考 `lua` 同目录找),两边跑同一份产物逐字节比。自己编给自己判卷等于没判, + 所以产物必须出自参考实现的编译器。语料覆盖 upvalue、变长参数、`goto`、 + 混合表构造、整数与浮点边界、协程、300 项的常量表,另有两条是故意报错 + 的——错误消息里带着行号与局部变量名,undump 丢了调试信息就会红; +* **本实现自己 `string.dump` 出来的 chunk**(`require`、`dofile`、`loadfile`、 + `mode` 参数、`binary string` 源名规则,以及被截断与被篡改的 chunk——那些必须 + 是加载期错误而不是崩)。 + +#### 附:fuzz 语料有 89% 是死的 + +生成器一直在产出**编译不过**的程序: + +* 表构造写成 `{'key0' = v}`。只有**名字**能裸着当键,字符串键必须写成 + `[k] = v`——数字键分支加了方括号,字符串键分支没有; +* `expression` 无条件生成 `(select('#', ...))`,而函数只有 40% 的概率声明 + `...`。非变长函数里写它就是 + `cannot use '...' outside a vararg function`。 + +拿参考的 `luac` 量了一下:**200 颗种子里 178 个编不过**。编不过的程序在两边是 +**同一条编译错误**,比对照样「一致」——这道最硬的闸门在几万颗种子上静默地什么 +也没测。修完(键一律加方括号;生成器跟踪「当前函数在生成的是不是变长」)复测: +0 / 400。 + +`FuzzTest.assertCompiles` 让这个状态不可能再出现:参考实现的输出只要是编译期 +错误的形状,那颗种子直接判失败。判据是「消息以 `near 'tok'` 收尾」而不是 +「含 `expected`」——运行期错误里到处是 `expected` +(`bad argument #1 to 'upper' (string expected, got nil)`),而那些是**期望之中** +的。 + +#### 顺带把字节码那条路也 fuzz 上 + +常规 fuzz 走的一直是文本路径,`Dump` / `Undump` 只被几十条手写用例碰过—— +「require 加载不了预编译 chunk」恰好就在这个盲区里。 +`generatedChunksSurviveDumpAndUndump` 补上:把生成的程序体包进变长函数, +`string.dump` 出来再 `load` 回来跑。于是几千个**随机的** `Proto` 被推过序列化: +常量表顺序与变长编码、行号表、局部变量与 upvalue 的调试名、`maxstacksize`、 +嵌套 proto。另有一条哨兵输出(`dumped true loaded true`)被断言, +`string.dump` 静默失败没法再伪装成一致。 + +#### 参考实现又裁定的五件事 + +fuzz 生成器学会了 `table.*`,立刻回本:五处不一致,没有一处是手写用例撞得到的。 + +* **`l_str2int` 的累加器是 `lua_Unsigned`。** `lobject.c` 里那个 `a` 是无符号的, + 溢出判据是 `a >= MAXBY10`。用有符号 `long`,`a` 一旦过 2^63 就变负,判据从此 + 不再触发,溢出就漏了:`1 + (math.mininteger .. 0)` 的操作数是 + `"-92233720368547758080"`,绕回成精确的 0,于是返回 `1`,而正确行为是落到 + 浮点路径给 `-9.2233720368548e+19`。改成 `Long.compareUnsigned`。 +* **`luaG_runerror` 是当场加 `file:line:` 的。** `idiv` / `mod` 的除零走 + `luaG_runerror`,它在抛出点就调 `luaG_addinfo`。我们抛的是裸 `LuaError`, + 前缀要等到受保护边界才补上,那时抛出的栈帧已经没了——所以 + `coroutine.resume` 把错误当值返回时它是没有位置的: + `false attempt to divide by zero`,参考实现给的是 + `false file:line: attempt to divide by zero`。为此 `Arith.rawArith` 现在收一个 + `LuaState`。 +* **`luaH_newkey` 拒绝 nil 值。** 它的第一行是 + `if (ttisnil(value)) return; /* do not insert nil values */`。给一个不存在的键 + 赋 nil,不建节点、也不触发 rehash。而建了节点就会改变算出来的数组部分 + 大小,那是 `#t` 的直接输入:`table.move({[1]=4.0}, 0, 2, 1)` 之后参考实现 + 的 `#t` 是 `2`,建了节点就成了 `0`。nil 键与 NaN 键的检查在那一行之前, + 所以 `t[nil] = nil` 仍然要报 `table index is nil`。 +* **`lauxlib.c: interror` 判的是 `lua_isnumber`,不是类型。** + `luaL_checkinteger` 取不到整数时,若 `lua_isnumber(L, arg)` 成立(能解析成 + 数字的字符串也算),报 `number has no integer representation`,否则才是 + `number expected, got string`。于是 `string.format("%d", "3.5")` 是前者 + (`"3.5"` 解析得成 3.5,只是 3.5 不是整数), + `string.format("%d", "abc")` 才是后者。原来的判据写成 `v.isNumber()`, + 两条都成了后者。 +* **`%.14g` 只能舍一次,而且要从真值舍。** Java 的 + `String.format("%.Nf")` 舍的不是真值,而是 `Double.toString` 的最短表示 + ——等于连舍两次。`math.sqrt(math.maxinteger)` 的真值是 + `3037000499.976049900054931640625`,最短表示是 `3037000499.97605`,第 15 位 + 是 5,第二次舍入就进位成 `3037000499.9761`;参考实现给 + `3037000499.976`。`StringLib` 本来就是用 `BigDecimal` 的 + (`new BigDecimal(double)` 是精确的二进制展开),`luaO_tostring` 却另写了一份 + 基于 `String.format` 的。同一个 C 转换有两份实现,正是它们分家的原因, + 现在 `Vm.formatG` 直接转调 `StringLib.generalDigits`。 + +最后一条值得单独说清楚:拿 4602 个浮点实测,旧实现错了 **12** 个(0.26%)。 +手写用例永远撞不到,上游套件全绿也撞不到——是 fuzzer 撞到的。 +`RuntimeTest.floatFormattingMatchesReferenceAcrossMagnitudes` 现在用脚本内部 +的 LCG 生成几千个值钉住它,随机数发生器本身不进比较范围。 + +#### 把语料拓宽之后又咬出两处 + +生成器接着学会了 `utf8.*`、`gmatch`、`next`、`string.pack`/`unpack`、 +`table.pack`、非字符串错误值、`math.ult`/`fmod`/`max`/`min`、 +`rawequal`/`rawlen`/`select`。信它之前先量了一遍:每条新分支在 6%–29% 的程序里 +出现,语料确实是活的。立刻又咬出两处,都在 `ltable.c` —— 前五处里也有三处在 +这个文件。 + +* **`luaH_getn` 是先答 `limit-1`,根本不二分。** 手册只要求 `#t` 返回某个 + 边界,这让「我们找到了一个合法边界」听起来就够了。不够:真实程序看得见 + 具体是哪一个。`ltable.c` 为最常见的一串操作(`t[#t] = nil` 之后马上再取长度) + 专门留了快路径: + + ```c + if (limit > 0 && isempty(&t->array[limit - 1])) { + if (limit >= 2 && !isempty(&t->array[limit - 2])) + return limit - 1; /* (1a) 不去二分 */ + else { /* (1b) */ ... binsearch ... } + } + ``` + + 数组 `[1, nil, 3, nil]`:二分给 `1`(它找的是第一个边界),参考实现给 + `3`。`table.pack(1, nil, 3, nil)` 之后 `#t` 是 `3` 不是 `1`。系统性地铺开各种 + 空洞(单个空洞到 `n = 32`、空洞对到 `n = 19`、三空洞到 `n = 11`、 + `table.pack` 的全部 32 种 nil 组合)—— **4968 条结果里错 212 条(4.3%)**。 + +* **`luaH_resize` 必须允许哈希部分缩成 dummy。** `rehash` 之后 C 的 `reinsert` + 用 `luaH_set` 搬旧数据,整数键只要 `<= asize` 就被路由进新数组部分,不占 + 哈希槽;所以 `luaH_resize(L, t, 4, 0)` 结束时哈希部分是 dummy(0 槽)。 + 这里早先按「所有搬过去的条目」撑哈希,于是数组 4 配着 2 个用不上的哈希槽。 + 要命的是 `luaH_newkey` 的判据里有 `isdummy(t)`:哈希不是 dummy 时,下一次插入 + 直接占空槽、不再 rehash,数组部分就再也长不大。 + `t = {}; t[2]=2; t[3]=3; t[4]=4; t[6]=6; t[7]=7`,参考实现数组部分 8、 + `#t` 是 7,这里是 4 和 4。上面那份空洞扫描正是把它挖出来的 —— 旧语料只会按 + `1..n` 顺序插入,永远走不到这条路。 + +两处都由 `RuntimeTest.tableLengthBoundaryMatchesReference` 钉住:空洞图案在脚本 +内部用循环铺开(用 LCG 会把随机数发生器本身也拉进比较范围),每张表把 +`#t` 读两次,因为 C 会在第一次调用时顺手把 `alimit` 改成一个提示。 + +#### 把语料搬进协程里跑,又咬出一处 + +同一批语料后来也塞进 `coroutine.create` 里跑了一遍,咬出一个跟协程其实没关系的 +bug: + +> 协程里的 `pcall(math.random, 5, 1)` 报 +> `bad argument #1 to '?' (interval is empty)`,参考实现报 +> `bad argument #1 to 'math.random' (interval is empty)`。 + +* **`_LOADED` 是 `global_State` 级的,不是每线程一份。** `lauxlib.c` 的 + `pushglobalfuncname` 会拿当前函数去 `package.loaded` 里反查两层,把 `'?'` + 换成真名: + + ```c + static void pushglobalfuncname (lua_State *L, lua_Debug *ar) { + ... + lua_getfield(L, LUA_REGISTRYINDEX, LUA_LOADED_TABLE); + if (findfield(L, L.top - 1, 2)) { ... } + ``` + + `LUA_LOADED_TABLE` 是 registry 的字段,而 registry 属于 `global_State` + —— 所有 `lua_State` 共用同一个。这里早先把 `loaded` 做成了每个 `LuaState` + 一张,于是协程里的 `package.loaded` 是空的,协程内的每条诊断都退化成 `'?'`。 + 修法只有一行:`loaded = main.loaded`。注意它只在协程里现形——主线程 + 自己会填自己的那张表,所以整个上游套件和此前所有 fuzz 种子对此都一声不吭。 + +由 `RuntimeTest.libraryFunctionNamesResolveInsideCoroutines` 钉住。 + +#### 闸门本身有个洞 + +对 `string.format('%q', ...)` 的系统性扫描(0–255 每个字节、四种位置)是第一个 +往 stdout 里写**非 UTF-8 字节**的探针。引擎本身没查出问题,倒是把探针自己查了 +出来:两边都把捕获到的输出按 UTF-8 解码成 Java `String`。任何非法字节都会变成 +`U+FFFD`,于是 `0x80` 与 `0xEF 0xBF 0xBD` 判成相等: + +``` +>>> b'"\x80"\n'.decode('utf-8', 'replace') == b'"\xef\xbf\xbd"\n'.decode('utf-8', 'replace') +True +>>> b'"\x80"\n'.decode('latin-1') == b'"\xef\xbf\xbd"\n'.decode('latin-1') +False +``` + +不是推理,是量的:把 `print` 故意改成经 Java `String` 往返一趟(于是每个非 +UTF-8 字节都被写坏),新的 `%q` 扫描在 Latin-1 下红、在 UTF-8 下绿 +—— 旧闸门放行了真实的字节级损坏。闸门现在按 Latin-1 解码(字节↔字符一一对应), +比对才算真的逐字节。钉子: +`RuntimeTest.quoteFormatMatchesReferenceForEveryByte`。 + +#### 把运算符和类型铺成矩阵,又咬出一处 + +下一个探针是全矩阵:每个运算符 × 每对类型,无元方法,比报错文本,1651 行。 +第二个探针同样铺一遍但带上元方法 —— 两个 handler 谁先走、`__le` 回退成 +`not (b < a)`、`__index`/`__newindex` 的表与函数两种形态、`__concat` 的右结合、 +`__close` 的时机与错误传递,245 行。第二个干净,第一个咬出这一处: + +* **`luaG_tointerror` 用的是 `F2Ieq`,不是「取整到整数」。** + `lvm.h` 里 `LUA_FLOORN2I` 默认就定义成 `F2Ieq` —— 名字叫 floor,语义是 + 「只接受整数值」: + + ```c + void luaG_tointerror (lua_State *L, const TValue *p1, const TValue *p2) { + lua_Integer temp; + if (!luaV_tointegerns(p1, &temp, LUA_FLOORN2I)) + p2 = p1; /* 左边转不成整数就报左边 */ + luaG_runerror(L, "number%s has no integer representation", varinfo(L, p2)); + } + ``` + + 按「取整」的模式判,`1.5` 会被当成能转,于是 `1.5 & 3` 去报右边的 + `3`:`number (local 'b') has no integer representation`;参考实现报左边 + `(local 'a')`。矩阵里错 10 行。手写的用例只用 inf/nan 这两个极端值,两种模式 + 在它们上面结论一样 —— 所以此前每道闸门都放它过去了。 + +两个矩阵都固化成了用例:`RuntimeTest.operatorTypeErrorsMatchReference` 与 +`RuntimeTest.metamethodDispatchMatchesReference`。 + +#### 把格式串铺成矩阵,又咬出两处 + +`lstrlib.c: str_format` 是源码里分支最密的函数之一,所以下一个探针就照着它的每个 +分支铺:22 个动词 × 10 组修饰符 × 5 种宽度 × 9 种精度 × 22 种值类型 —— +**20777 行**。咬出两处: + +* **两道校验的先后。** `str_format` 并不先校验格式串。`d i u o x X` 是 + 先 `luaL_checkinteger`,`f e E g G` 是先 `luaL_checknumber`: + + ```c + case 'd': case 'i': flags = L_FMTFLAGSI; goto intcase; + ... + intcase: { + lua_Integer n = luaL_checkinteger(L, arg); + checkformat(L, form, flags, 1); + ``` + + (`c`、`a`、`s` 才是先校验格式串。)这里一律先校验格式串,于是 + `("%.100d"):format("x")` 报 `invalid conversion specification`,参考实现报 + `number expected, got string` —— **错 6620 行**。 + +* **整数动词给了精度时,`0` 标志失效。** C99:「For `d`, `i`, `o`, `u`, `x`, and + `X` conversions, if a precision is specified, the `0` flag is ignored」。 + 所以 `("%05.3d"):format(42)` 是 ` 042` —— 精度先补成 `042`,宽度再用空格 + 补,不是 `00042`。错 220 行。浮点动词没有这条规则(`%08.3f` 照旧补零), + 所以改动只落在 `emitInt` 里。 + +两处都由 `RuntimeTest.formatSpecifierMatrixMatchesReference` 钉住:单独回退任何 +一处,该用例就变红。 + +#### `luaL_checkinteger` 的第二份实现 + +下一片扫描是 `os.date` 的每个说明符、`os.time` 的参数校验、`utf8.*` 的边界、 +`tonumber` 的 2–36 全进制,854 行。咬出一个老形状的 bug: +**`luaL_checkinteger` 有两份实现**,`BaseLib` 里那份少两样东西 —— 函数名前缀 +(`bad argument #2 (...)` 而不是 `bad argument #2 to 'tonumber' (...)`), +以及「无整数表示」那一档(`1.5` 报成了 `number expected, got number`)。 +四个调用点全受影响:`tonumber` 的进制、`select` 的下标、`error` 的层号、 +`ipairs` 迭代器的下标。现在统一转调 `LuaAux.checkInteger`。 + +同一批扫描还翻出第二个「不是我们错、是 libc 不一致」的地方: +**`os.date('!%z', t)`**。C 的 `strftime` 读的是进程全局时区,不是 Lua 传给 +它的那个 `tm`;带 `!`(UTC)时两个 libc 答案相反 —— macOS 给本地偏移 `+0800`, +glibc 给 `+0000`。所以用例只测不带 `!` 的 `%z` / `%Z`:两边同一台机器, +结果是确定的。 + +`string.pack` / `unpack` / `packsize` 也整套扫了一遍 —— 全部格式动词(含 +`i`/`I` 的 1–16 字节宽度、`c`/`s` 的计数、`X` 与 `!n` 对齐、四种字节序)× +各类型的值,外加非法格式串与偏移越界,706 行。这批是干净的,也收成了用例: +把「已经一致」这件事钉住,否则下次动 pack 那片分支时没有一条用例会说话。 + +#### 有一处不是分歧:次正规数的 `%a` + +`quotefloat` 走的是 C 的 `%a`: + +```c +int nb = lua_number2strx(L, buff, MAX_ITEM, "%" LUA_NUMBER_FRMLEN "a", n); +``` + +C99 只规定小数点左边恰好一位十六进制数字,并且「这个数字只在数次正规时为 +0」—— 次正规具体怎么写是 unspecified。glibc 与 Apple libc 选的正好相反: + +| 值 | glibc(CI 的 ubuntu) | Apple libc / musl | +| --- | --- | --- | +| `5e-324` | `0x0.0000000000001p-1022` | `0x1p-1074` | +| `3 * 5e-324` | `0x0.0000000000003p-1022` | `0x1.8p-1073` | + +这里取 **glibc**:真正作数的闸门(CI)跑在 `ubuntu-latest` 上。 +`StringLibTest.subnormalQuoteFollowsGlibc` 用普通断言在差分表之外钉住它, +免得有人照 macOS 上的参考实现「修」一把反而把 Linux 上本来对的行为改错。 +(`Double.toHexString` 给出的正是 glibc 那种写法,所以不需额外实现。) + +#### 第二处不是分歧:`%a` 带精度的舍入,与 `inf`/`nan` 的零填充 + +Apple libc 在「`%a` 给了精度、且舍弃的第一位十六进制数字恰为 8」时是错的 +——它根本不是就近舍入,判据成了「第二位舍弃数字的奇偶」: + +```text +("%.4a"):format(0x1.500082p0) -> 0x1.5000p+0 # 0x82 > 0x80,却舍了下去 +("%.4a"):format(0x1.500081p0) -> 0x1.5001p+0 # 这个反倒进上来 +``` + +glibc 的判据照抄自 `stdio-common/printf_fphex.c` 与 `include/rounding-mode.h`: + +```c +bool more_bits = ((next_digit_value & 7) != 0 || precision + 1 < numend - numstr); +do_round_away = round_away (negative, last_digit_value & 1, + next_digit_value >= 8, more_bits, mode); +/* FE_TONEAREST: half_bit && (last_digit_odd || more_bits) */ +``` + +即就近舍入、平局取偶;并且 `precision == 0` 时「保留的最后一位」是小数点 +左边那一位,所以 `("%.0a"):format(1.5)` 是 `0x2p+0` 而不是 `0x1p+0`。 +这批由 `StringLibTest.hexFloatPrecisionRoundingFollowsGlibc` 用普通断言钉住; +差分网格里跳过所有「舍弃的第一位为 8」的用例(两边跳过的完全一致——判据是用 +不舍入的 `%a` 算的,那个两边一致)。 + +同一个文件里还藏着一条:**`inf` / `nan` 不参与零填充**。`special` 分支无条件走 +`pad(buf, ' ', width)`,压根不看 `pad == '0'`,所以 +`("%020.5a"):format(math.huge)` 是 17 个空格加 `inf`。 + +#### 把 `table` 库铺开扫:咬出 `tmove` 少了一个条件 + +`table` 矩阵 —— `insert`/`remove`/`move`/`concat`/`unpack`/`pack`/`sort` 的 +位置 × 表大小 × 偏移 × 值类型,含同表重叠 `move` 与跨表 `move`,**6277 行** —— +跑出 268 行分歧,而且每一行都是同一个让人心里发毛的形状:**表内容完全一样, +只有 `#t` 不一样**。 + +```text +< MV2-dump #0 -2=nil,-1=nil,0=nil,1=nil,2=1,3=nil,4=nil,... +> MV2-dump #2 -2=nil,-1=nil,0=nil,1=nil,2=1,3=nil,4=nil,... +``` + +根因是漏了一个条件。C 判拷贝方向是这样写的: + +```c +if (t > e || t <= f || (tt != 1 && !lua_compare(L, 1, tt, LUA_OPEQ))) { + for (i = 0; i < n; i++) { ... } /* 正向 */ +} else { + for (i = n - 1; i >= 0; i--) { ... } /* 逆向 */ +} +``` + +这份实现只写了前两项。第三项跟位置毫无关系:**源和目标是两张不同的表时 +一律正向**,与 `t`、`f` 谁大谁小无关。漏掉它,本该正向的情形会走成逆向。 + +次序不改变表里最终有什么,但改变布局,而 `#t` 读的就是布局。 +`table.move({1}, -2, 1, -1, {1})` 之后目标表同样只剩 `[2]=1`:正向先把 `[1]` +挖空,`alimit` 仍是 1,于是 `luaH_getn` 的第一条分支(「`array[alimit-1]` 为 +nil 就只在数组部分二分」)成立,答案是 `0`;逆向却先塞 `[2]`,把 `alimit` 顶到 +2,那条分支不再成立,答案变成 `2`。于是就有了那个绕人的现象:同样的写入序列, +走 `rawset` 一致、走 `table.move` 不一致——差的只是内部布局。 + +两条经验落进了测试: + +* **光比表内容不够,必须连 `#t` 一起比**,否则布局类的错永远看不见。 +* 这一片现在是三个常驻测试: + `RuntimeTest.tableLibraryMatrixMatchesReference`(6277 行,就是它咬出来的)、 + `RuntimeTest.patternMatchingMatrixMatchesReference`(模式匹配 × 主题 × + `init` × 替换形态,24819 行,**干净**,照样钉住,照第 25 条经验:已经一致的 + 也得有人替它说话)、 + `RuntimeTest.tableLengthAcrossInsertionHistoriesMatchesReference` + (3736 行,把插入历史本身当成扫描维度:`1..n` 的全部子集 × 三种插入 + 次序、先填满再挖空、`move` 造出的布局、`insert`/`remove` 交替、负键与 0 键、 + 二分上到 40、尾部截断——也干净)。 + +把那一个条件回退,第一个测试立刻变红。 + +#### `debug` / `load` / `require` 矩阵:咬出 `xpcall` 少了参数校验 + +下一片:`debug.getinfo` 的 14 个字段 × 0–6 层、`getlocal`/`setlocal`、 +upvalue 全家、`traceback`(含协程内)、`sethook` 的 10 种掩码加计数钩子、 +`load` 的源码/读取函数/模式/env/二进制块、`package.path`/`cpath`/`config`/ +`searchers` 与 `require` 的几种怪角、`error` 的 0–4 层、`xpcall`、`assert`, +以及错误在协程里的传播,289 行。咬出一处:**`xpcall` 把参数校验丢了**。 + +```c +static int luaB_xpcall (lua_State *L) { + ... + luaL_checktype(L, 2, LUA_TFUNCTION); /* check error function */ +``` + +C 在调用任何东西之前就先校验消息处理函数必须是函数。这份实现只判了 +「参数够不够两个」,于是 `xpcall(f, 5)` 不报错,照常调 `f`,`f` 抛错后拿 +`5` 当处理函数: + +| | 参考实现 | 修之前的本实现 | +| --- | --- | --- | +| `xpcall(f, 5)` | `false bad argument #2 to 'xpcall' (function expected, got number)` | `true false error in error handling` | +| `xpcall()` | `false bad argument #2 to 'xpcall' (function expected, got no value)` | `false bad argument #2 to 'xpcall' (value expected)` | + +顺带把 `io` 矩阵也铺了:32 种打开模式(含全部非法模式)、`read` 的全部格式与 +0–24 字节计数、数字读法、`seek`、`lines`、关闭后再用、 +`io.input/output/read/write/close`、追加模式的 `seek` 语义、`r+` 交错、 +5000 字节大块、同一文件开两次,167 行,干净。它仍值得留着:只有它碰得到 +**errno 那一层** —— `io.open` 失败要返回 `"路径: No such file or directory"` +加错误号 `2`,C 那是 `strerror` 给的,我们得自己对齐。 + +#### 闸门自己又有个洞:回溯把输出静默截断了 + +`debug` 矩阵第一版是绿的 —— 而 `xpcall` 那个 bug 还在代码里。这是闸门 +失灵,不是代码过关。 + +`Diff.assertMatches` 的归一化会把第一个 `stack traceback:` 之后的全部内容 +砍掉。对「脚本自己崩了」的情形这是有意的:消息在前、回溯在后,回溯本来 +就没法逐行比。但当回溯是脚本打印出来的(`debug.traceback()` 的返回值), +它就落在中间,后面脚本打印的东西全被丢掉 —— 这片脚本第 5 段打印了回溯, +后面还有 180 行。 + +现在加了门禁:回溯块之后只要出现「不以制表符开头的非空行」(回溯自己的每行 +都以制表符开头),就直接判失败,并提示改用不剥回溯的 `assertFullMatches`。 +既有 13 个用例的输出里带回溯,收紧后的判据(看的是回溯之后有没有内容, +而不是「有没有回溯」)把它们全放过了 —— 那些用例里回溯确实在最后。 + +这是第二次发现「闸门瞎了、代码没错」(第一次是捕获输出按 UTF-8 解码)。 +一个悄悄比得比它声称的少的闸门,比没有闸门更糟:它报绿。 + +#### fuzz 咬出的一处:错的是驱动,不是引擎 + +放到 16500 个程序上跑,fuzzer 产出一个程序,栈溢出时参考实现报第 67 行、 +我们报第 65 行。引擎没错,是驱动少占了一格栈: + +```c + lua_pushcfunction(L, &pmain); /* to call 'pmain' in protected mode */ + lua_pushinteger(L, argc); /* 1st argument */ + lua_pushlightuserdata(L, argv); /* 2nd argument */ + status = lua_pcall(L, 2, 1, 0); /* do the call */ +``` + +`lua.c` 的 `main` 给 `pmain` 传两个实参,栈的初始 `top` 是 3; +`Diff.runLikeLuaC` 只传了一个。少一格会一路带到底:递归多走一层, +最深那一帧就换了个函数,报错行号也就跟着换。 + +难查在于它不是每次都差:每帧开销是 `k+1`(`k` 为局部变量个数), +深度是 `floor((limit - base) / (k+1))`,基址差 1 只在刚好跨过整除边界时 +才改变结果。按帧大小量下来: + +| 局部变量 `k` | 0 | 1 | 2 | 3 | 4 | 6 | 8 | 16 | 32 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 修之前是否差一层 | 是 | 是 | 是 | 否 | 否 | 是 | 否 | 否 | 否 | + +只挑一个例子试会以为没问题。补上那一格之后九个全对,现在由 +`RuntimeTest.stackOverflowDepthAndLineMatchReference` 铺开盯着。 + +(那片探针还有个陷阱:`local function f() return f() end` 是尾调用, +不占栈,会变成死循环而不是溢出 —— 参考实现实测挂满 5 分钟才被杀。 +所以探针里一律写成 `return 1 + f()` 这种非尾形式。) + +#### 三片扫下来是干净的 + +不是每片都能咬出东西。`math` 全函数 × 30 个边界值(含 ±inf、nan、-0.0、 +`maxinteger`、`mininteger`、`2^53`、`2^63`、`1/3`)、`modf` 的两个返回值、 +`log` 的底、每个函数六类参数错误、播种后的随机序列,加上 `os` 里输出确定的 +那部分,共 **1423 行**,干净。照样留在套件里,理由三条: + +* **浮点的最后一比特**:`math.sin` 这些走的是 `libm`,`Math.sin` 与之未必 + 逐比特一致。默认 `tostring` 只打 14 位有效数字,正好把它盖住 —— 这片证的 + 是「确实盖得住」,而不是「从来没看过」。 +* **`math.random` 的序列**:xoshiro256** 把它定死了,播种后必须逐个对上。 +* **`errno` / `strerror` 那一层**,在 `os.remove` / `os.rename` 上再碰一次。 + +其中有三处根本没法测:`os.clock`(耗时)、无参 `math.randomseed` +(拿时钟播种)、无参 `os.time`(当前时间)。 + +#### `string` 库矩阵:咬出两条,外加一处参考实现自己都对不上的 + +`string` 库非模式匹配的那一半 —— `sub` / `byte` / `char` / `rep` / +`upper` / `lower` / `len` / `reverse` / `format` / `tonumber(base)` / +`tostring`(模式匹配的 pattern 语义另有 24819 行那片单独覆盖)—— +铺开 **3921 行**,咬出两条: + +**其一,`__name` 走的是 C 字符串语义。** + +```c + int tt = luaL_getmetafield(L, idx, "__name"); + const char *kind = (tt == LUA_TSTRING) ? lua_tostring(L, -1) : ...; + lua_pushfstring(L, "%s: %p", kind, lua_topointer(L, idx)); +``` + +`lua_tostring` 交出来的是 `const char *`,`"%s"` 又按 C 字符串处理,于是名字 +在第一个 `\0` 处被悄悄砍断:`__name = "A\0B"`,参考实现只打印 `A`,我们把整个 +`"A\0B"` 都打了出去。Lua 字符串不以 NUL 结尾、C 字符串以 NUL 结尾,这一局是 +后者赢。修法是 `LuaString.toCString()`,专供「C 版当 `const char *` 用」的那些 +地方。 + +**其二,`%p` 不补前导零。** 地址写成了 `0x%016x`,参考实现是 `0x7ab30002c0` +这种不补零的样子(macOS 与 glibc 都不补)。 + +**再有一处是两边根本不能比的**:`"+"` / 空格标志作用在 NaN 上时,macOS 的 libc +不打符号(`nan`),glibc 打(`+nan`)。CI 的 conformance job 在 ubuntu 上跑、 +本机在 macOS 上跑,于是**参考实现自己**两次跑出来就不一样。脚本里先把 NaN 的 +符号抹掉再比,实现这边保持与 glibc 一致。 + +#### 顺带发现:参考实现有一处是不确定的 + +`math` 矩阵第一次跑红了,报的是 `math.atan` 的报错名对不上。查下去才发现 +`atan` 与 `atan2` 在 `lmathlib.c` 里是同一个 `math_atan`: + +```c + {"atan", math_atan}, + ... +#if defined(LUA_COMPAT_MATHLIB) + {"atan2", math_atan}, +``` + +报错里的函数名由 `lauxlib.c: findfield` 遍历 `package.loaded` 时**先撞见谁** +决定,而那个顺序由字符串哈希决定,哈希又被 `luai_makeseed`(按时间)播种: + +``` +$ for i in 1 2 3; do lua -e 'print(pcall(math.atan))'; done +false bad argument #1 to 'math.atan2' (number expected, got no value) +false bad argument #1 to 'math.atan2' (number expected, got no value) +false bad argument #1 to 'math.atan' (number expected, got no value) +``` + +八次里六次 `atan2`、两次 `atan`。**参考实现不确定,我们确定**(哈希不按时间 +播种),这是有意保留的偏离。把 `math` / `string` / `table` / `os` / `io` / +`coroutine` / `debug` 七个库所有函数的零参报错名各跑八遍扫过, +`atan` 是唯一会变的一个;脚本里统一成 `atan` 再比。 + +#### 元方法与「参数缺失」矩阵:同一个根因咬出七处 + +元方法语义 × 变量来源推断 × 参数缺失,**2025 行**。前两片是干净的: +19 个二元操作符 × 9 类值、`-` / `#` / `~` / `not`,以及 +`global` / `local` / `upvalue` / `field` / `method` / `constant` / 变长参数 +这七种上下文里同一句报错带的不同后缀 —— 那部分本来就对。 + +咬出的全在第三片:六十来个库函数各用「不传 / `nil` / 数字 / 字符串 / 表 / +布尔」调一遍。根子就一个 —— **缺的参数读到的是残值**。 + +```java +public LuaValue arg(int i) { + return stack[ci.func + 1 + i]; // 越界?读到的是上一帧留下的东西 +} +``` + +C 里 `lua_type` 对越界下标返回 `LUA_TNONE`、取值拿到的是 `nil`;我们的栈是 +**复用的数组**,越界读出来的是上一帧留下的东西。于是: + +| | 参考实现 | 修之前 | +| --- | --- | --- | +| `pcall(setmetatable)` | `(table expected, got no value)` | `got function` / `got nil`(看那一刻槽里是什么) | +| `assert()` | `false bad argument #1 to 'assert' (value expected)` | `true`(残值为真 → 返回 0 个值) | +| `ipairs()` | `(value expected)` | `true function 0 0` | + +**同一个用例这次绿、下次红**——这类最难查。改成越界返回 `nil` 之后才谈得上 +「缺参」到底是什么语义,接着就是七处: + +1. **缺参报 `no value`,不是 `nil`**:`rawlen` / `rawequal` / `rawget` / + `rawset` / `setmetatable` / 五个 `coroutine.*`,全军覆没。C 的 + `luaL_typename` 对越界下标给 `no value`。 +2. **`rawequal` 漏了两遍 `luaL_checkany`**:`rawequal(1)` 本该报 + `bad argument #2 ... (value expected)`,我们返回 `true true`。 +3. **`pairs` / `ipairs` 不要求是表**:C 只做 `luaL_checkany`, + `pairs(5)` 照样返回三个值,真正炸是在迭代时 `next(5)` 报 + `(table expected, got number)`。我们多加了「必须是表」,把它变成报错。 +4. **`select` 的 `#` 只比第一个字节**:C 是 + `*lua_tostring(L, 1) == '#'`,所以 `select("#abc", 1)` 也是「数个数」, + 返回 1;判断整串等于 `"#"` 就漏了。另外缺参报的是 + `number expected, got no value`,不是 `value expected`。 +5. **`rawget` 没有「键不能是 nil」那条**:`rawget({}, nil)` 是 `true nil` + (nil 键落到 `ltable.c` 的 dummynode)。报「table index is nil」的是 + `rawset`,那条来自 `luaH_newkey`。 +6. **`setmetatable` 的第二个参数**走 `luaL_argexpected`,必须带 + `got <实际类型>`;而且缺参是 `TNONE`,不属于 {nil, table},所以 + `setmetatable({})` 要报错 —— 我们把它当合法放过去了。 +7. **`next` 的第二个参数不给就补一个**,不是报错(C 的 `lua_settop(L, 2)`)。 + +(顺带:`__index` 链、`__eq` / `__lt` 的回退、`__metatable` 保护、元方法自身 +报错、把带 `__call` 的表当元方法用 —— 那些一行不差。) + +#### 可选参数矩阵:两处「校验顺序写反」 + +上一片只试了「0 个 / 1 个实参」,这一片把第 2、第 3 个参数「给不给、给什么」 +也做全排列:七十来个函数,按 `{nil, 1, "x"}` 的所有组合从 0 个实参一直喂到 +各函数的形参上限,**1866 行**。咬出两条,都是同一个毛病 —— **多道校验的先后 +写反了**: + +**其一,`string.gsub`。** + +```c + int tr = lua_type(L, 3); /* 只记类型,不校验 */ + lua_Integer max_s = luaL_optinteger(L, 4, srcl + 1); + int anchor = (*p == '^'); + lua_Integer n = 0; + int changed = 0; + ... + luaL_argexpected(L, tr == LUA_TNUMBER || tr == LUA_TSTRING || + tr == LUA_TFUNCTION || tr == LUA_TTABLE, 3, + "string/function/table"); +``` + +第 4 个参数的校验在前面。于是 `gsub(s, p, nil, "x")` 报的是 +`bad argument #4 (number expected, got string)`,而我们因为先校验第 3 个参数, +报成了 `bad argument #3 (string/function/table expected, got nil)`。 + +**其二,`tonumber`。** 三道校验的顺序是 +`luaL_checkinteger(2)` → `luaL_checktype(1, LUA_TSTRING)` → +`luaL_argcheck(2 <= base <= 36, 2, "base out of range")`,**范围检查在最后**: + +| | 参考实现 | 修之前 | +| --- | --- | --- | +| `tonumber(nil, 1)` | `bad argument #1 (string expected, got nil)` | `bad argument #2 (base out of range)` | +| `tonumber(1, 1)` | `bad argument #1 (string expected, got number)` | `bad argument #2 (base out of range)` | + +三个函数不在这张表里:`os.time` / `os.date` 第一个参数为 `nil` 时读的是时钟, +`math.randomseed(nil)` 拿时钟播种,`io.lines` 无参会去读 stdin。 + +#### `package` / `require` / `load` 矩阵:两处 bug,外加一个必须共用的计数器 + +**311 行**:`package` 的静态结构、`searchpath`、`require` 的各种形态(模块返回 +`nil` / `false` / `true` / 什么都不返回、缓存语义、模块里抛错、循环依赖、 +`preload` 压过磁盘、模块名带斜杠),以及 `load` 的 `mode` / `env` / +`chunkname` / 读函数。 + +**其一,`package.loadlib` 的第三个返回值说的是「在哪一步失败」,不是「失败」。** +`ERRLIB`(库打不开)给 `"open"`,`ERRFUNC`(库里没这个符号)给 `"absent"`: + +```c + lua_pushliteral(L, (stat == ERRLIB) ? "open" : "absent"); +``` + +我们照着「没有 dlopen 的兜底分支」写死成 `"absent"` —— 可那个分支下 +`lookforfunc` 恒返回 `ERRLIB`,照样该给 `"open"`。用第三个值决定「还找不找」 +的代码会因此走错分支。 + +**其二,循环依赖报的不是裸的 `C stack overflow`。** 参考实现给的是: + +``` +error loading module '_pk_b' from file './_pk_b.lua': + C stack overflow +``` + +因为语法分析器与 `luaD_call` 共用同一个计数器: +`#define enterlevel(ls) luaE_incCstack(ls->L)`。第 200 层不是在 `lua_call` 上 +撞到的,而是在某次 `luaL_loadfile` 的解析中途撞到的 —— 错误被 +`luaD_protectedparser` 兜住,`loadfile` 返回非 `LUA_OK`,最后由 `checkload` +包上那层前缀。调用深度与表达式嵌套深度各数各的,就只能拿到裸消息。 +连模块名都钉在用例里了。 + +修它顺带咬出计数器的另一半 —— **回滚**。`luaD_call` 的自增写在 +`try` 等价物之前,`luaE_checkcstack` 抛错时 `finally` 管不到那一下; +C 靠 `luaD_rawrunprotected` 末尾的 `L->nCcalls = oldnCcalls`(在 `pcall` 边界) +把账补回来,Java 没有 longjmp 可退。不补的话每次 C 栈溢出永久漏一层: +用 `pcall` 递归探针量出来是 197 → 196 → 195 → 194 → 193,上游 `errors.lua` +于是开始报 `error in error handling`,而参考实现那里是 `OK`。解析器那头同理, +回滚点必须挂在 `lua_load` 上,挂低了(挂在二进制 / 文本分派处)一次失败的 +`load` 就漏两层。 + +**还有一处根本没法比**:`package.loadlib` 的失败**消息**来自平台 libc +(macOS 是 `dlopen(...)`,glibc 是 `cannot open shared object file...`), +本实现压根不加载动态库 —— 三边三种文本,只能比「它是不是字符串」。 + +#### `io` 库深矩阵:`fseek` 欠你一个 `EINVAL` + +`io` 原本是十个标准库里最薄的一片(167 行),这一片补上 **124 行输出**: +`open` 的十六种合法模式与九种非法模式(非法模式是**抛错**不是返 `nil`+消息, +`l_checkmode` 的判据是 `[rwa]%+?b?`)、`read` 的全部格式、`write` 写整数与 +浮点的形态差异、`seek` 三个起点与越界、`lines`、`setvbuf`、关两次、 +`io.output` / `io.input` 切换,以及含 `\0` 与非法 UTF-8 的二进制安全。 + +咬出一处:**负的目标位置是 `EINVAL`,不是笼统的 IO 错**。 + +``` +参考实现: nil Invalid argument 22 -- seek("set", -1) +修之前: nil Input/output error 5 +``` + +`fseek` 对三种起点(`SET` / `CUR` / `END`)算出来的负偏移**一律**给 `EINVAL`, +而 Java 的 `RandomAccessFile.seek` 抛的是 "Negative seek offset",掉进 +`errnoOf` 的兜底分支就成了 `EIO`(5)。往 EOF **之后**定位是合法的(返回新位置, +不报错)—— 修这条时别顺手一起拦了。 + +一处刻意不测:`print` 写的是 C 层的 `stdout`,不受 `io.output` 切换影响 —— +假设它会跟着变的脚本,两边会各自给出一个错的答案。 + +`fopen` 一个**目录**原本也写在这一节里,那是错的:它是一条完整的 POSIX 规则 +(`open(dir, O_RDONLY)` 成功、`O_WRONLY` 给 `EISDIR`、第一次读失败于 `EISDIR`), +macOS 与 glibc 一样,只是 JVM 不愿意建模。现在建模了,见下文 *目录那个 bug*。 +真正不能比的只有目录上的 `seek("end")`:给的是目录自身的大小(本机 64), +那是文件系统的事实,不是 Lua 的事实。 + +#### `os.time` 那半边:`lua_tointegerx` 是带字符串转换的 + +既有那片 854 行铺的是 `os.date` 的每个转换说明符;这一片铺它没盖住的半边: +`os.time` 的各种表形态(非闰年进位、月/日/时越界、缺省值、被忽略的多余字段)、 +`difftime`、`getenv`、`remove` / `rename`、`tmpname`、`execute` 的退出码、 +`setlocale`。 + +**日期表的字段是用 `lua_tointegerx` 读的,它会转换字符串**: + +```c + lua_Integer res = lua_tointegerx(L, -1, &isnum); /* cvt2num:字符串也走 */ +``` + +所以 `os.time{year = "2000", month = 1, day = 1}` **就是** 2000 年,而我们报 +`field 'year' is not an integer`。库函数参数走的是会做类型转换的 `luaL_check*` +那一套,VM 的算术指令则刻意不转换 —— 两条相反的规矩,很容易写成同一条。 + +顺带钉住:`month` / `day` 是**必填**(缺了报 `field 'month' missing in date +table`,`year` 有缺省、`hour` 缺省 12);字段大到越出 int 范围报 +`field 'x' is out-of-bound`;`yday` / `wday` 之类的多余字段被忽略。 + +一处刻意不测:`year = -100` 这种极端年份(`mktime` 的可表示范围随 libc 而定, +macOS 报 `time result cannot be represented in this installation`)。 + +删**目录**原本也写在这里,理由是「`ENOTEMPTY` 的数值 macOS 是 66、glibc 是 39」 +—— 但「数值随系统变」是**按系统取值**的理由,不是跳过这条用例的理由。现在比了, +`ENOTEMPTY` 按 `os.name` 取,见下文 *errno 形状表*。而且当初跳过它,正好把 +「errno 写死成 39、在哪儿都不对」这个错盖住了。 + +#### 弱表 / `__gc` / `` 矩阵:`isdecGCmodegen` + +这是整个项目**语义风险最大**的一块 —— **没有收集器**(`lgc.c` 不移植),弱表靠 +`WeakReference`、`__gc` 靠 `Cleaner` 重建,光靠 `GcTest` 那 12 个手写用例不够。 +这一片铺:弱键 / 弱值 / 双弱、仍被强引用着的键、互相引用的弱值、根本不可收集的 +值(字符串 / 数字 / 布尔)、`__mode` 的十种写法、`__gc` 一个与二十个、`__gc` +里抛错、`setmetatable` **之后**才加的 `__gc`(C 不认,只在设元表那一刻检查)、 +`` 的关闭顺序与 `err` 参数。 + +咬出一处:**`collectgarbage("generational"/"incremental")` 报的是 +`isdecGCmodegen`,不是 `gckind` 本身**: + +```c +#define isdecGCmodegen(g) (g->gckind == KGC_GEN || g->lastatomic != 0) +``` + +`lastatomic` 在**头一次真正收集之后**就非 0,而加载脚本本身就要分配 —— 用户代码 +刚跑起来时它已经不是 0,于是尽管 `gckind` 是 `KGC_INC`,看起来却是分代模式。 +参考实现连续切换五步: + +``` +1 generational 2 incremental 3 incremental 4 generational 5 incremental +``` + +只记 `gckind` 的话**第一行就假**。两种切换都会把 `lastatomic` 清 0,之后就只看 +`gckind`。 + +两处不可比:`collectgarbage("count")` 的数值(没有收集器,无从统计)与 +`collectgarbage("step")` 的返回值(参考实现的一「步」永远走不完一轮、恒 +`false`;我们没有增量收集器,一步就是一整轮、恒 `true`。**刻意**取 `true`: +取 `false` 会让 `repeat until collectgarbage("step")` 这类写法死循环)。 + +#### `debug` × 协程 交叉矩阵:`_LOADED` 得挂在注册表上 + +两边各自都铺过(114 / 99 行),没铺的是**交叉**那一层:给协程做 `getinfo` / +`getlocal` / `traceback` / `sethook`、钩子的**计数**(打印成一个数而不是一条一行 +—— 计数本身就是对行号表与调用序列的强校验,`tail call` 事件也钉在里面)、 +registry / `uservalue` / `setmetatable`,以及协程状态机的补充。 + +咬出一处:**`_LOADED` 必须是注册表的一个字段**: + +```c +#define LUA_LOADED_TABLE "_LOADED" /* lauxlib.h */ +``` + +`package.loaded` 不是「别处存一份、注册表里再放一份」,它**就是**那个注册表字段, +所以 `debug.getregistry()["_LOADED"]` 得拿得到。我们只在 `LuaState` 里存了一份, +于是 `type(debug.getregistry()["_LOADED"])` 给 `nil`,参考实现给 `table` —— 靠 +注册表做手脚的脚本会直接失效。改成从注册表里读回来:脚本把 `_LOADED` 换掉时 +我们也跟着换。 + +#### 错误矩阵(208 行):`__name` 上的两处 + +一条此前没人铺过的轴:`error(msg, level)` 的 level 从 `-1` 一路到 +`math.maxinteger`、各种类型的**错误对象**(以及 `print` / `tostring` / +`string.format("%s")` 各自怎么处置它)、`luaL_where` 的位置前缀在**每一个可能 +抛错的语法位置**上的取值(表构造、数值与泛型 `for`、十二个元方法、`gsub` 的替换 +函数、`sort` 的比较器、协程、``、方法、`__index` 链、尾调用、`load` 出来的 +chunk 配 `=` / `@` / 字符串三种名字)、`xpcall` 的形状、钩子里抛错、以及 `strip` +过的 chunk。208 行一致。 + +咬出两处,都在 `luaT_objtypename` 上: + +```c +const char *luaT_objtypename (lua_State *L, const TValue *o) { + Table *mt; + if ((ttistable(o) && (mt = hvalue(o)->metatable) != NULL) || + (ttisfulluserdata(o) && (mt = uvalue(o)->metatable) != NULL)) { + const TValue *name = luaH_getshortstr(mt, luaS_new(L, "__name")); + if (ttisstring(name)) /* is '__name' a string? */ + return getstr(tsvalue(name)); /* use it as type name */ + } + return ttypename(ttype(o)); /* else use standard type name */ +} +``` + +**`__name` 是 C 字符串**:`getstr()` 交出来的是 `const char *`,`luaG_typeerror` +用 `%s` 拼,于是在第一个 `\0` 处被砍断 —— `__name = "A\0B"` 报的是 +`attempt to perform arithmetic on a A value`,只有 `A`。我们把整个字节数组都 +交出去了。(`luaL_tolstring` 那条路上同一个坑早就修过,这是它的第二份拷贝。) + +**只有表与(完整)userdata 认 `__name`**:其余类型**连类型元表都不看**,直接走 +`ttypename`。所以 `debug.setmetatable(nil, {__name = "NilT"})` 之后 `nil + 1` +参考实现报的仍是 `... on a nil value`。我们的查找走的是 `luaT_gettmbyobj`,它会 +把**类型元表**也算进来,于是多报了 `NilT`。 + +另有四条钉住了、但并不是 bug 的: + +* **`debug.traceback` 把非字符串、非 `nil` 的消息原样返回**(`ldblib.c: + db_traceback`),所以 `xpcall(f, debug.traceback)` 碰上表错误对象返回的是那张 + **表**,不是回溯。而 `lua_tostring` **不**认 `__tostring` —— 这正是 `lua.c` 的 + `msghandler` 要显式 `luaL_callmeta(L, 1, "__tostring")` 的原因。 +* **行钩子在这一行的第一条指令之前触发**,所以 `print(pcall(f))` 与钩子写在 + 同一行是**拦不住**的;调用钩子连 `print` / `pcall` 自己都算。 +* **`xpcall` 会丢掉处理器的第二个返回值**:`ldo.c: dothecall` 是 + `luaD_callnoyield(L, L->top - 2, 1)`,结果数写死 1。 +* `coroutine.wrap` 抛出的错带**两层**位置前缀;`pcall(error, "x")` **没有**前缀 + (C 函数没有行号);库函数报错**有**,因为 `luaL_error` 的 `luaL_where(L, 1)` + 取的是调用者那一层。 + +#### 内嵌 `\0` 矩阵(88 行):C 在十三个门口各砍一刀 + +一条轴、一条规则:Lua 字符串是字节串,但 C 里凡是接 `const char *` 的 API —— +`strlen` / `%s` / `strstr` / `strchr` / `strcmp` —— 都在第一个 NUL 处停。88 行, +咬出**十三处**分歧,全是同一个 bug 在十三个不同门口: + +| 在第一个 `\0` 处砍断 | chunk 名(`luaS_new`)、模块名与 `package.loaded` 的键、`searchpath` 四个参数、`io.open` / `io.lines` / `os.remove` / `os.rename` / `os.getenv` 的文件名、`fopen` 的**模式**、`debug.getinfo` 的 `what`、`debug.sethook` 的 mask、`luaL_traceback` 的消息、`luaL_checkoption`(`collectgarbage`)、`string.pack` 的格式串、`__name` | +| --- | --- | +| **不**砍断 | `string.format("%s")`(走 `luaL_tolstring`,带长度)、`error` 的消息(`lua_concat`)、`os.date`(按 `s + slen` 扫完整个格式串)、表键、`gsub`、`upper` | +| 是**拒绝**而不是砍断 | 带修饰符的 `%s` 与 `string.pack` 的 `z`:`luaL_argcheck(L, l == strlen(s), arg, "string contains zeros")` | + +其中两处值得单独说: + +**`os.remove("/x\0y")` 直接把进程干掉了。** `Paths.get` 抛 +`InvalidPathException`,它是 `RuntimeException`、**不是 `LuaError`**,而 +`pcall` 只拦 `LuaError`,于是整颗逃逸出去。按 C 字符串语义截断之后这类根本 +不会出现了,`catch` 顺手也把 `RuntimeException` 兜住。 + +**`__name` 是它的第二份拷贝。** `luaL_typeerror` 与 `luaG_typeerror` 都走 +`luaT_objtypename`,但 `typeArgError` 自己又写了一份 +`metafieldOf(v, "__name")` —— 于是两条规则(截断、只认表与 userdata)**两条都 +错**。现在跟另外两处一样调 `LuaTm.objTypeName`。 + +两个 `debug` 上的最阴:`debug.getinfo(f, "S\0ln")` **不该**报 `invalid option` +(C 扫到 NUL 就收工,`l` 和 `n` 它根本没看见);`debug.sethook(f, "l\0c")` +只装行钩子,`"c\0l"` 只装调用钩子 —— 两者的计数正好反过来。 + +#### 内嵌 `\0` 第二轮(40 行):`load` / `loadfile` / `io.output` + +把 `src/main` 里剩下的 `.toJavaString()` 逐个过一遍,又揪出三个门口: +`load` 的**模式**(`checkmode` 比的是 `const char*`)、`loadfile` 的文件名、 +`io.output` / `io.input` 拿到的文件名。于是 `load(src, name, "t\0b")` 的模式是 +`"t"`,而 `load(src, name, "b\0t")` 会去 undump 一个文本 chunk、报 +`attempt to load a text chunk (mode is 'b')`。 + +还咬出一处 C 与 Java **判路径判得不一样**的地方:**`loadfile("")`**。C 的 +`fopen("")` 给 `ENOENT`,报 `cannot open : No such file or directory`;Java 把 +空路径解析成当前目录、失败在读,给 `Is a directory`,我们于是报成了 +`Input/output error`。空路径是唯一一处,按 C 的判据单独取 `ENOENT`。 +目录原本在同一句里被断言成「不用改、也不该改」—— 而这句是错的,值得留着当教训: +它是**读 lauxlib.c 读出来的,不是跑参考实现跑出来的**。C 里 `loadfile("/tmp")` +确实是 `fopen` 成功、失败在读,但本实现报的是 +`cannot open /tmp: Input/output error`(Java 的 `Files.readAllBytes` 在开就抛了), +也就是另一条分支。量过之后,一句错话变成五个门口的真 bug, +见下文 *目录那个 bug*。 + +#### 一片铺下来是干净的:`load` 的读取器契约(54 行) + +`lua_load` 的 `lua_Reader` 此前没人铺:一次给完 / 一个字节一个字节给 / 任意切分、 +**空串不是结束**(`Zio` 会继续问下一个,只有 `nil` 才收尾,所以 +`{"", "", src}` 编出来是 `src`,而 `{"", ""}` 编出来是**空 chunk**)、读取器 +返回 `true` / 表 / 函数就当结束、读取器抛错要原样传出去(哪怕已经喂进去一半)、 +读取器里**不能 yield**(`lua_load` 走 `luaD_callnoyield`)、以及 `load` **不像 +`loadfile` 那样吃 BOM 与 `#!` 行**(那属于加载层,不属于词法层)。 + +一处也没咬出来 —— 但这本身就是铺它的理由:字节流翻译最容易在读取器这一层 +丢字节或重复字节。 + +#### 两片铺下来是干净的:协程语义、数值 `for` 与强制转换 + +**协程(99 行)**:`yield` 到底能跨哪些边界、`close` 的语义、错误怎么传播、 +四个状态怎么转、嵌套 `resume`。实测分界(两边一致): + +| 能 `yield` | `pcall` / `xpcall`、数值与泛型 `for`(含自定义迭代器)、`gmatch`、 +`__index` / `__newindex` / `__add` / `__lt` / `__concat` / `__len` / +`__call` / `__close` | +|---|---| +| 不能 | `table.sort` 的比较函数、`string.gsub` 的替换函数、`print` 里的 +`__tostring` —— 报 `attempt to yield across a C-call boundary` | + +另外几处一并盯住了:`__eq` 里 `yield` 之后协程是 **dead**(不是 suspended); +自己 `close` 自己报 `cannot close a running coroutine`;`` 变量按 +**声明的逆序**关闭,且 `__close` 里抛错会替换掉原错误。 + +**语法与作用域(121 行)**:用 `load()` 把六十来个片段编一遍,把**编译期**错误 +也纳入比对 —— `goto` 跳进作用域、重复标签、给 `` 赋值、`break` 在循环外、 +畸形数字与转义、函数名的各种形态。这些只有手写才碰得到,fuzzer 生成的程序 +撞不上。运行期那一半是多重赋值「右边先全部求值」、表构造器的求值顺序与键覆盖、 +变长参数(含 `nil` 混在里面)、`:` 的各种畸形用法、upvalue 共享、 +`repeat` 的条件能看见循环体里的 `local`、运算符优先级与结合性。 + +**`debug` 与 `utf8`(114 行)**:`getinfo` 的 `what` 每个字母单独与组合、 +`getlocal`/`setlocal`/`getupvalue`/`setupvalue` 的越界、`sethook` 的四个掩码、 +`traceback` 的**形状**(不整段比对,里头有 chunk 名与地址)、`utf8` 五个函数的 +越界与非法序列。 + +**数值 `for` 与强制转换(551 行)**:`forprep`/`forloop` 的四个分支 +(整数升/降、浮点升/降)各铺一遍,含 `maxinteger` 边界、字符串当起止、 +步长 0、浮点步长;外加 `//` / `%` / 位运算 / 字符串转换的角落。三点值得记住: +`for i = 0, 1, 0.5` 的首值是**整数** `0`(`for i = 0, 1.0, 0.5` 则全程浮点); +位运算**不**做字符串转换(`"10" // 2` 给 5,但 `"10" & 1` 报错); +`for i = 2^53, 2^53+2` 与 `for i = 1e300, 1e300` 在浮点精度下是**不推进**的 +死循环 —— 探针里加了 20000 次兜底,两边在同一处跳出来。 + +#### 一次符号审计:`warn` 整函数缺失,而 828 个用例全绿 + +这一片与上面那些矩阵**不是一类**:那些是在「已经写到的函数」里找分歧。这次是拿 +`lbaselib.c` 的 `base_funcs[]` 逐行对 `BaseLib.open` 的注册,发现 **`warn` 压根 +没实现** —— 而 828 个用例全绿,因为没有任何一个脚本用过它。行为矩阵按构造就 +看不见「缺席的函数」。 + +于是留下两样东西:实现,和一道**常驻闸门**——把两边的**符号集**对撞 +(190 行:全部全局、九个库的每个键、字符串元表、`package.loaded`,各打成 +`名字 类型`)。不比值:值是地址,归一化之后什么都不剩,用例会退化成 +`assertFullMatches` 本来就拦的那种空用例。 + +同一片审计还咬出 **`_VERSION`**,第二个整缺口。它是 `Lua 5.4` 而不是 +`Lua 5.4.7`(`LUA_VERSION` 不含修订号,带修订号的叫 `LUA_RELEASE`,是 +`lua -v` 打的)。它比看上去要紧:真实项目到处是 +`if _VERSION == "Lua 5.1" then …` 这种探测,缺了 `_VERSION` 走的是 else 分支 +——**悄悄错**,而不是响亮地崩。 + +第三个 `arg` **不是**库的缺口:`lua.c` 在 `luaL_openlibs` 之后由 `createargtable` +造的。差分驱动既然整体在模仿 `lua.c` 的形状(`pmain`、`msghandler`、多占的那一格), +这一张表也照着造——否则所有读 `arg[1]` 的脚本都进不了比对。 + +实现 `warn` 要连带移植 `lauxlib.c` 的三态状态机,探针咬出三处,每一处都是 +「只测 `warn("@on")` 永远碰不到」的: + +| 规则 | 漏了的症状 | +|---|---| +| `checkcontrol` 要求 `tocont == 0` —— 所以 `warn("@on", "x")` 是把 `@onsecond` **原样打出来** | `@on` 被当控制消息吃掉 | +| 只有 **OFF 与 ON** 两个状态查 `checkcontrol`,`warnfcont` 直接写 —— `warn("a", "@on")` 打的是 `a@on` | 一条消息里出现第二个 `Lua warning: ` 前缀 | +| 消息是 `const char*`,且 `lua_writestringerror` 写的是**字节** | `warn("@on\0junk")` 不再能打开警告;`warn("\255")` 出来变成 `0xC3 0xBF` | + +最后那两条就是前面几节里的 NUL 规则与字节规则,在一个函数里同时出现——这正是 +要按**契约**铺、而不是按例子铺的理由。 + +#### 符号闸门自己也有个洞:文件方法住在 `__index` 里 + +闸门第一版只查 `io` 表,于是**七个文件方法整片漏看** —— `close` / `flush` / +`lines` / `read` / `seek` / `setvbuf` / `write`。它们住在 +`getmetatable(io.stdout).__index` 里(旁边还有 `__gc` / `__close` / +`__name = FILE*` / `__tostring`),不在 `io` 表上。符号审计必须顺着 +`__index` 往下挖才有用,现在挖了。 + +顺着这七个方法一探,咬出一处真分歧:**`fwrite(s, 1, 0, f)` 返回 0,正好等于 +`l`,所以写 0 个字节一定成功** —— 往**只读**文件 `f:write("")` 返回的是文件 +自己,而不是 `nil, "Bad file descriptor", 9`。我们的 `write` 是先判 +`!writable` 再看长度。 + +只有一格能把它逼出来,这正是要铺格子而不是挑例子的理由: + +| | 可写文件 | 只读文件 | +| --- | --- | --- | +| `f:write("")` | 文件(两边同) | **文件** / ~~`nil, msg, 9`~~ | +| `f:write("a")` | 文件(两边同) | `nil, msg, 9`(两边同) | + +读那一侧是不对称的、本来就是对的,**而且得保持不对称**:`f:read(0)` 走的不是 +`read_chars` 而是 `test_eof`(`getc` 再 `ungetc`),所以在只写流上照样失败; +而 `read` 是先 `done >= len` 再判 `readable`。 + +一并实测且一致:只读流上 `flush` / `setvbuf` / `seek` 都成功; +`io.stdout:close()` 给 `nil, "cannot close standard file"`。 + +#### 两片「对所有库函数统一施加」的矩阵 + +`f:write("")` 那次是**返回元数**上的 bug,而且是手摸出来的。于是补两片机械的: +拿同一个输入施加到全部 ~130 个库函数上,比对「成功还是失败 + 每个返回值的 +**类型** + 出错时的消息」: + +* **零参**(约 120 行)—— 铺「参数缺失」这条路; +* **一个错类型实参**(`{}` / `nil` / `1`,359 行)—— 铺「类型不对」那条路。 + 两条在 `lauxlib.c` 里是两个不同的函数(`luaL_check*` 与 `luaL_opt*` + 对「缺失」的处理不一样),只铺一条会漏掉另一条的报错文本。 + +零参那片没咬出新东西,错类型那片铺下来也是干净的。留着是因为它们一次性 +钉住 ~370 个调用形状:以后改任何一个库的报错文本、返回值个数,或者少一个参数 +校验,这里立刻红 —— 而这类角落手写矩阵往往不碰。 + +四样必须排除或归一化,每一样都是踩出来的: + +| | 理由 | +| --- | --- | +| `os.exit` | 会真的杀掉进程 | +| `io.*`、`dofile`、`loadfile` | 动默认流或读 stdin,两边不一样 | +| `debug.debug()` | 进交互式提示符(`lua_debug>`)并读命令 | +| `math.atan` / `math.atan2` | **同一个 C 函数**(`{"atan", math_atan}` 与 +`{"atan2", math_atan}`),报错里报哪个取决于 `findfield` 用 `lua_next` 撞见谁, +而字符串哈希种子每次进程随机 —— 参考实现连跑六次,三次 `atan`、 +三次 `atan2`,它自己对自己都不一致。先把名字统一成 `atan` 再比。 | + +值一律不打印、只打 `type()`:`os.time()` / `os.date()` / `math.random()` 的值 +每次都不同。 + +#### 钩子的格子:掩码 × count × 线程 + +已有的钩子用例是「一个掩码一片」,这片是**全格子** —— 八个掩码、三种 count、 +四个函数体,外加按线程安装。因为好几条语义只在格子里看得出来: + +* `makemask` 用 `strchr`、只认 `c` / `r` / `l`:`"x"` 与 `"CC"` 都是**空掩码** + (而且大小写敏感)。只看 `"crl"` 永远发现不了。 +* **`lua_sethook` 在 `mask == 0` 时把钩子整个关掉** —— 所以 + `debug.sethook(f, "")` **装不上**钩子,`debug.gethook()` 返回 `nil`, + 而不是「装了一个什么都不触发的钩子」。`debug.sethook(f, "x")` 同理。 +* `count` 只有 `> 0` 才并进 `LUA_MASKCOUNT`:传 0 与 -1 都没有 count 事件, + 而 `""` + count 1 是**只有** count 事件。 +* 钩子是**按线程**装的:主线程的行钩子在协程里一行都不触发; + `debug.sethook(co, …)` 只管那个协程。 +* `gethook` 的掩码字符串**不含** count(`unmakemask` 只输出 c / r / l), + count 是第三个返回值单独给的。 + +两样形状上的约束与断言同等重要:事件**收集起来最后统一打印** —— 钩子开着的 +时候每一个 `print` 都会自己产生一对 call / return,直接把结果淹掉;钩子体本身 +跨好几行、还调函数 —— `allowhook` 的重入保护要是坏了,这里是无限递归而不是 +比对失败。 + +铺下来是干净的。 + +#### 整数 / 浮点子类型网格 + +5.4 里「数字」分成两个**子类型**,`math.type` 看得见差别 —— 这是移植 5.4 最容易 +错的一类:写成「一律 double」的实现大多脚本能跑,只在 `//` / `%` / +`string.format("%d")` / `math.tointeger` 这些地方悄悄不同。所以把标准库里**所有 +会返回数字**的门口过一遍,只比子类型(不比值:`os.time` 与 `math.random` 每次 +都不同)。 + +156 行,分两格:常用形状(数学库「整型进整型出、浮点进浮点出」、`tonumber` 的 +四种写法、位运算、`string.unpack` 各宽度、`io` 的 seek、`os.date` 各字段), +以及**边界**(`maxinteger + 1`、`mininteger // -1`、`math.fmod(1, 0)` 与 +`1 // 0` 是**抛错**而不是给 nan、`math.floor(-0.0)` 是整数、 +`math.tointeger(2^63)` 是 nil)。 + +每条都套在 `pcall` 里 —— 那两个抛错的就是这么发现的。不套的话脚本在第一条就死, +后面一百行一条也不会比,而用例照样是绿的。 + +这片第一版有个值得记下的 bug:合并两个探针脚本时,第二半的调用落到了第一半那个 +「收值不收 thunk」的同名 helper 上,于是 156 行里 72 行静默变成 `tag nil`, +而用例依然是绿的。**一整片都是 `nil` 的矩阵什么也没证明** —— 相信「铺下来干净」 +之前,先看一眼参考输出里有没有整片相同值的行。 + +#### 子进程那一片:`luaL_pushfail` 是 `nil` 不是 `false` + +`io.popen`、`io.tmpfile`、`setvbuf` 是 io 库里最薄的一块 —— 各只有一两个用例。 +四个脚本(管道读写、模式校验、tmpfile、`setvbuf` 各模式)铺开之后,第一跑就咬到: + +``` +e3 true nil exit 3 <- 参考实现 +e3 true false exit 3 <- 我们 +``` + +子进程非零退出时 `luaL_execresult` 压的是 `luaL_pushfail`,而 5.4 里这个宏 +就是 `lua_pushnil`(`lauxlib.h: #define luaL_pushfail(L) lua_pushnil(L)`) +—— 从 5.4 把「fail」与 `false` 拆开起就是这样。整个项目里它一直看不出来,是因为 +`os.execute` 自己另有一份逻辑、那份本来是对的;只有 `io.popen` 的收尾走共用的 +`execResult`,于是 popen 是唯一能暴露它的门口。**同一个 helper 有两份实现, +bug 就藏在没人测的那一份里。** + +这里有三样东西是实测定为「不比」的,每一条都是量过的,不是托辞: + +* **子进程的输出/错误必须在命令里重定向掉。** 继承来的描述符不进本实现的捕获 + 缓冲(Java 的子进程继承的是 JVM 真那个 stderr),两边对不齐。所以写的是 + `cat > /dev/null` 与 `2>/dev/null`,不是 `cat` 与裸命令。 +* **不排空管道就 `close` 是竞态。** 参考实现拿到 `nil, "signal", 13` + (`echo` 被 SIGPIPE 杀掉),我们给 `true, "exit", 0`。取决于子进程有没有先 + 写完,逐次运行不稳定,不能进比对。 +* **`setvbuf` 负的 / 极大的 size 是 libc 自己定的。** C 把 size 当 `size_t` + 交给 libc 的 `setvbuf`,能不能分配出来由 libc 决定:同一台机器上 `2^31` + 失败而 `2^40` 成功,`"no"` 配 `-1` 成功而 `"line"` 配 `-1` 失败。 + Java 侧没法合理建模。 + +#### 第三片矩阵:成功路径 + +前两片对每个库函数的矩阵问的是两件事:函数在不在(符号审计), +以及喂错参数时怎么报错(零参 / 错类型那两片)。两条走的都是 +`luaL_check*` / `luaL_opt*` —— **检查分支**。检查通过之后那段计算本体, +两片一行都没碰:一个算错的 `string.rep` 在它们底下会一直绿。 + +于是补第三片:给每个库函数喂一组正常实参,比返回值本身而不是类型。 +209 行,三个脚本 —— string / math(68 行)、table / utf8 / os / coroutine +(44 行)、io / debug / 全局(97 行)。 + +值由 `d()` 描述,既确定性又不失真:数字带 `math.type` 子类型、字符串带长度、 +表递归展开两层并按键排序(`pairs` 顺序与地址都进不来)。只有少数几处比类型 +不比值:`os.time`、`os.clock`、`collectgarbage("count")`、表的 `tostring`。 + +两个值得一提的结果:带种子的 `math.random` 对上了(xoshiro256** 的**序列**是对的, +不只是取值范围);`getmetatable("")` 一行就把整个字符串元表连同 `__index` 里 +十七个方法全倒出来了。 + +**矩阵的敏感性是量过的,不是假设的。** 把 `string.upper` 改坏一格 +(`'a'..'z'` 挪成 `'B'..'Z'`),这一片立刻红在 `s.upper [2] q(3)[Abc]`。 +不会红的矩阵比没有矩阵更糟,而唯一知道的办法就是改坏一处、看它红不红。 + +209 行逐字节一致,第一跑就干净 —— 这本身也是可疑结果(见上面「整片都是 nil +的矩阵什么也没证明」那条)。这里 209 行里有 205 行带着真值,所以不是空转。 + +#### `io.open` 的模式矩阵,以及 stdio 说了算的三件事 + +`l_checkmode` 认的是 `[rwa]%+?b*`,而且尾巴是 `strspn(mode, "b") == strlen(mode)`, +不是「最多一个 b」—— 所以 `"rbb"` 合法而 `"rb+"` 不合法,`"r b"` / `"br"` / +`"R"` / `"r\t"` 全失败。34 种模式 ×(读 / 定位 / 写 / 位置 / 关 / 最终内容), +79 行。 + +咬出**三处**分歧。三处都是 **stdio** 定的、不是 Lua 定的,而且每一处都是量过 +才放到一边的: + +1. **更新模式上「读完不定位直接写」** —— ISO C 里这本就是未定义行为 —— 落点由 + C 的**预读缓冲**决定。实测:12 字节的文件写到 13(EOF),6002 字节的写到 + 4097(BUFSIZ+1),而 `read(5000)` 又快又准地写到 5001(大块读绕过缓冲)。 + 矩阵改成每次写之前显式 `seek` 一次绕开这条 UB 路径 —— 绕开之后 + (`read → seek → write`)两边**逐字节一致**。 +2. **append 档上写完的位置。** `seek("set",0)` 再写 1 字节:参考给 **1** + (`base+n`),我们给 13(`EOF+n`,这一边才符合 POSIX)。再量下去发现它也是 + 缓冲定的:`n<=4095` 给 `base+n`、`n>=4096` 给 `EOF+n`。所以矩阵在量位置之前 + 先 `flush`,把位置压到 POSIX 那个唯一确定的答案上,两边才可比。 + **写进去的内容两边始终一致**,分歧只在 `seek("cur",0)` 报的数。 +3. **标准流上的 `seek`** 取决于真实 fd 可不可定位:重定向到文件给 0、 + `/dev/null` 给 0、管道与 tty 给 `"Illegal seek"`。本实现拿不到那个 fd,不比。 + +其中两处与 `setvbuf` 的 size 是同一个形状:C 把决定权交给 libc,没什么可建模的。 +差别在于这里的**可观测量**不只是 errno 而是文件偏移 —— 所以要写下来,而不是 +悄悄排除。 + +#### 让 `require` 真去磁盘上找模块 + +`package` 那道闸门之前只走了搜索器链的「纯 Lua 那一半」(在内存里摆 +`package.preload`),而 `PackageLib` 是未覆盖率第二高的类。所以补一片 +把模块真写进磁盘再让 `require` 去找的矩阵,57 行:模块返回 +nil / false / true / 数字 / 无返回 / 多值(只取第一个)、模块抛错、模块语法错、 +模块里自己设 `package.loaded`、点号模块名换目录分隔符、第二次 `require` 的缓存、 +循环 `require`(参考给 `C stack overflow`,不是哨兵)、`package.loaded` 预置成 +值 / false / nil 三种后果、`package.preload` 给函数 / 返回 nil / 抛错 / 非函数、 +`searchpath` 的 `sep` 与 `rep`、`package.searchers` 的个数与逐个调用、整条链被换掉。 + +顺带钉住一件会悄悄坏掉的事:每条 `module 'X' not found` 都把候选路径整串 +打印出来,所以默认的 `package.path` 必须与参考二进制编译进去的 +`LUA_PATH_DEFAULT` 逐字符一致。 + +57 行第一跑逐字节一致 —— 所以敏感性照旧是量过的:把 `searchpath` 的默认 `sep` +从 `"."` 改成 `"-"`,立刻红在 `sp_dots`。 + +#### 逐行读覆盖率报告 + +按「类的未覆盖率」挑目标只能挑到这一步 —— 模式矩阵与 require 矩阵铺完之后, +那几个大类的缺口几乎没动,因为一类里没跑到的指令大半是 +`catch (IOException e)`,Lua 脚本根本走不到。所以再往下走一层:打开 +`target/site/jacoco/<包>/IoLib.java.html`(XML 报告没有行级信息,HTML 有), +把 `class="nc"` 的行号抄出来,逐条问「**这一行 Lua 能走到吗**」。 + +`IoLib` 的 63 行未覆盖里,大多数确实走不到。剩下这些是能走到却从没走到的, +也就是真正漏测的行为,39 行: + +* 已关闭句柄的 `tostring` 是 `"file (closed)"` —— **不是** `io.type` 的 + `"closed file"`。两个字符串不一样,之前只测了其中一个。 +* `io.lines()` **无参**与显式 `nil`:走注册表里的 `IO_INPUT`,与 + `io.lines(文件名)` 不是同一条路。 +* `f:read("n")` 的各形状:指数(`1e5`,以及 `1e999` 给 inf)、十六进制 + (`0x10` / `0x1p4` / `0x.8p1`)、前导零(`0777` 是十进制不是八进制)、 + 残缺前缀(`0x` 与 `1e` 给 nil)、超长数字串(给 nil)。 +* `lines` 迭代途中把文件关掉:迭代器再调用报 `"file is already closed"`。 +* 大块写(5000 字节)走「绕开 wbuf 直接落盘」那条分支。 + +39 行全逐字节一致 —— 也就是说这些分支本来就是对的,只是没人跑过。 +它们的价值在回归:谁改了 `read("n")` 的前缀判断或 wbuf 的阈值,这里立刻红。 + +#### 目录那个 bug:一条 POSIX 规则,五处全错 + +读 PackageLib 的未覆盖行、逐条问「Lua 能不能走到」,摸到 `searcher_C` 的兜底 +分支,再摸到「模块名撞上目录」这件事 —— 结果是**一条规则没建模,五个门口全错**: + +POSIX 允许 `open(dir, O_RDONLY)` 成功。于是 `fopen(dir,"r")` 成功、 +`fopen(dir,"w")` 给 `EISDIR`、第一次 `fgetc`/`fread` 失败于 `EISDIR`。 +JVM 根本不让打开目录,于是本实现以同一种方式在五处同时错: + +| | 参考 | 修之前 | +| --- | --- | --- | +| `loadfile(dir)` | `nil, "cannot read

: Is a directory"` | `nil, "cannot open

: Input/output error"` | +| `io.open(dir,"r")` | file 句柄 | `nil, "

: No such file or directory", 2` | +| `io.open(dir,"w")` | `nil, "

: Is a directory", 21` | `nil, "

: No such file or directory", 2` | +| `package.searchpath` | 找到(返回路径) | `nil, "no file '

'"` | +| `require(name)` | `error loading module … cannot read …` | `module '…' not found` | + +修法是同一个念头落在四处:`openInto` 放行只读的目录打开(要写就报 EISDIR)、 +`LuaFile.getc`/`read` 报 21、`seek`/`tell` 按零长文件算(目录 fd **是**可定位的: +`f:seek("set",5)` 给 5,`f:seek("set",-1)` 仍然是 EINVAL)、 +`PackageLib.readable` 认目录、`loadfile` 与 `searcher_Lua` 改走 `cannot read` +那条分支。 + +修完 18 行逐字节一致。怎么发现的,比这个 bug 本身更值得记: + +* README 之前写着目录「没问题、不要修」—— 那是读 lauxlib.c 读出来的,不是 + 跑参考实现跑出来的,而且它是错的。先量,再写。 +* 代码注释(`BaseLib`)里那句同样断言了「目录不用改」。 + **注释跟代码保持一致不是证据。** + +#### errno 形状表:`os.remove` / `os.rename` + +`LuaAux` 还剩 15 行没走到,其中大半是 `errnoOf` / `strError` 里 `ENOTEMPTY`、 +`EACCES` 这些从没有脚本碰过的分支。22 行点名打过去,咬出**五处**,四处是真 bug: + +* **`ENOTEMPTY` 的数值是操作系统定的,不是 libc 定的**:Linux 39、macOS/BSD 66。 + 这里写死 39,于是 `os.remove(非空目录)` 消息对、数字差 27。改成按 `os.name` 取。 +* **`strError` 得同时认两个数值。** 只改数字会得到「errno 66 对、消息却是 + Input/output error」—— 数值常量与它的文本必须一起动。 +* **`os.remove("")`** 给 `EIO`,C 给 `ENOENT` —— 与 `loadfile("")` 同一类 + 「C 与 Java 判路径不一致」的角落。 +* **`os.rename` 的形状表整张没对**:文件 → 已存在的目录是 `EISDIR`(21)、 + 目录 → 已存在的文件是 `ENOTDIR`(20)、目录 → **空**目录是成功(替换)、 + 文件 → 已存在的文件也是替换。之前一律报 `EEXIST`,因为 `Files.move` 没带 + `REPLACE_EXISTING`。 + +(`os.remove("/etc")` 原先是跳过的 —— 本机因为 SIP 它是 `EPERM`,看着像机器相关。 +现在不跳了:见下面「errno 形状表」那一节,整类用例从**写死**改成**按 libc 推导**, +于是可移植了。) + +跟目录那个 bug 是同一条教训:**「这个值随平台变」是「按平台取」的理由,不是 +「不测」的理由** —— 恰恰是跳过它,才让一个写死的常量活到今天。 + +### 逐字节变异:把 `lundump.c` 的每一条 `error()` 都点名 + +`Undump` 有 11 个没被任何用例走到的分支——截断、整数溢出、常量字符串格式错、 +版本/格式/尺寸不匹配。手挑坏样本只会覆盖想得到的位置,所以这次改成 +**逐字节变异**:对一个真实 dump 做「逐长度截断 + 逐字节变异(4 个取值)+ 变长 +整数上限注入 + 尾部垃圾」,再对剥掉调试信息的版本和带 upvalue/内嵌函数的版本 +各扫一遍,共 865 行、21 种不同结果、542 行不是 `ok`。全部 11 条分支都被撞出来, +其中两个是手挑时不会想到的。 + +两个坑先说,因为它们会让整个矩阵白扫: + +* **`string.dump` 的产物里带着源名,而源名默认就是脚本路径。** 参考实现跑 + `/tmp/x.lua`、本实现跑 `target/rt/case.lua`,dump 就差 7 个字节,后面所有偏移 + 跟着错位——第一次跑出来的 40 处「分歧」全是这一个原因。探针里把 chunk 名固定 + 成 `"=X"`,两边字节才完全一致。 +* **变长整数的上限注入要落在 `nupvals` 之后。** 头是 31 字节(不是 30),第 32 + 字节才是 upvalue 数。 + +扫下来只有一处真分歧,而它是一处**我们凭空发明的分支**: + +* **`loadSize` 里那个 `v > Integer.MAX_VALUE → integer overflow`,C 里根本不存在。** + 上一版的注释还断言「真拿到放不下的长度时,C 会去分配并抛 `LUA_ERRMEM`」——这是 + **照着 C 源码读出来的结论,实测是错的**:把源名长度写成 `2^35-1`(变长整数 + `7f 7f 7f 7f ff`),参考实现给的是 `truncated chunk`,不是内存错误。64 位上 + malloc 34GB 是成功的,C 于是老老实实去读那 34GB,读到流末尾报截断。于是这个 + 分支被删掉,长字符串改成**边读边攒**:攒多少取决于流里实际有多少字节,而不是 + 声明了多少;流一尽照样报 `truncated chunk`。 + +改完之后做反向验证,把 `loadUnsigned` 的 `limit >>>= 7` 换成注释里警告过的 +`Long.MAX_VALUE >>> 7`,结果**顺带撞出第二个缺陷**: + +* **`loadCode` 的 `new byte[n * 4]` 会整数溢出成负数**,抛 + `NegativeArraySizeException`——**Java 异常,`pcall` 抓不住**,一个畸形 chunk + 就能把宿主进程炸掉。而且它比 C 多占一倍内存:C 只分配 `n` 条指令,这边额外再 + 铺 `4n` 字节,等于照不可信输入给的尺寸双倍付款。改成逐条读、缓冲复用之后既不 + 溢出、也只按 `n` 分配。再跑一次同样的破坏:**不再崩溃**,并且红 13 行——矩阵 + 对这类细微错误是敏感的。 + +教训还是那条:**注释跟 C 源码一致不是证据**。那条注释写得越像那么回事,越容易 +让人以为「这里对不上是有意的偏离」,于是一个凭空多出来的分支安安静静活到现在。 + +### JaCoCo 也会说谎:调用「必然抛异常的方法」的那一行 + +按未覆盖行挑目标这件事有一个坑,这次才看清:**JaCoCo 把「调用一个总是抛异常的 +方法」的那一行标成未覆盖**——方法调用指令之后还跟着一条丢弃返回值的 `pop`, +而那条 `pop` 永远走不到,于是整行记成 `nc`。 + +实测:`Undump` 的 10 条 `error()` 分支在测试里被触发了 **848 次**(在 `error()` +里加一行打印数的),`error()` 方法内部的每一行都是 `fc`,**而九个调用点一律 +`nc`**。照着报告去补用例,等于给早就覆盖到的代码再补一遍。 + +判据:**这一行是 `xxxError(...)` 调用或以 `throw` 开头,而同名方法内部是已覆盖 +的** —— 那就是假缺口。按这条过滤后,`Codegen` 的 15 行、`Parser` 的 21 行、 +`Undump` 的 10 行**全部消失**,剩下的才是真缺口。 + +### 表键矩阵:`rawequal(math.maxinteger, math.maxinteger + 1.0)` + +过滤后的真缺口里 `LuaTable` 有 16 行,都是「只在很窄的键区间上才走」的路径: +`hashFloat` 的 `-0.0` 归一化、`landsInArray` 的浮点分支、rehash 时哈希槽不够。 +手挑几个键碰不到,所以做成**键 × 键**的方阵:20 个键各写一遍、再用全部 20 个键 +各读一遍,外加 `math.maxinteger` 前后各 2048 个整数按步长扫。 + +扫出来的真分歧只有一处,而且**不在 `LuaTable` 里**: + +* **`rawequal(math.maxinteger, math.maxinteger + 1.0)` 应当是 `false`。** + 两个数确实不等(`math.maxinteger` 是 2^63−1,右边是浮点 2^63), + 但 `(double)(2^63−1)` 舍入成 `2^63`,于是「都转 double 再比」判成了相等。 + Lua 5.4 对变体不同的两个数不是这样比的:两边都按 F2Ieq 转成整数再比, + 浮点那侧转不成整数就判不等。修的是 `LuaValue.numberEqual`——它同时是 + `==`、`rawequal` 和**表的键比较**的实现,所以 `t[math.maxinteger]` 与 + `t[2^63]` 之前被当成了同一项。 +
顺带把 `LuaTable` 里那份重复的 `floatToIntExact` 收敛到 `LuaValue`, + 免得两处判据将来只改一处。 + +两条规则只在 `math.maxinteger` 往前约一千个整数上分岔,所以脚本末尾那段按步长扫 +是必须的:**去掉它,修与不修只差 6 行;带上它,改回旧写法红 77 行。** + +`next` 的**顺序**依赖哈希函数,两边不必相同,所以键集合一律排序后再打印—— +只比集合,不比顺序。 + +### `debug` 的帧形态矩阵:hook 帧在第 3 层 + +过滤掉假缺口之后 `DebugLib` 还有 22 行,全都是「栈长得不一样时才走」的路径: +`currentLine` 对非 Lua 帧返回 `-1`、`funcNameFromCallInfo` 的 `ci.hooked` 分支、 +`getinfo` 传**函数**而不是层号的那条取帧路、`traceback` 的 `(...tail calls...)`。 +手挑两个例子碰不到,于是做成帧形态矩阵:顶层/Lua 帧/C 帧(`table.sort` 与 +`pcall` 的回调里往外看)/协程内部与外部/尾调用链/长函数配 hook,共 106 行。 + +两件事值得记下来: + +* **hook 帧要取到 level 3。** 在 hook 里调 `getinfo` 时,level 1 是包在外面的 + `pcall`、level 2 是打印用的 helper,**hook 自己的帧在第 3 层**。只取到 2 层的话 + 那条 `namewhat = "hook"` 永远走不到——而它在报告上和别的未覆盖行长得一模一样。 +* **有一处不比**:最外层那个 C 帧的 `"(C temporary)"`,值正是宿主调用 `pmain` 时 + 压的实参。参考实现压的是 `(argc, argv)`,所以取出来是 `2` 和一个 userdata; + 本实现的驱动压的是脚本文本。**那不是 Lua 语义,是宿主自己的调用约定**,比不出 + 东西,脚本里只取到 level 2。 + +结果:106 行逐字节一致,没有真分歧,`getupvalue` 对 C 闭包、`getinfo` 传函数+线程、 +`traceback` 的尾调用标记这三条从未走过的路径被覆盖到了。剩下没走到的是防御分支 +(`HOOKKEY` 不存在、钩不是函数)和**必须由宿主设置**才走得到的那条—— +`debug.gethook` 的 `"external hook"`:Lua 里 `sethook` 设的永远是 `HOOKF`, +纯脚本到不了。 + +### 协程 × C 边界矩阵:`xpcall` 的消息处理函数不许 yield + +每个「由 C 发起回调」的入口都有自己的 `nny`(C 版的 `nyci`)记账,漏掉一处 `+1` +在没有东西正好在那个点上 yield 之前是看不见的。所以这是一张按 **yield 发生在哪里** +铺开的矩阵:`pcall` / `xpcall` / **`xpcall` 的消息处理函数** / `table.sort` 的比较 +函数 / `string.gsub` 的替换函数 / `load` 的 reader / 八个元方法(含 `__close`)/ +四种泛型 for 迭代器 / 调试 hook / `coroutine.wrap` / 嵌套协程 / resume 已死与已出错 +的协程 / 挂起的协程 resume 自己。每一行还单独问一次 `coroutine.isyieldable()`, +把「能不能」与「真做了会怎样」变成两个各自被测的答案。67 行。 + +咬出一处真分歧,值得把话说准 —— 因为错的那一边看起来**更能干**,不是更差: + +``` +run xpcall_msgh true false error in error handling <- 参考实现 + status xpcall_msgh dead +run xpcall_msgh true in msgh <- 本实现 + status xpcall_msgh suspended +``` + +`LuaState.applyErrMsg` 用的是可 yield 的 `pcall`,而 C 版是 +`luaD_callnoyield`。道理比症状重要:能在错误处理的续体里 yield,就等于承认 +「错误已交出去一半」这种中间态可以被挂起,而 C 版没有这种状态。改法是 +`callNoYield` 加一个 `catch (LuaError)`,处理函数自身出错不再兜底—— +`"error in error handling"` 正是从这里来的。 + +另外两件被这张矩阵钉死的事: + +* **`pairs` 在字符串键上的顺序是随机 seed 的,不能比。** 字符串键走 + `luaS_hash`,seed 来自 `lstate.c: luai_makeseed` —— `time(NULL)` 混三个 ASLR + 地址。同一份脚本在参考实现上连跑 5 次,第一个键依次是 `a a b a b`。 + **任何打印键先后的探针都是假红发生器**;这一片改成把协程跑到死、把收集到的 + yield 值**排序**再打印。(本实现没有 seed,顺序固定且确定——这是一处有意留下 + 的安全不对称:参考实现自己的顺序同样不可观测,因为没有东西能依赖一个每次启动 + 都变的值。) +* **`load` 的 reader 必须有终止条件。** 永不 `return nil` 的 reader 会让 `load` + 一直读下去 —— 探针第一版**把参考实现二进制挂住了**,只能 `pkill`。 + +反向验证:把 `callNoYield` 换回 `call`,正好红 2 行,测试失败。 + +### to-be-closed 矩阵:`__close` 里抛的错也要过消息处理函数 + +`` 是 5.4 唯一的新语法、边界最多,所以这张矩阵按**怎么离开这个块**铺开 +(掉到块尾/嵌套/`return` 提前/`break`/`goto` 跳出/`error` 展开,错误对象分 +字符串与表两种),交叉 **__close 自己干什么**(记一笔/抛错/抛非字符串/返回多 +值/`yield`/是个数字/是可调用表/压根没有),再交叉 `pcall` / `xpcall` / 协程 / +`coroutine.close`。56 行。 + +咬出两处分歧,同一个根因:**`__close` 里抛出的错误也要过消息处理函数**。 + +``` +msgh orig | close a err=H | msgh E:a | xpcall -> false H <- 参考实现 +msgh orig | close a err=H | xpcall -> false E:a <- 本实现 +``` + +`luaD_pcall` 是在**最后一句**才 `L->errfunc = old_errfunc` 的,而 +`luaD_closeprotected` 就在那句**之前** —— 所以跑 `__close` 的整个过程中处理函数 +一直装着,`__close` 里的 `error()` 和别的错一样走到 `luaG_errormsg`。容易漏的是 +它的实际后果:**最终结果是处理函数的返回值,不是 `__close` 那个错**。同一处修复 +顺带产出了「处理函数自己抛错」那条的 `error in error handling` —— 那句话正是 +`__close` 的错**第二次**过一个已经在失败的处理函数时被兜出来的。 + +第二处是同一条规则在协程侧的那一面:`finishpcallk` 是在 `luaF_close` **之后**才 +恢复 `L->errfunc`,所以处理函数必须在**关 `__close` 之前**跑,不是之后。放反了的 +表现是 `close a err=after | msgh h2 after`(参考给的是 +`msgh h2 after | close a err=H-h2`)—— 关变量拿到的是没处理过的原始错。 + +把那次调用提前又牵出第二个独立的坑:处理函数必须在把 `top` 压回 `fIdx + 1` +**之前**跑。`luaG_errormsg` 是在**出错那一刻的栈顶**压调用帧的,那会儿被保护函数 +的局部变量都还在;先收 `top` 再跑,它就从 `fIdx + 2` 开始写 —— 那正是第一个局部 +变量(也就是那个 `` 变量)的槽,于是 `__close` 被覆盖成处理函数自己, +下一次调用报 `attempt to call a nil value`。 + +**有一条组合是故意不放进矩阵的**:消息处理函数自己抛错。C 的 `luaG_errormsg` 会 +在那里无限递归到 C 栈溢出,输出几百行、行数随栈深度而变。矩阵只钉它的**结果**。 + +### 调试钩子矩阵:钩子抛的错能不能接住,取决于钩子设在哪 + +钩子是本实现唯一「用 Java 异常打断 VM 正常流程」的地方:事件在 `precall` / +`poscall` / 指令循环三处各发一次,而 `allowhook`、`nCcalls`、`ci` 在这三个点上的 +状态都不一样,形状最容易走偏。这张矩阵按 mask 的八种组合(`c`/`r`/`l` 及其并集) +× `count`(0/1/2/3/5)× 帧的种类(Lua / C / 尾调用 / 协程)× 钩子自己的行为 +(关自己/换自己/抛错/抛非字符串/`yield`/读 `debug.gethook`)铺开,56 行可比 +输出。 + +结果:**逐字节一致,没有真分歧**。仍然收进来,因为敏感性是量过的 —— 把事件名 +`"tail call"` 改成 `"tailcall"`,14 行立刻变红。从没红过的用例等于没证明任何东西。 + +钉住的不是 bug,而是写同类探针时的两个坑: + +* **钩子抛的错能不能被 `pcall` 接住,取决于 `sethook` 在哪调。** `sethook` 之后 + 下一个 `call` 事件就是 `pcall` 自己(`c` 掩码连 C 函数也算),而那一下 + 发生在 `pcall` 建立保护之前,错直接逃出去把脚本打死。参考实现报 + `lua: hk1`、回溯里是 `[C]: in function 'pcall'`,脚本到此为止。要测「接住」必须 + 把 `sethook` 放在 `pcall` 里面;「逃出去」那条只能放在脚本最末, + 否则后面几十行一条也不比 —— 而用例照样是绿的。 +* **钩子里取栈要取到第 3 层**:第 2 层是打印用的 helper,第 3 层才是 + `namewhat = "hook"` 那一帧,与帧形态矩阵的结论对得上。 + +### 类型元表矩阵:`setmetatable` 的第一个参数必须是表 + +`lua_setmetatable` 按类型分派三处:表与 userdata 存在对象自己身上,其余装进 +`global_State` 的 `mt[]`(**整个类型共享一张**)。这张矩阵把八种类型逐个走一遍 +`debug.setmetatable` / `debug.getmetatable`,48 行逐字节一致,且敏感性量过 —— +删掉 userdata 那个分支里的一句,6 行变红。 + +那条分支一直覆盖不到的原因值得写下来:**Lua 里的 `setmetatable` 第一个参数必须是 +表**(`lbaselib.c` 第一行就是 `luaL_checktype(L, 1, LUA_TTABLE)`),userdata 只能 +走 `debug.setmetatable`,而套件里从来没人那么写过。(报错里的类型名走 `__name`: +`bad argument #1 to 'setmetatable' (table expected, got FILE*)`。) + +钉住的两条: + +* **只有表与完整 userdata 认 `__name`。** 给数字 / nil / 布尔 / 函数 / 线程 / + 字符串设了带 `__name` 的类型元表,运行时依然说 `number` / `nil` / `boolean` 等 + 原名;只有 userdata 变成 `attempt to index a M_ud value`。 +* **字符串的类型元表就是 string 库那张**。换掉它 `("x"):upper()` 立刻失效,所以 + 探针必须存下来再还原 —— 而且必须在「给每种类型设空元表」那段**之前**存,否则 + 存的是错的,脚本会中途死掉。 + +### package 矩阵:模块名是 `const char *` + +`PackageLib` 是覆盖率最薄的类(0.89),所以这个矩阵把 `loadlib.c` 对路径做的事 +整个铺开:18 种模板形状(`""`、`";"`、`"a;"`、`";;a;;"`……)× `?` 替换 × +`sep` / `dirsep` 的边界 × 内嵌 NUL × 参数检查 × 四个搜索器逐个直调 × `loadlib`。 +156 行逐字节一致,且敏感 —— 把 `const char *` 那次截断去掉会红 3 行。 + +它抓出一处真分歧和一个写错的常量: + +* **`searcher_C` / `searcher_Croot` 没有在第一个 NUL 处砍断模块名。** C 里是 + `const char *name = luaL_checkstring(L, 1)`,于是 + `package.searchers[3]("a\0b")` 找的是 `a`;本实现找的是整串。只有直调搜索器 + 才走得到(`require` 自己先砍了),所以之前的探针一条都没碰上。 +* **`loadlib` 第三个返回值是两个常量,不是一个。** `ERRLIB`(库打不开) + 给 `LIB_FAIL` —— 启用 dlopen 的构建里是 `"open"`,兜底分支里被重定义成 + `"absent"`;而 `ERRFUNC`(库能打开、符号找不到)给的是写死的 `"init"`, + 兜底分支只重定义了前者。拿真 `.dylib` 在参考实现上量过: + `package.loadlib(real, "luaopen_nosuch")` → `nil, , "init"`。 + 本实现的 `lookForFunc` 恒返回 `ERRLIB`,这一支走不到,但常量已经改成 C 那个。 + +矩阵**故意不比**两样东西: + +* **`dlerror()` 的文本。** 它带地址,`dlsym(0x…, luaopen_x): symbol not found` + 每次运行都不一样,所以 C 搜索器那节只比 `\n\t` 之前那一截。 +* **`LUA_PATH` 的处理。** `;;` → 「把默认路径插在这里」这条展开是真的、也容易写错 + (只替换**第一个** `;;`,且不在默认路径两边补分隔符)。它塞不进这套差分驱动 —— + JUnit 改不了参考二进制的环境变量 —— 所以单独用 `env LUA_PATH=…` 量了九种形状 + (`a;;b`、`;;`、`a;;`、`;;b`、`a;;b;;c`、`;;;`、`noMark`、空串、 + `LUA_PATH_5_4` 优先):每一次都一致。 + +**这个矩阵一点覆盖率都没抬** —— 跑完 `PackageLib` 仍是 0.891、miss 仍是 117。 +那些行本来就被别的用例**走到**了,只是**走完没有比对**。覆盖率看不出这件事, +「覆盖率涨了」也不是「缺口补上了」的判据。 + +### io 的边角矩阵:`f:read(0)`、大块写、`ESPIPE`、目录 + +`IoLib.LuaFile` 是剩下最薄的类(0.80),而它每一行都是一条 stdio 规则,不是 Lua +语义。这个矩阵覆盖 `read(0)`、绕过 8192 写缓冲的大块写、管道上的 `seek`、读目录、 +`io.lines()` 无参。43 行逐字节一致,且敏感 —— 把目录那条 errno 从 `EISDIR`(21) +改成 `EIO`(5) 会红 3 行。 + +各组钉住的: + +* **`f:read(0)` 在非 EOF 处给 `""`、在 EOF 处给 `nil`。** `fread` 要 0 个字节 + 返回 0,算「读到了」,所以空串是一次真结果而不是出错。它的镜像(往只读文件写 + 空串成功)更早前就钉过了,这是同一条规则的两半。 +* **超过 8192 写缓冲的整块直接落盘**,所以 10000 / 8192 / 8191 三种长度各写一次 + 再读回来验位置。 +* **管道上的 `seek` 是 `ESPIPE`(29)**,不是随便一个 IO 错。只用 `io.popen` + 出来的文件测这条 —— 原因见下面那条警告。 +* **目录:`fopen` 成功、读失败(`EISDIR` 21)、`seek` 又是另一套** —— 位置照算, + 只有负的目标位置才是 `EINVAL`(22)。 +* **`io.lines()` 无参取的是注册表里的默认输入。** 把它关掉之后再要,报的是 + `attempt to use a closed file` 而**不是** `no input file`:注册表里那一项还在, + 只是已经关了。 + +**差分探针里永远不要对 `io.stdout` / `io.stderr` 做 `seek`。** stdout 被重定向成 +**文件**时那条 `seek("set", 0)` 是**成功**的,写指针回到 0,后面每一行都从头覆盖 +—— 参考实现的输出会变成一坨乱序的残片,看起来跟「实现有 bug」一模一样。只有重定向 +到**管道**时才报 `ESPIPE`。这条取决于驱动怎么捕获输出,不属于 Lua 语义。 + +### errno 形状表:errno 与 strerror 是一对逆函数 + +文件操作不是「存在不存在」一个维度,`errno` 由**目标的形状**决定: +`io.open("文件/y")` 是 `ENOTDIR`(20)、`io.open("/etc/hosts", "w")` 是 +`EACCES`(13)、`os.remove(".")` 是 `EINVAL`(22)、`os.remove("/")` 是 +`EISDIR`(21)、`os.remove("/etc")` 在本机是 `EPERM`(1)。本实现原先只按**Java 异常 +类型**分类,认不出的全落进 `ENOENT` / `EIO` 兜底 —— 五条全错。 + +修法是**反查而不是另列一张表**:errno 与 strerror 是逆函数,而 Java 的 IO 异常 +把 libc 的 `strerror` 原文带在消息里(`FileNotFoundException` 是 +`" (Permission denied)"`,`FileSystemException.getReason()` 直接是 +`"Not a directory"`),所以拿 `LuaAux.strError` 那张表反查就能拿回数字。 +44 行逐字节一致,且敏感 —— 去掉反查会红 9 行。 + +**这也是这个矩阵能跨平台的原因。** `os.remove("/etc")` 在 macOS 上是 `EPERM` +(SIP)、在 Linux 上是 `ENOTEMPTY`,但**两边现在都从同一个 libc 推导**,于是各自 +平台上一致,而不是一边揣着一个平台相关的常量。这正好反驳「这个值随平台变,所以 +跳过它」—— 恰恰是跳过它,才让一个写死的常量活到今天;而推导它,才让它可测。 + +写探针时踩的两个坑,都记在用例 javadoc 里: + +* **素材放子目录、收尾整个删掉**,别撒在仓库根目录; +* **永远不要拿真的系统文件试 `os.remove`**。`os.remove("/etc/hosts")` 一旦以 + root 跑就**真删掉了**。走 `EACCES` 那条路用的是测试自己建的 `chmod 555` 目录。 + +#### `utf8` 矩阵:一个字面量抄了两份,以及一行谁也走不到 + +`Utf8Lib` 是 0.930,而它跟 `IoLib` / `PackageLib` 不一样 —— 缺的都是纯逻辑, +没有平台行为、没有 `IOException` 兜底。逐行读未覆盖行捞出来的是**同一份字面量抄了 +两遍**:`open` 里内联拼了一份 `utf8.charpattern` 的字节数组,旁边的 +`charPattern()` 一个调用者都没有。同一常量两份拷贝,是看不出漂移的 —— 覆盖率 +报告上就是两段都 `nc`。合成一处(并让矩阵真的去读 `utf8.charpattern`),这条才进 +了测试。 + +72 行矩阵覆盖 `utf8.char` 的每个边界(0、`0x7F`、`0x80`、`0x7FF`、`0x800`、 +`0xFFFF`、`0x10000`、`0x10FFFF`、`0xD800`、`0x110000`、`MAXUTF`、`-1`、 +`math.mininteger` / `math.maxinteger`、非整数、字符串)、**只有第 2 个及之后的参数 +才走**的那条 `value out of range` 分支、`utf8.codepoint` 的全部越界形状 +(`i > j` 一个值都不返回、0、越过末尾、负数、空串、`lax` 真假、浮点 / 字符串 / +布尔参数),以及**真正拿来用**而不是看一眼的 `charpattern` —— +`"a中b\u{1F600}"` 上 `gmatch` 出 4 个字符,`"\xff\xfe"` 上出 0 个。 + +第一跑逐字节一致。`Utf8Lib` 从 0.930 到 **0.995**,只剩一行: +`throw L.libError("string slice too long")`,要一条不小于 2GB 的字符串才走得 +到。记为「走不到」,不去追。 + +#### 语法错误矩阵:把 `lparser.c` 与 `llex.c` 里每一条报错都走一遍 + +`Parser` 是第二大缺口(169 条未覆盖指令),而且跟 `IoLib` / `PackageLib` 不一样, +它的缺口全是纯逻辑。逐行读未覆盖行发现几乎全是**报错文案** —— 而且每一条都是 +`nc`,也就是从来没有任何用例让它们出现哪怕一次。 + +所以这片矩阵不是凭印象挑的,而是**按 C 源码的报错站点枚举**: +`error_expected` 与 `check_match` 的两种形态、`errorlimit` 的局部变量 / +upvalue / 构造器项、`attempt to assign to const variable`、 +`jumpscopeerror` / `undefgoto` / 重复标签、` or '...' expected`、 +`function arguments expected`、`unexpected symbol`、`unknown attribute`、 +`multiple to-be-closed variables`、 +`cannot use '...' outside a vararg function`、`'=' or 'in' expected`、 +`control structure too long`,外加词法层那一整套(unfinished string / +long string / long comment、malformed number、hexadecimal digit、`missing '{'`、 +UTF-8 value too large、`missing '}'`、decimal escape too large、 +invalid escape sequence、invalid long string delimiter)。172 行。 + +钉住三件会悄悄坏掉的事: + +* **`` 与 `` 打印出来是不带引号的**,而 `'end'` 与 `','` 带 —— + `luaX_token2str` 对 `token >= TK_EOS` 直接返回裸串。而 `near` 后面那个 token 走 + `txtToken`,字符串与数字取**原文**,于是消息里会出现嵌套引号: + `hexadecimal digit expected near ''\x''`。 +* **C 栈溢出是「返回」不是「抛」。** 深度嵌套触发 `luaE_incCstack`,但 + `luaD_protectedparser` 会兜住,所以 `load` 给的是 + `nil + "C stack overflow"`;只有 `load` 自己的参数校验 + (`bad argument #2 to 'load'`)才是真的抛。探针一律走 `pcall` 才能把这两种分开。 +* **`control structure too long` 要循环体超过 `MAXARG_Bx`**(262143)条指令才触发, + 所以那条用例是现生成的 1MB 源码 —— 参考实现 0.02 秒、本实现 0.6 秒,留着不亏。 + +第一跑逐字节一致,且敏感:给所有 token 名加引号红 11 行;去掉 `check_match` 的 +同行捷径红 13 行;`errorlimit` 的 `"main function"` 改成 `"main chunk"` 红 1 行。 + +#### ……以及覆盖率报告自己有个洞,不是代码有洞 + +装进去之后 `Parser` 的覆盖率几乎没动 —— 169 → 167,而未覆盖行的清单逐字没变。 +172 行新的报错路径真的跑过了的话,这不可能;而它们确实跑过了:给其中一行插个 +`println` 会打出来,而且那条逐字节一致的输出只可能来自那几处 `throw`。 + +露出马脚的是**它上面那一行**: + +```html +if (!lh.v.isVar()) { + throw ls.syntaxError("syntax error"); +``` + +JaCoCo 知道这个分支两个方向都走过,行号表也证明 1315 行有自己的指令 —— 可这一行 +显示 `nc`。`Parser` 一共有 **19 行 `throw`,没有一行是 `fc`**(17 个 `nc`、 +2 个 `pc`),而项目其余部分是 136 个 `fc` / 41 个 `nc`。 + +对「怎么挑目标」的结论:**未覆盖的 `throw` 行不能当作「这条路径没跑到」的证据。** +`Parser` 那半个缺口里有相当一部分是这个假象,追下去只会花一下午去证明本来正确的 +代码是对的。真正能说明路径跑到了的是**比对**,不是覆盖率数字 —— 这正是 +「覆盖率涨了 ≠ 缺口补上了」那条规矩的另一面。 + +#### `os.date` 形状矩阵:`*t` 是 `strcmp` 不是前缀 + +`OsLib` 是 0.952,而原有的 `FORMAT` 组只跑了 `!`(UTC)那一半。新增 337 行补另外 +半边:**本地时区**下的每个转换符、同一批转换符在五个时间点上的值(`0`、2038 边界、 +1900、`-1`、公元 1 年)、非法转换符的报错形状,以及 `os.time` / `os.difftime` / +`os.setlocale` / `os.getenv` 的边角。 + +咬出**一处真 bug**:`*t` 写成了「以 `*t` 开头」,而 C 是 `strcmp(s, "*t") == 0`。 +于是 `os.date("*t x")` 本该落到 strftime 那条路、`*`、`t`、空格、`x` 全当字面量返回 +字符串 `"*t x"`,本实现却返回了表。 + +修的时候带出第二个细节 —— 还是这套代码里到处都有的那个坑:**`strcmp` 见到 `\0` +就停**,所以 `os.date("*t\0x")` 在 C 里**仍然是取表**。光比 `f.length == 2` 不够, +得先截到第一个 NUL。 + +敏感性:把 `LUA_STRFTIMEOPTIONS` 里 `E` / `O` 两字符那组删掉红 38 行;`*t` 退回 +前缀匹配红 1 行;长度改用 `f.length`(不截 NUL)红 2 行。 + +两条有意不进矩阵:**`!%z`**(macOS 的 strftime 即使在 gmtime 下报的也是**本地** +偏移,glibc 报 `+0000`;本实现按 POSIX 给 `+0000`,只能跟其中一边对上)与 +**无参的 `os.date` / `os.time`**(墙上时钟)。 + +#### `name` / `namewhat` 网格:函数的名字是反推出来的,不是存下来的 + +函数的 `name` 不是函数自身的属性,而是**从调用点反推**出来的 +(`funcnamefromcall` → `funcnamefromcode` → `getobjname`):看调用者那条指令是 +`OP_GETFIELD` / `OP_GETTABUP` / `OP_GETUPVAL` / `OP_MOVE` / `OP_SELF` 里的哪一种, +再从常量表或寄存器里把名字抠出来。所以同一个函数在每个语法位置上 `name` 与 +`namewhat` 都不同 —— 这正是该铺矩阵的东西:77 行覆盖九种 `namewhat` +(`global` / `local` / `field` / `method` / `upvalue` / `metamethod` / +`for iterator` / `""` / `nil`)。 + +几个猜不出来的形状: + +* **`t[1]()` 的名字是 `"integer index"`,不是 `"?`。** 整数键在 `getobjname` 里 + 是单独一支;只有浮点键与布尔键才落到 `"?"`。 +* **`"?"` 的 `namewhat` 仍然是 `"field"`** —— 名字推不出来,调用方式推得出来, + 两者互不挂钩。 +* **metamethod 那一路的 `name` 是事件名去掉 `__`**(`index` / `newindex` / `add` / + `len` / `close` …),而 `__call` 走普通调用,给 `name="a1", namewhat="local"`。 +* 什么都不剩时,`name` 是 `nil` 而 `namewhat` 是**空串**,不是 `nil`。 + +第一跑逐字节一致(除脚本路径),所以敏感性量过:`"integer index"` 改成 `"?"` 红 5 行; +`namewhat` 的 `"field"` 改大小写红 1 行。 + +### `__gc` 不再往深里做(有意停止) + +本实现没有 `lgc.c`,`__gc` 是用 `Cleaner` 重建的,**什么时候跑取决于 JVM 的回收 +时机**,而参考实现那边取决于它自己的增量收集器。两边都不保证,逐字节比对在原理上 +就不稳定 —— 连「设元表之后才补 `__gc` 生不生效」这条,表与 userdata 实测结论都 +不一样。`GcTest` 里那几条能稳定复现的形态(只跑一次、错误静默、晚补不生效)就是 +全部目标:不再为它铺矩阵,在那里看到的「不一致」也不当作 bug 的证据。 + +### 还没补上的 + +语言与十个标准库本身没有缺项,上游套件里能逐字节比的那些也都比过了。剩下的是 +验证的广度:更多差分脚本、更大的 fuzz 语料。上游用例里没进表的只剩 +`all.lua` / `main.lua`(总驱动,要 `ltests.c` 编出来的 `T` 模块,还写 +`time.txt`)、`attrib.lua` / `files.lua` / `big.lua`(要 `libs/` 里那几个 C +模块,以及 `all.lua` 建的全局环境——参考实现自己单跑也是失败)、`heavy.lua` +(要跑好几分钟)、`bwcoercion.lua`(单跑没有输出)。 + +### 库函数 × 错误参数网格 + +`bad argument #1 to 'string.rep' (string expected, got boolean)` 是真实项目里天天 +见到的消息,产生它的分支全在 `lauxlib.c`。这张网格的函数表是运行时枚举出来 +的(`_G` 加九个库表),不是手写清单——手写清单漏掉的函数不会被任何用例发现,而 +「静默」正是 `warn` 整个函数没实现那段时间 828 个用例全绿的原因。108 个函数 × +{0,1,2} 个实参 × 10 种值 = **2396 行**,逐字节一致。 + +钉住的三处判定: + +* **缺参报 `no value`,不是 `nil`。** `luaL_typename` 对越界下标给 `no value`。 + 破坏它红 **212** 行。 +* **`lua_type` 对越界下标是 `LUA_TNONE`** 不是 `LUA_TNIL`,于是「nil 也算合法」 + 的参数不会因为缺参而静默通过:`setmetatable({})` 必须报 + `nil or table expected, got no value`。网格里只有 1 行照到它——比之前的 0 行强。 +* **方法形式调用要把参数编号减一**(`luaL_argerror` 里的 `arg--`),减完是 0 就 + 改报 `calling 'x' on bad self (...)`。直接调用永远走不到这两条,所以另加一组 + 字面生成的 `(""):f(v)` / `(1):f(v)`(377 行)——`OP_SELF` 只能由语法上的 + 方法调用产生,运行时拼不出来。敏感度 75 / 22。 + +两处归一化是刻意的:`math.atan` 与 `math.atan2` 是同一个 C 函数,`findfield` +反查到哪个取决于字符串哈希的 seed(每次启动都变);地址一律换成 `PTR`。黑名单里 +的函数是因副作用排除,不是因为难测:`print` / `io.write` 往 stdout 写、 +`os.date` / `os.time` / `os.clock` 读时钟、`math.random` 随机、 +`io.read` / `io.lines` 读 stdin、`debug.sethook` 装了钩子不卸、 +`debug.setmetatable` 改的是全局类型元表、`collectgarbage` 的 `step` / `count` +是记录在案的有意偏离。 + +同一片网格上还有第三层:合法调用返回什么。第一版成功时只打 `OK`,把返回值 +全丢了——而「返回值的类型」是独立的一类错:`math.abs(-1)` 返回整数 +`1` 而不是浮点 `1.0`,`tostring` 出来一个是 `1` 一个是 `1.0`。把 `math.abs` 的 +整数分支改成走浮点,错误参数网格 一行都不红,这一片红 6 行;反过来 +`math.type` 返回值的破坏在这一片红 2 行、在那一片红 0 行。2729 行输出里有 959 行 +是成功调用,覆盖 139 种不同的返回值形状。两个函数因返回值排除: +`os.tmpname` 随机;`debug.traceback` 的返回值含 `stack traceback:`,打出来会被 +驱动从那儿整段砍掉,等于让它之后的内容一行也不比。 + +### `debug.debug`:唯一一个会读 stdin 的函数 + +`ldblib.c: db_debug` 是个循环,铺这片网格之前它三条规矩全错: + +* 每次读之前先往 stderr 打 `"lua_debug> "`(没有换行)。原来整句都漏了, + 于是参考实现的输出里凭空多出 11 个字节; +* 是循环,一直读到 EOF 或某一行正好是 `"cont\n"`,中间每行都当 chunk 执行, + chunk 名是 `=(debug command)`。原来的 Java 版只读一行就返回; +* 命令失败时把 `luaL_tolstring(L, -1)` 打到 stderr,然后继续下一轮。 + +验这些需要 stdin,而差分驱动原本没有:它把参考实现的 stdin 定向到 `/dev/null`、 +`LuaState#in` 默认是空流,于是只能验到「打一个提示符就 EOF」这一种形状。新增的 +`Diff.assertMatchesWithStdin` 给两边喂同样的字节,把循环体、出错分支、`cont`、 +末尾没有换行的行、以及 `fgets` 那个 250 字节缓冲的切分(超长一行被切成多段, +每段各自当 chunk 跑)全部带进 CI。六个形状现在逐字节一致。 + +顺带记一条流顺序:C 的 `print` 走 `lua_writeline`,它每次都 +`fflush(stdout)`,所以 stdout 与 stderr 在合并捕获时严格按程序顺序交错, +不是「stdout 攒到最后」。 + +### `varinfo` 矩阵:错误消息里的名字是反推出来的,不是记下来的 + +`attempt to index a nil value (local 'x')` 那个括号尾巴出自 +`ldebug.c: varinfo`,它没有被存在任何地方。`varinfo` 先问「这个值是不 +是正在跑的闭包的某个 upvalue」,再问「它落在第几个寄存器」,最后把寄存器交给 +`getobjname`,由后者根据最后一次改写这个寄存器的指令去猜。网格 +(**112 行**)把所有 kind 都钉住:`local`、`upvalue`、`constant`、`global`、 +`field`、`method`、`(for iterator 'for iterator')`,外加两种推不出来的形状 +`field 'integer index'`(`OP_GETI`,所以 kind 是 `field`、而名字是字面串 +`integer index`)和 `field '?'`(键推不出来)。空后缀同样是答案的一部分: +`attempt to call a number value` 后面不能多出 `(constant '42')`—— +`kname` 只给字符串常量起名字,两行用例专门钉这条。 + +这片矩阵值钱的地方在于它「红不起来」这件事本身。前 21 条是按「条件赋值会让 +名字消失」这个直觉写的:`local x = nil; if c then x = nil end; return x.y`。 +21 条全部答 `(local 'x')`,把 `filterpc` 改成无条件的 `return pc`, +21 条一行都不红。原因在 `basicgetobjname`:它先调 `luaF_getlocalname`, +只要那个寄存器在出错那一点是活着的局部量,当场就返回 `"local"` —— +`findsetreg` 连同它的跳转目标过滤根本进不去。符号执行只负责 +临时寄存器。 + +真正能进去的是 `and` / `or`:`(c and t.a or t.b).z` 两条分支写的是同一个 +临时寄存器,而且都写在跳转目标之前,于是最后一条改写记录被判成「可能没 +执行」而作废,尾巴塌成空串。补上这几行之后,同一个破坏从 0 行红变成 +234 行。 + +敏感度(破坏一处再还原,数变红的行数):`filterpc` 234、`instack` 435、 +`upvalname` 的 `"?"` 211、`kname` 只认字符串 131、`OP_MOVE` 的 `b < a` 22、 +`OP_LOADNIL` 的寄存器区间 10、`"integer index"` 3、`isenv` 的 `global` 3、 +`method` 2。 + +有一条分支是如实记成不可达,而不是假装被覆盖了。给 `findsetreg` 插桩, +统计它落在哪些 opcode 上(矩阵 + 上游 33 个用例):`LOADNIL` 1080、 +`GETTABUP` 494、「没找到」326、`GETUPVAL` 199、`MOVE` 52……而 `TFORCALL` +是 **0**。那条分支(`change = reg >= a + 2`)从错误消息里观测不到: +`TFORCALL` 位于循环体末尾,它影响的那些寄存器在循环内都是活局部量,循环外又 +必然先被重写一遍才有人读。代码是从 C 逐字搬过来的,1032 行用例没有一行走到 +它——这是量出来的。 + +### 文件方法矩阵:六个方法 × 六个打开模式 × 每种参数 + +`IoLib.LuaFile` 是全项目覆盖率最低的类(0.758)。冲着它写的这张网格有九块、 +**1262 行**输出:六个文件方法(`flush lines read seek setvbuf write`)在六个打开 +模式上跑 21 种参数,再在**已关闭**的句柄上跑第二遍;`seek` 的**双参数**矩阵;写完 +立刻定位;`read` 的多格式与 `lines` 迭代;`write` 的多参数;目录句柄;**另开一个 +句柄**读同一个文件;`read("n")` 失败后再 `read(1)`;`popen` 的关闭。 + +其中两块是「因为最直观的那一块是瞎的」才加的: + +* `f:seek(-1)` 压根到不了偏移这段代码——第二个参数是 **option**,传数字只会得到 + `invalid option`。只有 `f:seek(w, off)` 才走到偏移,而它值 **46 行红**。 +* `seek` 汇报位置之前会 `flushW`,所以「`flush()` 到底有没有真把字节推出去」在这 + 儿看不出来。**另开一个句柄**去读才看得出来:**4 行红**。 + +咬出来两个真分歧: + +* **`f:read(-1)` 抛的是 `NegativeArraySizeException`**。C 写的是 + `size_t l = (size_t)luaL_checkinteger(L, n)`——负数按无符号解释成天文数字, + `luaL_prepbuffsize` 分配失败,`resizebox` 抛的是普通的 Lua 错误 + `"not enough memory"`(在 `pcall` 下就是 `false not enough memory`)。 + `new byte[(int)n]` 不是这句话的译文。现在 `read_chars` 分块读到「流干」或「够数」 + 为止——这本来就更像 `fread`,因为短读时 C 会继续读——只有大到任何系统都分配不出 + 来时才拒绝。拒绝的边界是平台相关的(参考实现在 macOS 上断在 `2^46` + 附近,有过量提交的系统上又不一样),所以故意不进差分表;确定性的输入只有 + 负数那个。 +* **成功的 `fseek` 会丢掉 `ungetc` 退回来的字节**。`f:read("n")` 在 `"three"` 上 + 失败时把 `'t'` 退了回去,接着 `f:seek("set",0)` 再 `f:read(3)`——参考给 + `"1 2"`,我们在 `seek` 里清掉它之前给的是 `"t1 "`。 + +实测灵敏度:`seek` 负位置不报 EINVAL **46**、`read` 不判目录 **整片崩**、 +`append` 不回末尾 5、`flush` 不 `flushW` 4、`seek` 不清 pushback 3、`seek` 目录 +基准 2、`write` 不特判 0 字节 1。 + +还有一个诚实的 0:**`tell` 里的 `+ wlen` 是 0** —— `f:seek` 先 `flushW` 再 +`tell`,那儿缓冲恒为空。覆盖上去了,那个分支仍然没被观测到。第八、九两块同理只补 +覆盖:去掉 `done >= len` 那条短路、或者不关 `popen` 的 `in`,矩阵照样全绿。它们 +的作用是让 JaCoCo 少说两句谎,不是多钉一条判定 —— `LuaFile` 里剩下没覆盖的多数是 +`IOException` 的 catch 块,那要故障注入,不是再堆脚本。 + +### `os.time` / `os.date` 网格:被参数网格排除掉的那两个函数 + +`os.time` 与 `os.date` 当初是**被「库函数 × 错误参数」网格拉黑的**——无参时读时钟。 +但带上显式参数它们完全是确定性的,于是那一整片没人看。这张网格 **1152 行**: +`os.time` 的七个字段 × 23 种值、缺字段的默认值(`hour` 默认 12, +`year`/`month`/`day` **没有**默认值)、`setallfields` 把归一后的字段回写进调用方 +那张表、`os.date` 的 60 种格式 × 15 个时间戳、`os.difftime`、`os.getenv`、 +`os.setlocale`。 + +咬出来一个真分歧:**`os.time{year=2147483647}` 给的是垃圾**。参考实现给 +`67767976201968000`,我们给 `-6019000092870206`。原因是 `GregorianCalendar` 存的是 +**毫秒**,这种量级 `getTimeInMillis()` 直接溢出、折回负数。现在改走 +`OsLib.civilToEpoch`(前推格里历的纯 `long` 算术),两边一致——而 +`67767976201968000` 正好等于 `86400 * days_from_civil` 减时区偏移,是**纯算术**, +两边 libc 都一样。 + +两处是量出来「不进表」的,不是猜的: + +* **1900 年以前**:Apple libc 的 `mktime` 拒绝(实测 `year=1900` 成、`1899` 不成), + glibc 不拒绝。两边各自推导谁也不算对,探针里这一档直接打 `SKIP`。 +* **`os.time(nil)` / `os.time()`**:走 `lua_isnoneornil`,读的是时钟。 + +实测灵敏度:转换符报错文案 54、`civilToEpoch` 的月份基准(`tmMon` 是 0 起)29、 +`isdst` 恒为 0 / `isdst=1` 不挪一小时 24、`setAllFields` 的字段名 22、退回 +`Calendar`(溢出)3。还有一个诚实的 0:**`os.date` 的 `MAX_SEC` 挡是 0**—— +`2^60` 在放宽前后都落在挡外,那一刀这条网格照不到。 + +--- + +## 从旧 fakescript 语言迁移 + +这是一次**破坏性重写**。旧 fakescript 语法整体移除:`package`、`include`、 +`struct`、`const`、`->` 式 `for`、`array()`、`map()`、`fake` 协程、`:=`、 +Java 绑定注解、以及性能剖析器。 + +既有的 fakescript 脚本需要改写成 Lua——而这正是重点:改完之后跑的是 Lua 5.4, +脚本可以在任意其他 Lua 5.4 实现之间来回移植。仍需要旧语言的用户请锁定到 +重写前的最后一个提交。 + +--- + +## 构建 + +```bash +./mvnw clean test # 需要 JDK 21+ +``` + +机器上没有 `lua5.4` 二进制时,差分测试会自己跳过(`Diff` 里一个 `assumeTrue` +守卫),覆盖率门禁跟着一起跳过——差分套件占了行覆盖的绝大部分,在它们全被跳过 +的情况下还跑门禁,只会因为一个不相干的理由一直红。要拿到真正的门禁,设 +`LUA54_BIN`,或者把参考实现编到 `target/lua54/src/lua`: + +```bash +LUA54_BIN=/path/to/lua5.4 ./mvnw clean test +``` + +CI 从源码编参考实现并导出 `LUA54_BIN` 与 `FKDUMP_BIN`,所以这只影响本地没有 +参考二进制的构建。 + +--- + +## 许可证 + +MIT diff --git a/README_EN.md b/README_EN.md deleted file mode 100644 index e15e665..0000000 --- a/README_EN.md +++ /dev/null @@ -1,150 +0,0 @@ -# fakejava - -[](https://github.com/esrrhs/fakejava) -[](https://github.com/esrrhs/fakejava) -[](https://github.com/esrrhs/fakejava) -[](https://github.com/esrrhs/fakejava/actions) - -Lightweight embedded scripting language - -## Brief introduction -**fakejava** is a lightweight embedded scripting language , using Java language, grammar lessons from lua, golang, erlang, based on jflex, bison generative grammar tree , compiled into byte code interpreted. -fakescript for C/C++ - -## Script feature -* The syntax is similar to lua -* All function -* Support array, map, unlimited nested -* Support generating routine like fake testfunc (param1), to achieve the effect of multithreading -* Support binding Java Static functions and Java class member functions -* With interpreter -* Support multi return value -* Support profile, can get the script running time of each function -* Support hot update -* Support Int64 -* Support const definitions -* Support Package -* Support struct -* Support byte code optimize -* Support global map - - - -## Sample - -``` - - --- Current package name -package mypackage.test - --- include file -include "common.fk" - --- struct define -struct teststruct - sample_a - sample_b - sample_c -end - --- const define -const hellostring = "hello" -const helloint = 1234 -const hellomap = {1 : "a" 2 : "b" 3 : [1 2 3]} - --- func1 comment -func myfunc1(arg1, arg2) - - -- Java static function calls and Java class member functions - arg3 := cfunc1(helloint) + arg2:memfunc1(arg1) - - -- Branch - if arg1 < arg2 then - -- create routine - fake myfunc2(arg1, arg2) - elseif arg1 == arg2 then - print("elseif") - else - print("else") - end - - -- for loop - for var i = 0, i < arg2, i++ then - print("i = ", i) - end - - -- array use - var a = array() - a[1] = 3 - - -- map use - var b = map() - b[a] = 1 - b[1] = a - - -- Int64 - var uid = 1241515236123614u - log("uid = ", uid) - - -- sub func call - var ret1, var ret2 = myfunc2() - - -- other package call - ret1 = otherpackage.test.myfunc1(arg1, arg2) - - -- struct use - var tt = teststruct() - tt->sample_a = 1 - tt->sample_b = teststruct() - tt->sample_b->sample_a = 10 - - -- switch branch - switch arg1 - case 1 then - print("1") - case "a" then - print("a") - default - print("default") - end - - -- multi return value - return arg1, arg3 - -end -``` - -## Java Sample - -``` -// create instance -fake f = fk.newfake(null); -// regist all func marked with @fakescript in package -fk.reg(f, "com.test"); -// parse script file -fk.parse(f, "test.fk"); -// run script func myfunc1, put in two param 1 and 2 -double ret = (double)fk.run(f, "myfunc1", 1, 2); - -``` - -## How to use -#### Maven -``` - - com.github.esrrhs - fakescript-java - 1.0.12 - - -``` - -## Debugging environment -* IDE - -![image](img/ide.png) - -* Command-line - -![image](img/debug.png) diff --git a/_config.yml b/_config.yml index c741881..c419263 100644 --- a/_config.yml +++ b/_config.yml @@ -1 +1 @@ -theme: jekyll-theme-slate \ No newline at end of file +theme: jekyll-theme-cayman \ No newline at end of file diff --git a/build.sh b/build.sh deleted file mode 100644 index 3981623..0000000 --- a/build.sh +++ /dev/null @@ -1,10 +0,0 @@ -#! /bin/sh - -cd jflexbison -jflex jflex.flex -mv Yylex.java ../src/main/java/com/github/esrrhs/fakescript/Yylex.java -f - -bison -L JAVA YYParser.y -mv YYParser.java ../src/main/java/com/github/esrrhs/fakescript/YYParser.java -f - - diff --git a/deploy.bat b/deploy.bat index fb4b81e..3bd3af3 100644 --- a/deploy.bat +++ b/deploy.bat @@ -1,4 +1,4 @@ pause -start https://oss.sonatype.org/#stagingRepositories -mvn clean deploy -P sonatype-oss-release -pause \ No newline at end of file +start https://central.sonatype.com/publishing +mvn clean deploy -P release +pause diff --git a/img/debug.png b/img/debug.png deleted file mode 100644 index 46b082c..0000000 Binary files a/img/debug.png and /dev/null differ diff --git a/img/donate.png b/img/donate.png deleted file mode 100644 index 7ee8460..0000000 Binary files a/img/donate.png and /dev/null differ diff --git a/img/ide.png b/img/ide.png deleted file mode 100644 index 539c180..0000000 Binary files a/img/ide.png and /dev/null differ diff --git a/jflexbison/YYParser.y b/jflexbison/YYParser.y deleted file mode 100644 index 550f496..0000000 --- a/jflexbison/YYParser.y +++ /dev/null @@ -1,1671 +0,0 @@ -%{ -package com.github.esrrhs.fakescript; - -import com.github.esrrhs.fakescript.syntree.*; -import java.io.*; - -%} - -/* Terminals (tokens returned by the scanner). */ -%token VAR_BEGIN; -%token RETURN; -%token BREAK; -%token FUNC; -%token WHILE; -%token FTRUE; -%token FFALSE; -%token IF; -%token THEN; -%token ELSE; -%token END; -%token STRING_DEFINITION; -%token IDENTIFIER; -%token NUMBER; -%token SINGLE_LINE_COMMENT; -%token DIVIDE_MOD; -%token ARG_SPLITTER; -%token PLUS; -%token MINUS; -%token DIVIDE; -%token MULTIPLY; -%token ASSIGN; -%token MORE; -%token LESS; -%token MORE_OR_EQUAL; -%token LESS_OR_EQUAL; -%token EQUAL; -%token NOT_EQUAL; -%token OPEN_BRACKET; -%token CLOSE_BRACKET; -%token AND; -%token OR; -%token FKFLOAT; -%token PLUS_ASSIGN; -%token MINUS_ASSIGN; -%token DIVIDE_ASSIGN; -%token MULTIPLY_ASSIGN; -%token DIVIDE_MOD_ASSIGN; -%token COLON; -%token FOR; -%token INC; -%token FAKE; -%token FKUUID; -%token OPEN_SQUARE_BRACKET; -%token CLOSE_SQUARE_BRACKET; -%token FCONST; -%token PACKAGE; -%token INCLUDE; -%token IDENTIFIER_DOT; -%token IDENTIFIER_POINTER; -%token STRUCT; -%token IS; -%token NOT; -%token CONTINUE; -%token YIELD; -%token SLEEP; -%token SWITCH; -%token CASE; -%token DEFAULT; -%token NEW_ASSIGN; -%token ELSEIF; -%token RIGHT_POINTER; -%token STRING_CAT; -%token OPEN_BIG_BRACKET; -%token CLOSE_BIG_BRACKET; -%token NULL; - -/* Precedences */ -%left PLUS; -%left MINUS; -%left DIVIDE; -%left MULTIPLY; -%left DIVIDE_MOD; -%left STRING_CAT; - -%expect 55 - -%% - -/* Top level rules */ -program : package_head - include_head - struct_head - const_head - body - ; - -body : - /* empty */ - { - } - | - function_declaration - | - body function_declaration - ; - -function_declaration : - FUNC IDENTIFIER OPEN_BRACKET function_declaration_arguments CLOSE_BRACKET block END - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FUNC IDENTIFIER OPEN_BRACKET function_declaration_arguments CLOSE_BRACKET block END"); - func_desc_node p = ((Yylex)yylexer).new_node(func_desc_node.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_funcname = ((ParserVal)((ParserVal)$2)).sval; - p.m_arglist = (func_desc_arglist_node)((ParserVal)$4).obj; - p.m_block = (block_node)((ParserVal)$6).obj; - p.m_endline = ((Yylex)yylexer).get_mybison().get_jflex().get_line(); - ((Yylex)yylexer).get_mybison().add_func_desc(p); - } - | - FUNC IDENTIFIER OPEN_BRACKET function_declaration_arguments CLOSE_BRACKET END - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FUNC IDENTIFIER OPEN_BRACKET function_declaration_arguments CLOSE_BRACKET END"); - func_desc_node p = ((Yylex)yylexer).new_node(func_desc_node.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_funcname = ((ParserVal)((ParserVal)$2)).sval; - p.m_arglist = (func_desc_arglist_node)((ParserVal)$4).obj; - p.m_endline = ((Yylex)yylexer).get_mybison().get_jflex().get_line(); - ((Yylex)yylexer).get_mybison().add_func_desc(p); - } - ; - -function_declaration_arguments : - /* empty */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: empty"); - } - | - function_declaration_arguments ARG_SPLITTER arg - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: function_declaration_arguments ARG_SPLITTER arg "); - func_desc_arglist_node p = (func_desc_arglist_node)((ParserVal)$1).obj; - p.add_arg((syntree_node)((ParserVal)$3).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - arg - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: arg"); - func_desc_arglist_node p = ((Yylex)yylexer).new_node(func_desc_arglist_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.add_arg((syntree_node)((ParserVal)$1).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -arg : - IDENTIFIER - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: IDENTIFIER"); - identifier_node p = ((Yylex)yylexer).new_node(identifier_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_str = ((ParserVal)((ParserVal)$1)).sval; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - - -function_call : - IDENTIFIER OPEN_BRACKET function_call_arguments CLOSE_BRACKET - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: IDENTIFIER OPEN_BRACKET function_call_arguments CLOSE_BRACKET "); - function_call_node p = ((Yylex)yylexer).new_node(function_call_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_fuc = ((ParserVal)((ParserVal)$1)).sval; - p.m_arglist = (function_call_arglist_node)((ParserVal)$3).obj; - p.m_fakecall = false; - p.m_classmem_call = false; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - IDENTIFIER_DOT OPEN_BRACKET function_call_arguments CLOSE_BRACKET - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: IDENTIFIER_DOT OPEN_BRACKET function_call_arguments CLOSE_BRACKET "); - function_call_node p = ((Yylex)yylexer).new_node(function_call_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_fuc = ((ParserVal)((ParserVal)$1)).sval; - p.m_arglist = (function_call_arglist_node)((ParserVal)$3).obj; - p.m_fakecall = false; - p.m_classmem_call = false; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - function_call OPEN_BRACKET function_call_arguments CLOSE_BRACKET - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: function_call OPEN_BRACKET function_call_arguments CLOSE_BRACKET "); - function_call_node p = ((Yylex)yylexer).new_node(function_call_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_prefuc = (syntree_node)((ParserVal)$1).obj; - p.m_arglist = (function_call_arglist_node)((ParserVal)$3).obj; - p.m_fakecall = false; - p.m_classmem_call = false; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - variable COLON IDENTIFIER OPEN_BRACKET function_call_arguments CLOSE_BRACKET - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable COLON IDENTIFIER OPEN_BRACKET function_call_arguments CLOSE_BRACKET "); - function_call_node p = ((Yylex)yylexer).new_node(function_call_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_fuc = ((ParserVal)((ParserVal)$3)).sval; - p.m_arglist = (function_call_arglist_node)((ParserVal)$5).obj; - if (p.m_arglist == null) - { - p.m_arglist = ((Yylex)yylexer).new_node(function_call_arglist_node.class, ((ParserVal)((ParserVal)$1)).ival); - } - p.m_arglist.add_arg((syntree_node)((ParserVal)$1).obj); - p.m_fakecall = false; - p.m_classmem_call = true; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - function_call COLON IDENTIFIER OPEN_BRACKET function_call_arguments CLOSE_BRACKET - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: function_call COLON IDENTIFIER OPEN_BRACKET function_call_arguments CLOSE_BRACKET "); - function_call_node p = ((Yylex)yylexer).new_node(function_call_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_fuc = ((ParserVal)((ParserVal)$3)).sval; - p.m_arglist = (function_call_arglist_node)((ParserVal)$5).obj; - if (p.m_arglist == null) - { - p.m_arglist = ((Yylex)yylexer).new_node(function_call_arglist_node.class, ((ParserVal)((ParserVal)$1)).ival); - } - p.m_arglist.add_arg((syntree_node)((ParserVal)$1).obj); - p.m_fakecall = false; - p.m_classmem_call = true; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -function_call_arguments : - /* empty */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: empty "); - } - | - function_call_arguments ARG_SPLITTER arg_expr - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: function_call_arguments ARG_SPLITTER arg_expr "); - function_call_arglist_node p = (function_call_arglist_node)((ParserVal)$1).obj; - p.add_arg((syntree_node)((ParserVal)$3).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - arg_expr - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: arg_expr "); - function_call_arglist_node p = ((Yylex)yylexer).new_node(function_call_arglist_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.add_arg((syntree_node)((ParserVal)$1).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -arg_expr : - expr_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: expr_value"); - $$ = ((ParserVal)((ParserVal)$1)); - } - ; - -/* function declaration end */ - -block : - block stmt - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: block stmt "); - block_node p = (block_node)((ParserVal)$1).obj; - p.add_stmt((syntree_node)((ParserVal)$2).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - stmt - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: stmt"); - block_node p = ((Yylex)yylexer).new_node(block_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.add_stmt((syntree_node)((ParserVal)$1).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -stmt : - while_stmt - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: while_stmt"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - if_stmt - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: if_stmt"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - return_stmt - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: return_stmt"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - assign_stmt - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: assign_stmt"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - multi_assign_stmt - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: multi_assign_stmt"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - break - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: break"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - continue - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: continue"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - expr - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: expr"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - math_assign_stmt - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: math_assign_stmt"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - for_stmt - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: for_stmt"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - for_loop_stmt - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: for_loop_stmt"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - fake_call_stmt - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: fake_call_stmt"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - sleep - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: sleep_stmt"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - yield - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: yield_stmt"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - switch_stmt - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: switch_stmt"); - $$ = ((ParserVal)((ParserVal)$1)); - } - ; - -fake_call_stmt : - FAKE function_call - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FAKE function_call"); - function_call_node p = (function_call_node)((ParserVal)$2).obj; - p.m_fakecall = true; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -for_stmt : - FOR block ARG_SPLITTER cmp ARG_SPLITTER block THEN block END - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FOR block ARG_SPLITTER cmp ARG_SPLITTER block THEN block END"); - for_stmt p = ((Yylex)yylexer).new_node(for_stmt.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_cmp = (cmp_stmt)((ParserVal)$4).obj; - p.m_beginblock = (block_node)((ParserVal)$2).obj; - p.m_endblock = (block_node)((ParserVal)$6).obj; - p.m_block = (block_node)((ParserVal)$8).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - FOR block ARG_SPLITTER cmp ARG_SPLITTER block THEN END - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FOR block ARG_SPLITTER cmp ARG_SPLITTER block THEN END"); - for_stmt p = ((Yylex)yylexer).new_node(for_stmt.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_cmp = (cmp_stmt)((ParserVal)$4).obj; - p.m_beginblock = (block_node)((ParserVal)$2).obj; - p.m_endblock = (block_node)((ParserVal)$6).obj; - p.m_block = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -for_loop_stmt : - FOR var ASSIGN assign_value RIGHT_POINTER cmp_value ARG_SPLITTER expr_value THEN block END - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FOR var ASSIGN assign_value RIGHT_POINTER cmp_value ARG_SPLITTER expr_value THEN block END"); - for_loop_stmt p = ((Yylex)yylexer).new_node(for_loop_stmt.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_var = (syntree_node)((ParserVal)$2).obj; - p.m_begin = (syntree_node)((ParserVal)$4).obj; - p.m_end = (syntree_node)((ParserVal)$6).obj; - p.m_add = (syntree_node)((ParserVal)$8).obj; - p.m_block = (block_node)((ParserVal)$10).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - FOR var ASSIGN assign_value RIGHT_POINTER cmp_value ARG_SPLITTER expr_value THEN END - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FOR var ASSIGN assign_value RIGHT_POINTER cmp_value ARG_SPLITTER expr_value THEN END"); - for_loop_stmt p = ((Yylex)yylexer).new_node(for_loop_stmt.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_var = (syntree_node)((ParserVal)$2).obj; - p.m_begin = (syntree_node)((ParserVal)$4).obj; - p.m_end = (syntree_node)((ParserVal)$6).obj; - p.m_add = (syntree_node)((ParserVal)$8).obj; - p.m_block = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -while_stmt : - WHILE cmp THEN block END - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: WHILE cmp THEN block END "); - while_stmt p = ((Yylex)yylexer).new_node(while_stmt.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_cmp = (cmp_stmt)((ParserVal)$2).obj; - p.m_block = (block_node)((ParserVal)$4).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - WHILE cmp THEN END - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: WHILE cmp THEN END "); - while_stmt p = ((Yylex)yylexer).new_node(while_stmt.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_cmp = (cmp_stmt)((ParserVal)$2).obj; - p.m_block = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -if_stmt : - IF cmp THEN block elseif_stmt_list else_stmt END - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: IF cmp THEN block elseif_stmt_list else_stmt END"); - if_stmt p = ((Yylex)yylexer).new_node(if_stmt.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_cmp = (cmp_stmt)((ParserVal)$2).obj; - p.m_block = (block_node)((ParserVal)$4).obj; - p.m_elseifs = (elseif_stmt_list)((ParserVal)$5).obj; - p.m_elses = (else_stmt)((ParserVal)$6).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - IF cmp THEN elseif_stmt_list else_stmt END - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: IF cmp THEN elseif_stmt_list else_stmt END"); - if_stmt p = ((Yylex)yylexer).new_node(if_stmt.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_cmp = (cmp_stmt)((ParserVal)$2).obj; - p.m_block = null; - p.m_elseifs = (elseif_stmt_list)((ParserVal)$4).obj; - p.m_elses = (else_stmt)((ParserVal)$5).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -elseif_stmt_list : - /* empty */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: empty"); - - ParserVal ret = new ParserVal(null); - ret.ival = ((Yylex)yylexer).get_mybison().get_jflex().get_line(); - $$ = ret; - } - | - elseif_stmt_list elseif_stmt - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: elseif_stmt_list elseif_stmt"); - elseif_stmt_list p = (elseif_stmt_list)((ParserVal)$1).obj; - p.add_stmt((syntree_node)((ParserVal)$2).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - elseif_stmt - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: elseif_stmt"); - elseif_stmt_list p = ((Yylex)yylexer).new_node(elseif_stmt_list.class, ((ParserVal)((ParserVal)$1)).ival); - p.add_stmt((syntree_node)((ParserVal)$1).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -elseif_stmt : - ELSEIF cmp THEN block - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: ELSEIF cmp THEN block"); - elseif_stmt p = ((Yylex)yylexer).new_node(elseif_stmt.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_cmp = (cmp_stmt)((ParserVal)$2).obj; - p.m_block = (syntree_node)((ParserVal)$4).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - ELSEIF cmp THEN - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: ELSEIF cmp THEN"); - elseif_stmt p = ((Yylex)yylexer).new_node(elseif_stmt.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_cmp = (cmp_stmt)((ParserVal)$2).obj; - p.m_block = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -else_stmt : - /*empty*/ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: empty"); - - ParserVal ret = new ParserVal(null); - ret.ival = ((Yylex)yylexer).get_mybison().get_jflex().get_line(); - $$ = ret; - } - | - ELSE block - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: ELSE block"); - else_stmt p = ((Yylex)yylexer).new_node(else_stmt.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_block = (block_node)((ParserVal)$2).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - ELSE - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: ELSE"); - else_stmt p = ((Yylex)yylexer).new_node(else_stmt.class, ((Yylex)yylexer).get_mybison().get_jflex().get_line()); - p.m_block = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -cmp : - OPEN_BRACKET cmp CLOSE_BRACKET - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: OPEN_BRACKET cmp CLOSE_BRACKET"); - $$ = ((ParserVal)((ParserVal)$2)); - } - | - cmp AND cmp - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: cmp AND cmp"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_cmp = "&&"; - p.m_left = (syntree_node)((ParserVal)$1).obj; - p.m_right = (syntree_node)((ParserVal)$3).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - cmp OR cmp - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: cmp OR cmp"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_cmp = "||"; - p.m_left = (syntree_node)((ParserVal)$1).obj; - p.m_right = (syntree_node)((ParserVal)$3).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - cmp_value LESS cmp_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: cmp_value LESS cmp_value"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_cmp = "<"; - p.m_left = (syntree_node)((ParserVal)$1).obj; - p.m_right = (syntree_node)((ParserVal)$3).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - cmp_value MORE cmp_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: cmp_value MORE cmp_value"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_cmp = ">"; - p.m_left = (syntree_node)((ParserVal)$1).obj; - p.m_right = (syntree_node)((ParserVal)$3).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - cmp_value EQUAL cmp_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: cmp_value EQUAL cmp_value"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_cmp = "=="; - p.m_left = (syntree_node)((ParserVal)$1).obj; - p.m_right = (syntree_node)((ParserVal)$3).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - cmp_value MORE_OR_EQUAL cmp_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: cmp_value MORE_OR_EQUAL cmp_value"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_cmp = ">="; - p.m_left = (syntree_node)((ParserVal)$1).obj; - p.m_right = (syntree_node)((ParserVal)$3).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - cmp_value LESS_OR_EQUAL cmp_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: cmp_value LESS_OR_EQUAL cmp_value"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_cmp = "<="; - p.m_left = (syntree_node)((ParserVal)$1).obj; - p.m_right = (syntree_node)((ParserVal)$3).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - cmp_value NOT_EQUAL cmp_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: cmp_value NOT_EQUAL cmp_value"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_cmp = "!="; - p.m_left = (syntree_node)((ParserVal)$1).obj; - p.m_right = (syntree_node)((ParserVal)$3).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - FTRUE - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FTRUE"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((Yylex)yylexer).get_mybison().get_jflex().get_line()); - p.m_cmp = "true"; - p.m_left = null; - p.m_right = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - FFALSE - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FFALSE"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((Yylex)yylexer).get_mybison().get_jflex().get_line()); - p.m_cmp = "false"; - p.m_left = null; - p.m_right = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - IS cmp_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: IS cmp_value"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_cmp = "is"; - p.m_left = (syntree_node)((ParserVal)$2).obj; - p.m_right = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - NOT cmp_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: NOT cmp_value"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_cmp = "not"; - p.m_left = (syntree_node)((ParserVal)$2).obj; - p.m_right = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -cmp_value : - explicit_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: explicit_value"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - variable - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - expr - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: expr"); - $$ = ((ParserVal)((ParserVal)$1)); - } - ; - -return_stmt : - RETURN return_value_list - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: RETURN return_value_list"); - return_stmt p = ((Yylex)yylexer).new_node(return_stmt.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_returnlist = (return_value_list_node)((ParserVal)$2).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - RETURN - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: RETURN"); - return_stmt p = ((Yylex)yylexer).new_node(return_stmt.class, ((Yylex)yylexer).get_mybison().get_jflex().get_line()); - p.m_returnlist = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -return_value_list : - return_value_list ARG_SPLITTER return_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: return_value_list ARG_SPLITTER return_value"); - return_value_list_node p = (return_value_list_node)((ParserVal)$1).obj; - p.add_arg((syntree_node)((ParserVal)$3).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - return_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: return_value"); - return_value_list_node p = ((Yylex)yylexer).new_node(return_value_list_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.add_arg((syntree_node)((ParserVal)$1).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -return_value : - explicit_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: explicit_value"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - variable - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - expr - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: expr"); - $$ = ((ParserVal)((ParserVal)$1)); - } - ; - -assign_stmt : - var ASSIGN assign_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: var ASSIGN assign_value"); - assign_stmt p = ((Yylex)yylexer).new_node(assign_stmt.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_var = (syntree_node)((ParserVal)$1).obj; - p.m_value = (syntree_node)((ParserVal)$3).obj; - p.m_isnew = false; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - var NEW_ASSIGN assign_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: var NEW_ASSIGN assign_value"); - assign_stmt p = ((Yylex)yylexer).new_node(assign_stmt.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_var = (syntree_node)((ParserVal)$1).obj; - p.m_value = (syntree_node)((ParserVal)$3).obj; - p.m_isnew = true; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -multi_assign_stmt : - var_list ASSIGN function_call - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: var_list ASSIGN function_call"); - multi_assign_stmt p = ((Yylex)yylexer).new_node(multi_assign_stmt.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_varlist = (var_list_node)((ParserVal)$1).obj; - p.m_value = (syntree_node)((ParserVal)$3).obj; - p.m_isnew = false; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - var_list NEW_ASSIGN function_call - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: var_list NEW_ASSIGN function_call"); - multi_assign_stmt p = ((Yylex)yylexer).new_node(multi_assign_stmt.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_varlist = (var_list_node)((ParserVal)$1).obj; - p.m_value = (syntree_node)((ParserVal)$3).obj; - p.m_isnew = true; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -var_list : - var_list ARG_SPLITTER var - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: var_list ARG_SPLITTER var"); - var_list_node p = (var_list_node)((ParserVal)$1).obj; - p.add_arg((syntree_node)((ParserVal)$3).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - var - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: var"); - var_list_node p = ((Yylex)yylexer).new_node(var_list_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.add_arg((syntree_node)((ParserVal)$1).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -assign_value : - explicit_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: explicit_value"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - variable - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - expr - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: expr"); - $$ = ((ParserVal)((ParserVal)$1)); - } - ; - -math_assign_stmt : - variable PLUS_ASSIGN assign_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable PLUS_ASSIGN assign_value"); - math_assign_stmt p = ((Yylex)yylexer).new_node(math_assign_stmt.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_var = (syntree_node)((ParserVal)$1).obj; - p.m_oper = "+="; - p.m_value = (syntree_node)((ParserVal)$3).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - variable MINUS_ASSIGN assign_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable MINUS_ASSIGN assign_value"); - math_assign_stmt p = ((Yylex)yylexer).new_node(math_assign_stmt.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_var = (syntree_node)((ParserVal)$1).obj; - p.m_oper = "-="; - p.m_value = (syntree_node)((ParserVal)$3).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - variable DIVIDE_ASSIGN assign_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable DIVIDE_ASSIGN assign_value"); - math_assign_stmt p = ((Yylex)yylexer).new_node(math_assign_stmt.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_var = (syntree_node)((ParserVal)$1).obj; - p.m_oper = "/="; - p.m_value = (syntree_node)((ParserVal)$3).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - variable MULTIPLY_ASSIGN assign_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable MULTIPLY_ASSIGN assign_value"); - math_assign_stmt p = ((Yylex)yylexer).new_node(math_assign_stmt.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_var = (syntree_node)((ParserVal)$1).obj; - p.m_oper = "*="; - p.m_value = (syntree_node)((ParserVal)$3).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - variable DIVIDE_MOD_ASSIGN assign_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable DIVIDE_MOD_ASSIGN assign_value"); - math_assign_stmt p = ((Yylex)yylexer).new_node(math_assign_stmt.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_var = (syntree_node)((ParserVal)$1).obj; - p.m_oper = "%="; - p.m_value = (syntree_node)((ParserVal)$3).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - variable INC - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable INC"); - explicit_value_node pp = ((Yylex)yylexer).new_node(explicit_value_node.class, ((ParserVal)((ParserVal)$1)).ival); - pp.m_str = "1"; - pp.m_type = explicit_value_type.EVT_NUM; - - math_assign_stmt p = ((Yylex)yylexer).new_node(math_assign_stmt.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_var = (syntree_node)((ParserVal)$1).obj; - p.m_oper = "+="; - p.m_value = pp; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -var : - VAR_BEGIN IDENTIFIER - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: VAR_BEGIN IDENTIFIER"); - var_node p = ((Yylex)yylexer).new_node(var_node.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_str = ((ParserVal)((ParserVal)$2)).sval; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - variable - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable"); - $$ = ((ParserVal)((ParserVal)$1)); - } - ; - -variable : - IDENTIFIER - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: IDENTIFIER"); - variable_node p = ((Yylex)yylexer).new_node(variable_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_str = ((ParserVal)((ParserVal)$1)).sval; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - IDENTIFIER OPEN_SQUARE_BRACKET expr_value CLOSE_SQUARE_BRACKET - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: IDENTIFIER OPEN_SQUARE_BRACKET expr_value CLOSE_SQUARE_BRACKET"); - container_get_node p = ((Yylex)yylexer).new_node(container_get_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_container = ((ParserVal)((ParserVal)$1)).sval; - p.m_key = (syntree_node)((ParserVal)$3).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - IDENTIFIER_POINTER - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: IDENTIFIER_POINTER"); - struct_pointer_node p = ((Yylex)yylexer).new_node(struct_pointer_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_str = ((ParserVal)((ParserVal)$1)).sval; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - IDENTIFIER_DOT - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: IDENTIFIER_DOT"); - variable_node p = ((Yylex)yylexer).new_node(variable_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_str = ((ParserVal)((ParserVal)$1)).sval; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -expr : - OPEN_BRACKET expr CLOSE_BRACKET - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: OPEN_BRACKET expr CLOSE_BRACKET"); - $$ = ((ParserVal)((ParserVal)$2)); - } - | - function_call - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: function_call"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - math_expr - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: math_expr"); - $$ = ((ParserVal)((ParserVal)$1)); - } - ; - -math_expr : - OPEN_BRACKET math_expr CLOSE_BRACKET - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: OPEN_BRACKET math_expr CLOSE_BRACKET"); - $$ = ((ParserVal)((ParserVal)$2)); - } - | - expr_value PLUS expr_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: expr_value PLUS expr_value"); - math_expr_node p = ((Yylex)yylexer).new_node(math_expr_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_oper = "+"; - p.m_left = (syntree_node)((ParserVal)$1).obj; - p.m_right = (syntree_node)((ParserVal)$3).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - expr_value MINUS expr_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: expr_value MINUS expr_value"); - math_expr_node p = ((Yylex)yylexer).new_node(math_expr_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_oper = "-"; - p.m_left = (syntree_node)((ParserVal)$1).obj; - p.m_right = (syntree_node)((ParserVal)$3).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - expr_value MULTIPLY expr_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: expr_value MULTIPLY expr_value"); - math_expr_node p = ((Yylex)yylexer).new_node(math_expr_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_oper = "*"; - p.m_left = (syntree_node)((ParserVal)$1).obj; - p.m_right = (syntree_node)((ParserVal)$3).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - expr_value DIVIDE expr_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: expr_value DIVIDE expr_value"); - math_expr_node p = ((Yylex)yylexer).new_node(math_expr_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_oper = "/"; - p.m_left = (syntree_node)((ParserVal)$1).obj; - p.m_right = (syntree_node)((ParserVal)$3).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - expr_value DIVIDE_MOD expr_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: expr_value DIVIDE_MOD expr_value"); - math_expr_node p = ((Yylex)yylexer).new_node(math_expr_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_oper = "%"; - p.m_left = (syntree_node)((ParserVal)$1).obj; - p.m_right = (syntree_node)((ParserVal)$3).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - expr_value STRING_CAT expr_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: expr_value STRING_CAT expr_value"); - math_expr_node p = ((Yylex)yylexer).new_node(math_expr_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_oper = ".."; - p.m_left = (syntree_node)((ParserVal)$1).obj; - p.m_right = (syntree_node)((ParserVal)$3).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -expr_value : - math_expr - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: math_expr"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - explicit_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: explicit_value"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - function_call - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: function_call"); - $$ = ((ParserVal)((ParserVal)$1)); - } - | - variable - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable"); - $$ = ((ParserVal)((ParserVal)$1)); - } - ; - -break : - BREAK - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: BREAK"); - break_stmt p = ((Yylex)yylexer).new_node(break_stmt.class, ((Yylex)yylexer).get_mybison().get_jflex().get_line()); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -continue : - CONTINUE - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: CONTINUE"); - continue_stmt p = ((Yylex)yylexer).new_node(continue_stmt.class, ((Yylex)yylexer).get_mybison().get_jflex().get_line()); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -sleep : - SLEEP expr_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: SLEEP"); - sleep_stmt p = ((Yylex)yylexer).new_node(sleep_stmt.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_time = (syntree_node)((ParserVal)$2).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -yield : - YIELD expr_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: YIELD"); - yield_stmt p = ((Yylex)yylexer).new_node(yield_stmt.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_time = (syntree_node)((ParserVal)$2).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -switch_stmt : - SWITCH cmp_value switch_case_list DEFAULT block END - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: SWITCH cmp_value switch_case_list DEFAULT block END"); - switch_stmt p = ((Yylex)yylexer).new_node(switch_stmt.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_cmp = (syntree_node)((ParserVal)$2).obj; - p.m_caselist = (syntree_node)((ParserVal)$3).obj; - p.m_def = (syntree_node)((ParserVal)$5).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - SWITCH cmp_value switch_case_list DEFAULT END - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: SWITCH cmp_value switch_case_list DEFAULT END"); - switch_stmt p = ((Yylex)yylexer).new_node(switch_stmt.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_cmp = (syntree_node)((ParserVal)$2).obj; - p.m_caselist = (syntree_node)((ParserVal)$3).obj; - p.m_def = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -switch_case_list : - switch_case_define - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: switch_case_define"); - switch_caselist_node p = ((Yylex)yylexer).new_node(switch_caselist_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.add_case((syntree_node)((ParserVal)$1).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - switch_case_list switch_case_define - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: switch_case_list switch_case_define"); - switch_caselist_node p = (switch_caselist_node)((ParserVal)$1).obj; - p.add_case((syntree_node)((ParserVal)$2).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -switch_case_define : - CASE cmp_value THEN block - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: CASE cmp_value THEN block"); - switch_case_node p = ((Yylex)yylexer).new_node(switch_case_node.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_cmp = (syntree_node)((ParserVal)$2).obj; - p.m_block = (syntree_node)((ParserVal)$4).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - CASE cmp_value THEN - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: CASE cmp_value THEN"); - switch_case_node p = ((Yylex)yylexer).new_node(switch_case_node.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_cmp = (syntree_node)((ParserVal)$2).obj; - p.m_block = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -package_head : - /* empty */ - { - } - | - PACKAGE IDENTIFIER - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: PACKAGE IDENTIFIER "); - ((Yylex)yylexer).get_mybison().set_package(((ParserVal)$2).sval); - } - | - PACKAGE IDENTIFIER_DOT - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: PACKAGE IDENTIFIER_DOT "); - ((Yylex)yylexer).get_mybison().set_package(((ParserVal)$2).sval); - } - ; - -include_head : - /* empty */ - { - } - | - include_define - | - include_head include_define - ; - -include_define : - INCLUDE STRING_DEFINITION - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: INCLUDE STRING_DEFINITION "); - ((Yylex)yylexer).get_mybison().add_include(((ParserVal)$2).sval); - } - ; - -struct_head : - /* empty */ - { - } - | - struct_define - | - struct_head struct_define - ; - -struct_define : - STRUCT IDENTIFIER struct_mem_declaration END - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: STRUCT IDENTIFIER struct_mem_declaration END "); - ((Yylex)yylexer).get_mybison().add_struct_desc(((ParserVal)$2).sval); - } - ; - -struct_mem_declaration : - struct_mem_declaration IDENTIFIER - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: struct_mem_declaration IDENTIFIER "); - } - | - IDENTIFIER - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: IDENTIFIER "); - } - ; - -const_head : - /* empty */ - { - } - | - const_define - | - const_head const_define - ; - -const_define : - FCONST IDENTIFIER ASSIGN explicit_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FCONST IDENTIFIER ASSIGN explicit_value "); - ((Yylex)yylexer).get_mybison().add_const_desc(((ParserVal)$2).sval, (syntree_node)((ParserVal)$4).obj); - } - ; - -explicit_value : - NULL - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: NULL "); - explicit_value_node p = ((Yylex)yylexer).new_node(explicit_value_node.class, ((Yylex)yylexer).get_mybison().get_jflex().get_line() + 1); - p.m_type = explicit_value_type.EVT_NULL; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - FTRUE - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FTRUE "); - explicit_value_node p = ((Yylex)yylexer).new_node(explicit_value_node.class, ((Yylex)yylexer).get_mybison().get_jflex().get_line() + 1); - p.m_type = explicit_value_type.EVT_TRUE; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - FFALSE - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FFALSE "); - explicit_value_node p = ((Yylex)yylexer).new_node(explicit_value_node.class, ((Yylex)yylexer).get_mybison().get_jflex().get_line() + 1); - p.m_type = explicit_value_type.EVT_FALSE; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - NUMBER - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: NUMBER "); - explicit_value_node p = ((Yylex)yylexer).new_node(explicit_value_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_str = ((ParserVal)((ParserVal)$1)).sval; - p.m_type = explicit_value_type.EVT_NUM; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - FKUUID - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FKUUID "); - explicit_value_node p = ((Yylex)yylexer).new_node(explicit_value_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_str = ((ParserVal)((ParserVal)$1)).sval; - p.m_type = explicit_value_type.EVT_UUID; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - STRING_DEFINITION - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: STRING_DEFINITION "); - explicit_value_node p = ((Yylex)yylexer).new_node(explicit_value_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_str = ((ParserVal)((ParserVal)$1)).sval; - p.m_type = explicit_value_type.EVT_STR; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - FKFLOAT - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FKFLOAT "); - explicit_value_node p = ((Yylex)yylexer).new_node(explicit_value_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_str = ((ParserVal)((ParserVal)$1)).sval; - p.m_type = explicit_value_type.EVT_FLOAT; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - OPEN_BIG_BRACKET const_map_list_value CLOSE_BIG_BRACKET - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: OPEN_BIG_BRACKET const_map_list_value CLOSE_BIG_BRACKET "); - explicit_value_node p = ((Yylex)yylexer).new_node(explicit_value_node.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_type = explicit_value_type.EVT_MAP; - p.m_v = (const_map_list_value_node)((ParserVal)$2).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - OPEN_SQUARE_BRACKET const_array_list_value CLOSE_SQUARE_BRACKET - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: OPEN_BIG_BRACKET const_array_list_value CLOSE_BIG_BRACKET "); - explicit_value_node p = ((Yylex)yylexer).new_node(explicit_value_node.class, ((ParserVal)((ParserVal)$2)).ival); - p.m_type = explicit_value_type.EVT_ARRAY; - p.m_v = (const_array_list_value_node)((ParserVal)$2).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - - -const_map_list_value : - /* empty */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: empty "); - const_map_list_value_node p = ((Yylex)yylexer).new_node(const_map_list_value_node.class, ((Yylex)yylexer).get_mybison().get_jflex().get_line()); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - const_map_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: const_map_value "); - const_map_list_value_node p = ((Yylex)yylexer).new_node(const_map_list_value_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.add_ele((const_map_value_node)((ParserVal)$1).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - const_map_list_value const_map_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: const_map_list_value const_map_value "); - const_map_list_value_node p = (const_map_list_value_node)((ParserVal)$1).obj; - p.add_ele((const_map_value_node)((ParserVal)$2).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - - -const_map_value : - explicit_value COLON explicit_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: explicit_value COLON explicit_value "); - const_map_value_node p = ((Yylex)yylexer).new_node(const_map_value_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.m_k = (syntree_node)((ParserVal)$1).obj; - p.m_v = (syntree_node)((ParserVal)$3).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -const_array_list_value : - /* empty */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: empty "); - const_array_list_value_node p = ((Yylex)yylexer).new_node(const_array_list_value_node.class, ((Yylex)yylexer).get_mybison().get_jflex().get_line()); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - explicit_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: explicit_value "); - const_array_list_value_node p = ((Yylex)yylexer).new_node(const_array_list_value_node.class, ((ParserVal)((ParserVal)$1)).ival); - p.add_ele((explicit_value_node)((ParserVal)$1).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - | - const_array_list_value explicit_value - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: const_array_list_value explicit_value "); - const_array_list_value_node p = (const_array_list_value_node)((ParserVal)$1).obj; - p.add_ele((explicit_value_node)((ParserVal)$2).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - $$ = ret; - } - ; - -%% - diff --git a/jflexbison/jflex.flex b/jflexbison/jflex.flex deleted file mode 100644 index dcdb2d5..0000000 --- a/jflexbison/jflex.flex +++ /dev/null @@ -1,390 +0,0 @@ -package com.github.esrrhs.fakescript; - -import com.github.esrrhs.fakescript.syntree.*; - -%% - -%unicode - -%line -%column -%implements YYParser.Lexer - -%byaccj - -%{ - fake m_f; - StringBuilder string = new StringBuilder(); - /* store a reference to the YYParser object */ - private YYParser yyparser; - - private ParserVal yylval; - - private mybison m_mybison; - - public fake get_fake() - { - return m_f; - } - - public void set_fake(fake f) - { - m_f = f; - } - - public void set_mybison(mybison mb) - { - m_mybison = mb; - } - - public mybison get_mybison() - { - return m_mybison; - } - - public T new_node(Class c, int lineno) - { - try - { - syntree_node t = null; - t = c.newInstance(); - t.m_lno = lineno; - return (T) t; - } - catch (Exception e) - { - return null; - } - } - - public int get_line() - { - return yyline; - } - - public Object getLVal() - { - return yylval; - } - - public void yyerror(String s) - { - m_mybison.lexer_error(s, yyline, yytext()); - } -%} - -LineTerminator = \r|\n|\r\n -InputCharacter = [^\r\n] -Comment = "--" {InputCharacter}* {LineTerminator}? -StringCharacter = [^\r\n\"\\] -WhiteSpace = {LineTerminator} | [ \t\f] - -%state STRING - -%% - - { - -{Comment} { /* ignore */ } - -\" { yybegin(STRING); string.setLength(0); } - -"var" { - return YYParser.VAR_BEGIN; -} - -"return" { - return YYParser.RETURN; -} - -"break" { - return YYParser.BREAK; -} - -"func" { - return YYParser.FUNC; -} - -"fake" { - return YYParser.FAKE; -} - -"while" { - return YYParser.WHILE; -} - -"for" { - return YYParser.FOR; -} - -"true" { - return YYParser.FTRUE; -} - -"false" { - return YYParser.FFALSE; -} - -"if" { - return YYParser.IF; -} - -"then" { - return YYParser.THEN; -} - -"else" { - return YYParser.ELSE; -} - -"elseif" { - return YYParser.ELSEIF; -} - -"end" { - return YYParser.END; -} - -"const" { - return YYParser.FCONST; -} - -"package" { - return YYParser.PACKAGE; -} - -"null" { - return YYParser.NULL; -} - -"include" { - return YYParser.INCLUDE; -} - -"struct" { - return YYParser.STRUCT; -} - -"and" { - return YYParser.AND; -} - -"or" { - return YYParser.OR; -} - -"is" { - return YYParser.IS; -} - -"not" { - return YYParser.NOT; -} - -"continue" { - return YYParser.CONTINUE; -} - -"yield" { - return YYParser.YIELD; -} - -"sleep" { - return YYParser.SLEEP; -} - -"switch" { - return YYParser.SWITCH; -} - -"case" { - return YYParser.CASE; -} - -"default" { - return YYParser.DEFAULT; -} - -[a-zA-Z_][a-zA-Z0-9_]* { - yylval = new ParserVal(yytext()); - yylval.ival = yyline + 1; - return YYParser.IDENTIFIER; -} - -[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)+ { - yylval = new ParserVal(yytext()); - yylval.ival = yyline + 1; - return YYParser.IDENTIFIER_DOT; -} - -[a-zA-Z_][a-zA-Z0-9_]*(\-\>[a-zA-Z_][a-zA-Z0-9_]*)+ { - yylval = new ParserVal(yytext()); - yylval.ival = yyline + 1; - return YYParser.IDENTIFIER_POINTER; -} - -[0-9]+u { - yylval = new ParserVal(yytext()); - yylval.ival = yyline + 1; - return YYParser.FKUUID; -} - --?[0-9]+ { - yylval = new ParserVal(yytext()); - yylval.ival = yyline + 1; - return YYParser.NUMBER; -} - --?[0-9]+\.[0-9]+([Ee]-?[0-9]+)? { - yylval = new ParserVal(yytext()); - yylval.ival = yyline + 1; - return YYParser.FKFLOAT; -} - -"%" { - return YYParser.DIVIDE_MOD; -} - -"," { - return YYParser.ARG_SPLITTER; -} - -"->" { - return YYParser.RIGHT_POINTER; -} - -"++" { - return YYParser.INC; -} - -"+" { - return YYParser.PLUS; -} - -"-" { - return YYParser.MINUS; -} - -"/" { - return YYParser.DIVIDE; -} - -"*" { - return YYParser.MULTIPLY; -} - -":=" { - return YYParser.NEW_ASSIGN; -} - -"+=" { - return YYParser.PLUS_ASSIGN; -} - -"-=" { - return YYParser.MINUS_ASSIGN; -} - -"/=" { - return YYParser.DIVIDE_ASSIGN; -} - -"*=" { - return YYParser.MULTIPLY_ASSIGN; -} - -"%=" { - return YYParser.DIVIDE_MOD_ASSIGN; -} - -"=" { - return YYParser.ASSIGN; -} - -">" { - return YYParser.MORE; -} - -"<" { - return YYParser.LESS; -} - -">=" { - return YYParser.MORE_OR_EQUAL; -} - -"<=" { - return YYParser.LESS_OR_EQUAL; -} - -"==" { - return YYParser.EQUAL; -} - -"!=" { - return YYParser.NOT_EQUAL; -} - -"(" { - return YYParser.OPEN_BRACKET; -} - -")" { - return YYParser.CLOSE_BRACKET; -} - -":" { - return YYParser.COLON; -} - -"[" { - return YYParser.OPEN_SQUARE_BRACKET; -} - -"]" { - return YYParser.CLOSE_SQUARE_BRACKET; -} - -"{" { - return YYParser.OPEN_BIG_BRACKET; -} - -"}" { - return YYParser.CLOSE_BIG_BRACKET; -} - -".." { - return YYParser.STRING_CAT; -} - -{WhiteSpace} { } - -<> { - return YYParser.EOF; -} - -} - - { - \" { yybegin(YYINITIAL); - yylval = new ParserVal(string.toString()); - yylval.ival = yyline + 1; - return YYParser.STRING_DEFINITION; } - - {StringCharacter}+ { string.append( yytext() ); } - - /* escape sequences */ - "\\b" { string.append( '\b' ); } - "\\t" { string.append( '\t' ); } - "\\n" { string.append( '\n' ); } - "\\f" { string.append( '\f' ); } - "\\r" { string.append( '\r' ); } - "\\\"" { string.append( '\"' ); } - "\\'" { string.append( '\'' ); } - "\\\\" { string.append( '\\' ); } - -} - -/* error fallback */ -[^] { } -<> { return YYParser.EOF; } diff --git a/log.txt b/log.txt deleted file mode 100644 index fe25a91..0000000 --- a/log.txt +++ /dev/null @@ -1,42 +0,0 @@ -2016.7.14:建立hg -2016.7.15:建立jflex,cup -2016.7.18:语法迁移ok,加语法树节点 -2016.7.19:加语法树节点 -2016.7.20:加语法树节点完成,下一步编译 -2016.7.21:加编译空class,下一步加基本的binary类型 -2016.7.22:开始加variant类型,下一步完善variant周边操作,variant用原生Object,string就用原生String,map和array在原生基础上加一个const -2016.7.23:fix build -2016.7.26:加编译const,java确实很方便,下一步继续移植compiler -2016.7.28:无脑翻译compile while -2016.7.29:移植compile完成,下一步加binary的dump -2016.7.30:加binary的dump完成,下一步加文件的lineno -2016.7.30:加文件的lineno完成,java版本无调试需求,下一步加解释器 -2016.7.31:加java的绑定,注解确实很方便 -2016.7.31:花了一下午,完成了解释器,下一步加内置函数 -2016.7.31:加基本的内置函数完成,基本java也够了,运行时间java 7s,c++ 4s -2016.8.1:用java1.7的lib -2016.8.2:增加regall的接口,增加了c:memfunc1():memfunc2()和a()(arg1)(arg2)的语法,回头同步到c++版本去 -2016.8.4:修复bug -2016.8.5:完善test,完善profile -2016.8.29:漏掉了include,修复传参数顺序的bug -2016.8.30:修复绑定函数返回null的bug -2016.8.31:添加null的比较,修复for的continue的设计bug -2016.9.2:增加全局绑定函数缓存,节省内存,java的对象比较占空间 -2016.9.5:添加调试功能,行号还有点问题,不是很准确,用complexsymbol搞定了 -2016.9.6:改下debug下的错误提示 -2016.9.8:加下字节码的优化 -2016.9.9:修改字节码的优化1 -2016.9.12:修改字节码的优化2 -2016.9.13:修改字节码的优化bug -2016.10.27:给java版本加个_G -2017.8.1:修复bug -2017.9.11:修改getMethods -2017.10.11:添加tolong接口 -2017.11.30:添加maven -2018.1.5:添加error接口 -2018.1.7:添加error接口 -2018.1.8:优化parse -2018.1.12:修改bug -2018.1.17:修改優化的bug -2018.6.23:修改map get key的bug - diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..bd8896b --- /dev/null +++ b/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..5761d94 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml index 552267d..1fb1973 100644 --- a/pom.xml +++ b/pom.xml @@ -7,19 +7,26 @@ com.github.esrrhs fakescript-java jar - 1.0.13 - - - org.sonatype.oss - oss-parent - 7 - - + 1.0.16 fakescript-java - Lightweight embedded scripting language. + Lua 5.4 implemented in pure Java, with the JVM handling garbage collection. https://github.com/esrrhs/fakescript-java + + UTF-8 + + 21 + 5.10.2 + 0.30.0 + + 0.95 + + The Apache Software License, Version 2.0 @@ -42,12 +49,287 @@ git@github.com:esrrhs/fakescript-java.git + + + + + org.junit.platform + junit-platform-commons + 1.10.2 + + + org.junit.platform + junit-platform-engine + 1.10.2 + + + org.junit.platform + junit-platform-launcher + 1.10.2 + + + + - edu.princeton.cup - java-cup - 10k + org.junit.jupiter + junit-jupiter + ${junit.jupiter.version} + test + + + + com.code-intelligence + jazzer-junit + ${jazzer.version} + test + + + com.code-intelligence + jazzer-api + ${jazzer.version} + test + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${maven.compiler.release} + ${project.build.sourceEncoding} + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + 180s + + + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + + prepare-agent + + prepare-agent + + + + report + test + + report + + + + check-coverage + test + + check + + + + + BUNDLE + + + LINE + COVEREDRATIO + + ${jacoco.line.minimum} + + + + + + + + + + + + + + + no-reference-lua + + + !env.LUA54_BIN + + + + + + org.jacoco + jacoco-maven-plugin + + + check-coverage + test + + check + + + true + + + + + + + + + bench + + + + + org.codehaus.mojo + exec-maven-plugin + 3.2.0 + + + + + + quality + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.8.6.4 + + false + ${project.basedir}/spotbugs-exclude.xml + + + + spotbugs + verify + + check + + + + + + + + + release + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.0 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.6.3 + + none + + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.4 + + + --pinentry-mode + loopback + + + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.4.0 + true + + central + true + + + + + + + \ No newline at end of file diff --git a/readme.md b/readme.md deleted file mode 100644 index 9fb3490..0000000 --- a/readme.md +++ /dev/null @@ -1,150 +0,0 @@ -# fakejava - -[](https://github.com/esrrhs/fakejava) -[](https://github.com/esrrhs/fakejava) -[](https://github.com/esrrhs/fakejava) -[](https://github.com/esrrhs/fakejava/actions) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/eaed26ff2181417f88d438343f42eb75)](https://www.codacy.com/manual/esrrhs/fakejava?utm_source=github.com&utm_medium=referral&utm_content=esrrhs/fakejava&utm_campaign=Badge_Grade) - -轻量级嵌入式脚本语言 - -## 简介[english](./README_EN.md) -**fakejava**是一款轻量级的嵌入式脚本语言,使用Java语言编写,语法吸取自lua、golang、erlang,基于jflex、bison生成语法树,编译成字节码解释执行。 -C/C++版本fake - -## 脚本特性 -* 语法类似lua -* 全部为函数 -* 支持array,map,可以无限嵌套 -* 支持fake testfunc(param1)产生routine,实现假多线程效果 -* 支持Java静态函数和Java类成员函数的绑定 -* 自带解释器 -* 支持多返回值 -* 自带profile,可获取脚本各个函数运行时间 -* 支持热更新 -* 支持Int64 -* 支持const定义 -* 支持包 -* 支持struct -* 支持字节码优化 -* 支持全局map - -## 示例 - -``` - --- 当前包名 -package mypackage.test - --- 引入的文件 -include "common.fk" - --- 结构体定义 -struct teststruct - sample_a - sample_b - sample_c -end - --- 常量值 -const hellostring = "hello" -const helloint = 1234 -const hellomap = {1 : "a" 2 : "b" 3 : [1 2 3]} - --- func1 comment -func myfunc1(arg1, arg2) - - -- Java静态函数和类成员函数的调用 - arg3 := cfunc1(helloint) + arg2:memfunc1(arg1) - - -- 分支 - if arg1 < arg2 then - -- 创建一个协程 - fake myfunc2(arg1, arg2) - elseif arg1 == arg2 then - print("elseif") - else - print("else") - end - - -- for循环 - for var i = 0, i < arg2, i++ then - print("i = ", i) - end - - -- 数组 - var a = array() - a[1] = 3 - - -- 集合 - var b = map() - b[a] = 1 - b[1] = a - - -- Int64 - var uid = 1241515236123614u - log("uid = ", uid) - - -- 子函数调用 - var ret1, var ret2 = myfunc2() - - -- 其他包的函数调用 - ret1 = otherpackage.test.myfunc1(arg1, arg2) - - -- 结构体 - var tt = teststruct() - tt->sample_a = 1 - tt->sample_b = teststruct() - tt->sample_b->sample_a = 10 - - -- 分支 - switch arg1 - case 1 then - print("1") - case "a" then - print("a") - default - print("default") - end - - -- 多返回值 - return arg1, arg3 - -end -``` - -## Java示例 - -``` -// 创建一个实例 -fake f = fk.newfake(null); -// 注册包里全部标记@fakescript的函数 -fk.reg(f, "com.test"); -// 解析fake脚本文件 -fk.parse(f, "test.fk"); -// 执行myfunc1函数,传入两个参数分别为1和2 -double ret = (double)fk.run(f, "myfunc1", 1, 2); - -``` - -## 使用 -#### Maven -``` - - com.github.esrrhs - fakescript-java - 1.0.12 - - -``` - - -## 调试环境 -* IDE - -![image](img/ide.png) - -* 命令行 - -![image](img/debug.png) - diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml new file mode 100644 index 0000000..b1419cd --- /dev/null +++ b/spotbugs-exclude.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/main/java/com/github/esrrhs/fakescript/ParserVal.java b/src/main/java/com/github/esrrhs/fakescript/ParserVal.java deleted file mode 100644 index f8ff021..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/ParserVal.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.github.esrrhs.fakescript; - -/** - * BYACC/J Semantic Value for parser: Parser - * This class provides some of the functionality - * of the yacc/C 'union' directive - */ -public class ParserVal -{ -/** - * integer value of this 'union' - */ -public int ival; - -/** - * double value of this 'union' - */ -public double dval; - -/** - * string value of this 'union' - */ -public String sval; - -/** - * object value of this 'union' - */ -public Object obj; - -//############################################# -//## C O N S T R U C T O R S -//############################################# -/** - * Initialize me without a value - */ -public ParserVal() -{ -} -/** - * Initialize me as an int - */ -public ParserVal(int val) -{ - ival=val; -} - -/** - * Initialize me as a double - */ -public ParserVal(double val) -{ - dval=val; -} - -/** - * Initialize me as a string - */ -public ParserVal(String val) -{ - sval=val; -} - -/** - * Initialize me as an Object - */ -public ParserVal(Object val) -{ - obj=val; -} -}//end class - -//############################################# -//## E N D O F F I L E -//############################################# diff --git a/src/main/java/com/github/esrrhs/fakescript/YYParser.java b/src/main/java/com/github/esrrhs/fakescript/YYParser.java deleted file mode 100644 index 66a62f8..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/YYParser.java +++ /dev/null @@ -1,3766 +0,0 @@ - -/* A Bison parser, made by GNU Bison 2.4.1. */ - -/* Skeleton implementation for Bison LALR(1) parsers in Java - - Copyright (C) 2007, 2008 Free Software Foundation, Inc. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . */ - -/* As a special exception, you may create a larger work that contains - part or all of the Bison parser skeleton and distribute that work - under terms of your choice, so long as that work isn't itself a - parser generator using the skeleton or a modified version thereof - as a parser skeleton. Alternatively, if you modify or redistribute - the parser skeleton itself, you may (at your option) remove this - special exception, which will cause the skeleton and the resulting - Bison output files to be licensed under the GNU General Public - License without this special exception. - - This special exception was added by the Free Software Foundation in - version 2.2 of Bison. */ - -/* First part of user declarations. */ - -/* Line 32 of lalr1.java */ -/* Line 1 of "YYParser.y" */ - -package com.github.esrrhs.fakescript; - -import com.github.esrrhs.fakescript.syntree.*; -import java.io.*; - - - -/** - * A Bison parser, automatically generated from YYParser.y. - * - * @author LALR (1) parser skeleton written by Paolo Bonzini. - */ -class YYParser -{ - /** Version number for the Bison executable that generated this parser. */ - public static final String bisonVersion = "2.4.1"; - - /** Name of the skeleton that generated this parser. */ - public static final String bisonSkeleton = "lalr1.java"; - - - /** True if verbose error messages are enabled. */ - public boolean errorVerbose = false; - - - - /** Token returned by the scanner to signal the end of its input. */ - public static final int EOF = 0; - -/* Tokens. */ - /** Token number, to be returned by the scanner. */ - public static final int VAR_BEGIN = 258; - /** Token number, to be returned by the scanner. */ - public static final int RETURN = 259; - /** Token number, to be returned by the scanner. */ - public static final int BREAK = 260; - /** Token number, to be returned by the scanner. */ - public static final int FUNC = 261; - /** Token number, to be returned by the scanner. */ - public static final int WHILE = 262; - /** Token number, to be returned by the scanner. */ - public static final int FTRUE = 263; - /** Token number, to be returned by the scanner. */ - public static final int FFALSE = 264; - /** Token number, to be returned by the scanner. */ - public static final int IF = 265; - /** Token number, to be returned by the scanner. */ - public static final int THEN = 266; - /** Token number, to be returned by the scanner. */ - public static final int ELSE = 267; - /** Token number, to be returned by the scanner. */ - public static final int END = 268; - /** Token number, to be returned by the scanner. */ - public static final int STRING_DEFINITION = 269; - /** Token number, to be returned by the scanner. */ - public static final int IDENTIFIER = 270; - /** Token number, to be returned by the scanner. */ - public static final int NUMBER = 271; - /** Token number, to be returned by the scanner. */ - public static final int SINGLE_LINE_COMMENT = 272; - /** Token number, to be returned by the scanner. */ - public static final int DIVIDE_MOD = 273; - /** Token number, to be returned by the scanner. */ - public static final int ARG_SPLITTER = 274; - /** Token number, to be returned by the scanner. */ - public static final int PLUS = 275; - /** Token number, to be returned by the scanner. */ - public static final int MINUS = 276; - /** Token number, to be returned by the scanner. */ - public static final int DIVIDE = 277; - /** Token number, to be returned by the scanner. */ - public static final int MULTIPLY = 278; - /** Token number, to be returned by the scanner. */ - public static final int ASSIGN = 279; - /** Token number, to be returned by the scanner. */ - public static final int MORE = 280; - /** Token number, to be returned by the scanner. */ - public static final int LESS = 281; - /** Token number, to be returned by the scanner. */ - public static final int MORE_OR_EQUAL = 282; - /** Token number, to be returned by the scanner. */ - public static final int LESS_OR_EQUAL = 283; - /** Token number, to be returned by the scanner. */ - public static final int EQUAL = 284; - /** Token number, to be returned by the scanner. */ - public static final int NOT_EQUAL = 285; - /** Token number, to be returned by the scanner. */ - public static final int OPEN_BRACKET = 286; - /** Token number, to be returned by the scanner. */ - public static final int CLOSE_BRACKET = 287; - /** Token number, to be returned by the scanner. */ - public static final int AND = 288; - /** Token number, to be returned by the scanner. */ - public static final int OR = 289; - /** Token number, to be returned by the scanner. */ - public static final int FKFLOAT = 290; - /** Token number, to be returned by the scanner. */ - public static final int PLUS_ASSIGN = 291; - /** Token number, to be returned by the scanner. */ - public static final int MINUS_ASSIGN = 292; - /** Token number, to be returned by the scanner. */ - public static final int DIVIDE_ASSIGN = 293; - /** Token number, to be returned by the scanner. */ - public static final int MULTIPLY_ASSIGN = 294; - /** Token number, to be returned by the scanner. */ - public static final int DIVIDE_MOD_ASSIGN = 295; - /** Token number, to be returned by the scanner. */ - public static final int COLON = 296; - /** Token number, to be returned by the scanner. */ - public static final int FOR = 297; - /** Token number, to be returned by the scanner. */ - public static final int INC = 298; - /** Token number, to be returned by the scanner. */ - public static final int FAKE = 299; - /** Token number, to be returned by the scanner. */ - public static final int FKUUID = 300; - /** Token number, to be returned by the scanner. */ - public static final int OPEN_SQUARE_BRACKET = 301; - /** Token number, to be returned by the scanner. */ - public static final int CLOSE_SQUARE_BRACKET = 302; - /** Token number, to be returned by the scanner. */ - public static final int FCONST = 303; - /** Token number, to be returned by the scanner. */ - public static final int PACKAGE = 304; - /** Token number, to be returned by the scanner. */ - public static final int INCLUDE = 305; - /** Token number, to be returned by the scanner. */ - public static final int IDENTIFIER_DOT = 306; - /** Token number, to be returned by the scanner. */ - public static final int IDENTIFIER_POINTER = 307; - /** Token number, to be returned by the scanner. */ - public static final int STRUCT = 308; - /** Token number, to be returned by the scanner. */ - public static final int IS = 309; - /** Token number, to be returned by the scanner. */ - public static final int NOT = 310; - /** Token number, to be returned by the scanner. */ - public static final int CONTINUE = 311; - /** Token number, to be returned by the scanner. */ - public static final int YIELD = 312; - /** Token number, to be returned by the scanner. */ - public static final int SLEEP = 313; - /** Token number, to be returned by the scanner. */ - public static final int SWITCH = 314; - /** Token number, to be returned by the scanner. */ - public static final int CASE = 315; - /** Token number, to be returned by the scanner. */ - public static final int DEFAULT = 316; - /** Token number, to be returned by the scanner. */ - public static final int NEW_ASSIGN = 317; - /** Token number, to be returned by the scanner. */ - public static final int ELSEIF = 318; - /** Token number, to be returned by the scanner. */ - public static final int RIGHT_POINTER = 319; - /** Token number, to be returned by the scanner. */ - public static final int STRING_CAT = 320; - /** Token number, to be returned by the scanner. */ - public static final int OPEN_BIG_BRACKET = 321; - /** Token number, to be returned by the scanner. */ - public static final int CLOSE_BIG_BRACKET = 322; - /** Token number, to be returned by the scanner. */ - public static final int NULL = 323; - - - - - - /** - * Communication interface between the scanner and the Bison-generated - * parser YYParser. - */ - public interface Lexer { - - - /** - * Method to retrieve the semantic value of the last scanned token. - * @return the semantic value of the last scanned token. */ - Object getLVal (); - - /** - * Entry point for the scanner. Returns the token identifier corresponding - * to the next token and prepares to return the semantic value - * of the token. - * @return the token identifier corresponding to the next token. */ - int yylex () throws java.io.IOException; - - /** - * Entry point for error reporting. Emits an error - * in a user-defined way. - * - * - * @param s The string for the error message. */ - void yyerror (String s); - } - - /** The object doing lexical analysis for us. */ - private Lexer yylexer; - - - - - - /** - * Instantiates the Bison-generated parser. - * @param yylexer The scanner that will supply tokens to the parser. - */ - public YYParser (Lexer yylexer) { - this.yylexer = yylexer; - - } - - private java.io.PrintStream yyDebugStream = System.err; - - /** - * Return the PrintStream on which the debugging output is - * printed. - */ - public final java.io.PrintStream getDebugStream () { return yyDebugStream; } - - /** - * Set the PrintStream on which the debug output is printed. - * @param s The stream that is used for debugging output. - */ - public final void setDebugStream(java.io.PrintStream s) { yyDebugStream = s; } - - private int yydebug = 0; - - /** - * Answer the verbosity of the debugging output; 0 means that all kinds of - * output from the parser are suppressed. - */ - public final int getDebugLevel() { return yydebug; } - - /** - * Set the verbosity of the debugging output; 0 means that all kinds of - * output from the parser are suppressed. - * @param level The verbosity level for debugging output. - */ - public final void setDebugLevel(int level) { yydebug = level; } - - private final int yylex () throws java.io.IOException { - return yylexer.yylex (); - } - protected final void yyerror (String s) { - yylexer.yyerror (s); - } - - - - protected final void yycdebug (String s) { - if (yydebug > 0) - yyDebugStream.println (s); - } - - private final class YYStack { - private int[] stateStack = new int[16]; - - private Object[] valueStack = new Object[16]; - - public int size = 16; - public int height = -1; - - public final void push (int state, Object value ) { - height++; - if (size == height) - { - int[] newStateStack = new int[size * 2]; - System.arraycopy (stateStack, 0, newStateStack, 0, height); - stateStack = newStateStack; - - - Object[] newValueStack = new Object[size * 2]; - System.arraycopy (valueStack, 0, newValueStack, 0, height); - valueStack = newValueStack; - - size *= 2; - } - - stateStack[height] = state; - - valueStack[height] = value; - } - - public final void pop () { - height--; - } - - public final void pop (int num) { - // Avoid memory leaks... garbage collection is a white lie! - if (num > 0) { - java.util.Arrays.fill (valueStack, height - num + 1, height, null); - - } - height -= num; - } - - public final int stateAt (int i) { - return stateStack[height - i]; - } - - public final Object valueAt (int i) { - return valueStack[height - i]; - } - - // Print the state stack on the debug stream. - public void print (java.io.PrintStream out) - { - out.print ("Stack now"); - - for (int i = 0; i < height; i++) - { - out.print (' '); - out.print (stateStack[i]); - } - out.println (); - } - } - - /** - * Returned by a Bison action in order to stop the parsing process and - * return success (true). */ - public static final int YYACCEPT = 0; - - /** - * Returned by a Bison action in order to stop the parsing process and - * return failure (false). */ - public static final int YYABORT = 1; - - /** - * Returned by a Bison action in order to start error recovery without - * printing an error message. */ - public static final int YYERROR = 2; - - /** - * Returned by a Bison action in order to print an error message and start - * error recovery. */ - public static final int YYFAIL = 3; - - private static final int YYNEWSTATE = 4; - private static final int YYDEFAULT = 5; - private static final int YYREDUCE = 6; - private static final int YYERRLAB1 = 7; - private static final int YYRETURN = 8; - - private int yyerrstatus_ = 0; - - /** - * Return whether error recovery is being done. In this state, the parser - * reads token until it reaches a known state, and then restarts normal - * operation. */ - public final boolean recovering () - { - return yyerrstatus_ == 0; - } - - private int yyaction (int yyn, YYStack yystack, int yylen) - { - Object yyval; - - - /* If YYLEN is nonzero, implement the default value of the action: - `$$ = $1'. Otherwise, use the top of the stack. - - Otherwise, the following line sets YYVAL to garbage. - This behavior is undocumented and Bison - users should not rely upon it. */ - if (yylen > 0) - yyval = yystack.valueAt (yylen - 1); - else - yyval = yystack.valueAt (0); - - yy_reduce_print (yyn, yystack); - - switch (yyn) - { - case 3: - if (yyn == 3) - -/* Line 353 of lalr1.java */ -/* Line 99 of "YYParser.y" */ - { - }; - break; - - - case 6: - if (yyn == 6) - -/* Line 353 of lalr1.java */ -/* Line 109 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FUNC IDENTIFIER OPEN_BRACKET function_declaration_arguments CLOSE_BRACKET block END"); - func_desc_node p = ((Yylex)yylexer).new_node(func_desc_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (7-(2)))))).ival); - p.m_funcname = ((ParserVal)((ParserVal)((yystack.valueAt (7-(2)))))).sval; - p.m_arglist = (func_desc_arglist_node)((ParserVal)((yystack.valueAt (7-(4))))).obj; - p.m_block = (block_node)((ParserVal)((yystack.valueAt (7-(6))))).obj; - p.m_endline = ((Yylex)yylexer).get_mybison().get_jflex().get_line(); - ((Yylex)yylexer).get_mybison().add_func_desc(p); - }; - break; - - - case 7: - if (yyn == 7) - -/* Line 353 of lalr1.java */ -/* Line 120 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FUNC IDENTIFIER OPEN_BRACKET function_declaration_arguments CLOSE_BRACKET END"); - func_desc_node p = ((Yylex)yylexer).new_node(func_desc_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (6-(2)))))).ival); - p.m_funcname = ((ParserVal)((ParserVal)((yystack.valueAt (6-(2)))))).sval; - p.m_arglist = (func_desc_arglist_node)((ParserVal)((yystack.valueAt (6-(4))))).obj; - p.m_endline = ((Yylex)yylexer).get_mybison().get_jflex().get_line(); - ((Yylex)yylexer).get_mybison().add_func_desc(p); - }; - break; - - - case 8: - if (yyn == 8) - -/* Line 353 of lalr1.java */ -/* Line 132 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: empty"); - }; - break; - - - case 9: - if (yyn == 9) - -/* Line 353 of lalr1.java */ -/* Line 137 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: function_declaration_arguments ARG_SPLITTER arg "); - func_desc_arglist_node p = (func_desc_arglist_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.add_arg((syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 10: - if (yyn == 10) - -/* Line 353 of lalr1.java */ -/* Line 148 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: arg"); - func_desc_arglist_node p = ((Yylex)yylexer).new_node(func_desc_arglist_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).ival); - p.add_arg((syntree_node)((ParserVal)((yystack.valueAt (1-(1))))).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 11: - if (yyn == 11) - -/* Line 353 of lalr1.java */ -/* Line 161 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: IDENTIFIER"); - identifier_node p = ((Yylex)yylexer).new_node(identifier_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).ival); - p.m_str = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).sval; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 12: - if (yyn == 12) - -/* Line 353 of lalr1.java */ -/* Line 175 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: IDENTIFIER OPEN_BRACKET function_call_arguments CLOSE_BRACKET "); - function_call_node p = ((Yylex)yylexer).new_node(function_call_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (4-(1)))))).ival); - p.m_fuc = ((ParserVal)((ParserVal)((yystack.valueAt (4-(1)))))).sval; - p.m_arglist = (function_call_arglist_node)((ParserVal)((yystack.valueAt (4-(3))))).obj; - p.m_fakecall = false; - p.m_classmem_call = false; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 13: - if (yyn == 13) - -/* Line 353 of lalr1.java */ -/* Line 189 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: IDENTIFIER_DOT OPEN_BRACKET function_call_arguments CLOSE_BRACKET "); - function_call_node p = ((Yylex)yylexer).new_node(function_call_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (4-(1)))))).ival); - p.m_fuc = ((ParserVal)((ParserVal)((yystack.valueAt (4-(1)))))).sval; - p.m_arglist = (function_call_arglist_node)((ParserVal)((yystack.valueAt (4-(3))))).obj; - p.m_fakecall = false; - p.m_classmem_call = false; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 14: - if (yyn == 14) - -/* Line 353 of lalr1.java */ -/* Line 203 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: function_call OPEN_BRACKET function_call_arguments CLOSE_BRACKET "); - function_call_node p = ((Yylex)yylexer).new_node(function_call_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (4-(1)))))).ival); - p.m_prefuc = (syntree_node)((ParserVal)((yystack.valueAt (4-(1))))).obj; - p.m_arglist = (function_call_arglist_node)((ParserVal)((yystack.valueAt (4-(3))))).obj; - p.m_fakecall = false; - p.m_classmem_call = false; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 15: - if (yyn == 15) - -/* Line 353 of lalr1.java */ -/* Line 217 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable COLON IDENTIFIER OPEN_BRACKET function_call_arguments CLOSE_BRACKET "); - function_call_node p = ((Yylex)yylexer).new_node(function_call_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (6-(1)))))).ival); - p.m_fuc = ((ParserVal)((ParserVal)((yystack.valueAt (6-(3)))))).sval; - p.m_arglist = (function_call_arglist_node)((ParserVal)((yystack.valueAt (6-(5))))).obj; - if (p.m_arglist == null) - { - p.m_arglist = ((Yylex)yylexer).new_node(function_call_arglist_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (6-(1)))))).ival); - } - p.m_arglist.add_arg((syntree_node)((ParserVal)((yystack.valueAt (6-(1))))).obj); - p.m_fakecall = false; - p.m_classmem_call = true; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 16: - if (yyn == 16) - -/* Line 353 of lalr1.java */ -/* Line 236 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: function_call COLON IDENTIFIER OPEN_BRACKET function_call_arguments CLOSE_BRACKET "); - function_call_node p = ((Yylex)yylexer).new_node(function_call_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (6-(1)))))).ival); - p.m_fuc = ((ParserVal)((ParserVal)((yystack.valueAt (6-(3)))))).sval; - p.m_arglist = (function_call_arglist_node)((ParserVal)((yystack.valueAt (6-(5))))).obj; - if (p.m_arglist == null) - { - p.m_arglist = ((Yylex)yylexer).new_node(function_call_arglist_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (6-(1)))))).ival); - } - p.m_arglist.add_arg((syntree_node)((ParserVal)((yystack.valueAt (6-(1))))).obj); - p.m_fakecall = false; - p.m_classmem_call = true; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 17: - if (yyn == 17) - -/* Line 353 of lalr1.java */ -/* Line 257 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: empty "); - }; - break; - - - case 18: - if (yyn == 18) - -/* Line 353 of lalr1.java */ -/* Line 262 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: function_call_arguments ARG_SPLITTER arg_expr "); - function_call_arglist_node p = (function_call_arglist_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.add_arg((syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 19: - if (yyn == 19) - -/* Line 353 of lalr1.java */ -/* Line 273 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: arg_expr "); - function_call_arglist_node p = ((Yylex)yylexer).new_node(function_call_arglist_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).ival); - p.add_arg((syntree_node)((ParserVal)((yystack.valueAt (1-(1))))).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 20: - if (yyn == 20) - -/* Line 353 of lalr1.java */ -/* Line 286 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: expr_value"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 21: - if (yyn == 21) - -/* Line 353 of lalr1.java */ -/* Line 296 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: block stmt "); - block_node p = (block_node)((ParserVal)((yystack.valueAt (2-(1))))).obj; - p.add_stmt((syntree_node)((ParserVal)((yystack.valueAt (2-(2))))).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 22: - if (yyn == 22) - -/* Line 353 of lalr1.java */ -/* Line 307 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: stmt"); - block_node p = ((Yylex)yylexer).new_node(block_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).ival); - p.add_stmt((syntree_node)((ParserVal)((yystack.valueAt (1-(1))))).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 23: - if (yyn == 23) - -/* Line 353 of lalr1.java */ -/* Line 320 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: while_stmt"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 24: - if (yyn == 24) - -/* Line 353 of lalr1.java */ -/* Line 326 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: if_stmt"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 25: - if (yyn == 25) - -/* Line 353 of lalr1.java */ -/* Line 332 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: return_stmt"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 26: - if (yyn == 26) - -/* Line 353 of lalr1.java */ -/* Line 338 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: assign_stmt"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 27: - if (yyn == 27) - -/* Line 353 of lalr1.java */ -/* Line 344 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: multi_assign_stmt"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 28: - if (yyn == 28) - -/* Line 353 of lalr1.java */ -/* Line 350 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: break"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 29: - if (yyn == 29) - -/* Line 353 of lalr1.java */ -/* Line 356 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: continue"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 30: - if (yyn == 30) - -/* Line 353 of lalr1.java */ -/* Line 362 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: expr"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 31: - if (yyn == 31) - -/* Line 353 of lalr1.java */ -/* Line 368 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: math_assign_stmt"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 32: - if (yyn == 32) - -/* Line 353 of lalr1.java */ -/* Line 374 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: for_stmt"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 33: - if (yyn == 33) - -/* Line 353 of lalr1.java */ -/* Line 380 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: for_loop_stmt"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 34: - if (yyn == 34) - -/* Line 353 of lalr1.java */ -/* Line 386 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: fake_call_stmt"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 35: - if (yyn == 35) - -/* Line 353 of lalr1.java */ -/* Line 392 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: sleep_stmt"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 36: - if (yyn == 36) - -/* Line 353 of lalr1.java */ -/* Line 398 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: yield_stmt"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 37: - if (yyn == 37) - -/* Line 353 of lalr1.java */ -/* Line 404 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: switch_stmt"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 38: - if (yyn == 38) - -/* Line 353 of lalr1.java */ -/* Line 412 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FAKE function_call"); - function_call_node p = (function_call_node)((ParserVal)((yystack.valueAt (2-(2))))).obj; - p.m_fakecall = true; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 39: - if (yyn == 39) - -/* Line 353 of lalr1.java */ -/* Line 425 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FOR block ARG_SPLITTER cmp ARG_SPLITTER block THEN block END"); - for_stmt p = ((Yylex)yylexer).new_node(for_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (9-(2)))))).ival); - p.m_cmp = (cmp_stmt)((ParserVal)((yystack.valueAt (9-(4))))).obj; - p.m_beginblock = (block_node)((ParserVal)((yystack.valueAt (9-(2))))).obj; - p.m_endblock = (block_node)((ParserVal)((yystack.valueAt (9-(6))))).obj; - p.m_block = (block_node)((ParserVal)((yystack.valueAt (9-(8))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 40: - if (yyn == 40) - -/* Line 353 of lalr1.java */ -/* Line 439 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FOR block ARG_SPLITTER cmp ARG_SPLITTER block THEN END"); - for_stmt p = ((Yylex)yylexer).new_node(for_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (8-(2)))))).ival); - p.m_cmp = (cmp_stmt)((ParserVal)((yystack.valueAt (8-(4))))).obj; - p.m_beginblock = (block_node)((ParserVal)((yystack.valueAt (8-(2))))).obj; - p.m_endblock = (block_node)((ParserVal)((yystack.valueAt (8-(6))))).obj; - p.m_block = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 41: - if (yyn == 41) - -/* Line 353 of lalr1.java */ -/* Line 455 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FOR var ASSIGN assign_value RIGHT_POINTER cmp_value ARG_SPLITTER expr_value THEN block END"); - for_loop_stmt p = ((Yylex)yylexer).new_node(for_loop_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (11-(2)))))).ival); - p.m_var = (syntree_node)((ParserVal)((yystack.valueAt (11-(2))))).obj; - p.m_begin = (syntree_node)((ParserVal)((yystack.valueAt (11-(4))))).obj; - p.m_end = (syntree_node)((ParserVal)((yystack.valueAt (11-(6))))).obj; - p.m_add = (syntree_node)((ParserVal)((yystack.valueAt (11-(8))))).obj; - p.m_block = (block_node)((ParserVal)((yystack.valueAt (11-(10))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 42: - if (yyn == 42) - -/* Line 353 of lalr1.java */ -/* Line 470 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FOR var ASSIGN assign_value RIGHT_POINTER cmp_value ARG_SPLITTER expr_value THEN END"); - for_loop_stmt p = ((Yylex)yylexer).new_node(for_loop_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (10-(2)))))).ival); - p.m_var = (syntree_node)((ParserVal)((yystack.valueAt (10-(2))))).obj; - p.m_begin = (syntree_node)((ParserVal)((yystack.valueAt (10-(4))))).obj; - p.m_end = (syntree_node)((ParserVal)((yystack.valueAt (10-(6))))).obj; - p.m_add = (syntree_node)((ParserVal)((yystack.valueAt (10-(8))))).obj; - p.m_block = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 43: - if (yyn == 43) - -/* Line 353 of lalr1.java */ -/* Line 487 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: WHILE cmp THEN block END "); - while_stmt p = ((Yylex)yylexer).new_node(while_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (5-(2)))))).ival); - p.m_cmp = (cmp_stmt)((ParserVal)((yystack.valueAt (5-(2))))).obj; - p.m_block = (block_node)((ParserVal)((yystack.valueAt (5-(4))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 44: - if (yyn == 44) - -/* Line 353 of lalr1.java */ -/* Line 499 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: WHILE cmp THEN END "); - while_stmt p = ((Yylex)yylexer).new_node(while_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (4-(2)))))).ival); - p.m_cmp = (cmp_stmt)((ParserVal)((yystack.valueAt (4-(2))))).obj; - p.m_block = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 45: - if (yyn == 45) - -/* Line 353 of lalr1.java */ -/* Line 513 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: IF cmp THEN block elseif_stmt_list else_stmt END"); - if_stmt p = ((Yylex)yylexer).new_node(if_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (7-(2)))))).ival); - p.m_cmp = (cmp_stmt)((ParserVal)((yystack.valueAt (7-(2))))).obj; - p.m_block = (block_node)((ParserVal)((yystack.valueAt (7-(4))))).obj; - p.m_elseifs = (elseif_stmt_list)((ParserVal)((yystack.valueAt (7-(5))))).obj; - p.m_elses = (else_stmt)((ParserVal)((yystack.valueAt (7-(6))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 46: - if (yyn == 46) - -/* Line 353 of lalr1.java */ -/* Line 527 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: IF cmp THEN elseif_stmt_list else_stmt END"); - if_stmt p = ((Yylex)yylexer).new_node(if_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (6-(2)))))).ival); - p.m_cmp = (cmp_stmt)((ParserVal)((yystack.valueAt (6-(2))))).obj; - p.m_block = null; - p.m_elseifs = (elseif_stmt_list)((ParserVal)((yystack.valueAt (6-(4))))).obj; - p.m_elses = (else_stmt)((ParserVal)((yystack.valueAt (6-(5))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 47: - if (yyn == 47) - -/* Line 353 of lalr1.java */ -/* Line 543 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: empty"); - - ParserVal ret = new ParserVal(null); - ret.ival = ((Yylex)yylexer).get_mybison().get_jflex().get_line(); - yyval = ret; - }; - break; - - - case 48: - if (yyn == 48) - -/* Line 353 of lalr1.java */ -/* Line 552 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: elseif_stmt_list elseif_stmt"); - elseif_stmt_list p = (elseif_stmt_list)((ParserVal)((yystack.valueAt (2-(1))))).obj; - p.add_stmt((syntree_node)((ParserVal)((yystack.valueAt (2-(2))))).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 49: - if (yyn == 49) - -/* Line 353 of lalr1.java */ -/* Line 563 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: elseif_stmt"); - elseif_stmt_list p = ((Yylex)yylexer).new_node(elseif_stmt_list.class, ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).ival); - p.add_stmt((syntree_node)((ParserVal)((yystack.valueAt (1-(1))))).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 50: - if (yyn == 50) - -/* Line 353 of lalr1.java */ -/* Line 576 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: ELSEIF cmp THEN block"); - elseif_stmt p = ((Yylex)yylexer).new_node(elseif_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (4-(2)))))).ival); - p.m_cmp = (cmp_stmt)((ParserVal)((yystack.valueAt (4-(2))))).obj; - p.m_block = (syntree_node)((ParserVal)((yystack.valueAt (4-(4))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 51: - if (yyn == 51) - -/* Line 353 of lalr1.java */ -/* Line 588 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: ELSEIF cmp THEN"); - elseif_stmt p = ((Yylex)yylexer).new_node(elseif_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(2)))))).ival); - p.m_cmp = (cmp_stmt)((ParserVal)((yystack.valueAt (3-(2))))).obj; - p.m_block = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 52: - if (yyn == 52) - -/* Line 353 of lalr1.java */ -/* Line 602 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: empty"); - - ParserVal ret = new ParserVal(null); - ret.ival = ((Yylex)yylexer).get_mybison().get_jflex().get_line(); - yyval = ret; - }; - break; - - - case 53: - if (yyn == 53) - -/* Line 353 of lalr1.java */ -/* Line 611 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: ELSE block"); - else_stmt p = ((Yylex)yylexer).new_node(else_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (2-(2)))))).ival); - p.m_block = (block_node)((ParserVal)((yystack.valueAt (2-(2))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 54: - if (yyn == 54) - -/* Line 353 of lalr1.java */ -/* Line 622 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: ELSE"); - else_stmt p = ((Yylex)yylexer).new_node(else_stmt.class, ((Yylex)yylexer).get_mybison().get_jflex().get_line()); - p.m_block = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 55: - if (yyn == 55) - -/* Line 353 of lalr1.java */ -/* Line 635 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: OPEN_BRACKET cmp CLOSE_BRACKET"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (3-(2)))))); - }; - break; - - - case 56: - if (yyn == 56) - -/* Line 353 of lalr1.java */ -/* Line 641 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: cmp AND cmp"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_cmp = "&&"; - p.m_left = (syntree_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_right = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 57: - if (yyn == 57) - -/* Line 353 of lalr1.java */ -/* Line 654 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: cmp OR cmp"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_cmp = "||"; - p.m_left = (syntree_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_right = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 58: - if (yyn == 58) - -/* Line 353 of lalr1.java */ -/* Line 667 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: cmp_value LESS cmp_value"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_cmp = "<"; - p.m_left = (syntree_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_right = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 59: - if (yyn == 59) - -/* Line 353 of lalr1.java */ -/* Line 680 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: cmp_value MORE cmp_value"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_cmp = ">"; - p.m_left = (syntree_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_right = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 60: - if (yyn == 60) - -/* Line 353 of lalr1.java */ -/* Line 693 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: cmp_value EQUAL cmp_value"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_cmp = "=="; - p.m_left = (syntree_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_right = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 61: - if (yyn == 61) - -/* Line 353 of lalr1.java */ -/* Line 706 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: cmp_value MORE_OR_EQUAL cmp_value"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_cmp = ">="; - p.m_left = (syntree_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_right = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 62: - if (yyn == 62) - -/* Line 353 of lalr1.java */ -/* Line 719 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: cmp_value LESS_OR_EQUAL cmp_value"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_cmp = "<="; - p.m_left = (syntree_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_right = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 63: - if (yyn == 63) - -/* Line 353 of lalr1.java */ -/* Line 732 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: cmp_value NOT_EQUAL cmp_value"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_cmp = "!="; - p.m_left = (syntree_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_right = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 64: - if (yyn == 64) - -/* Line 353 of lalr1.java */ -/* Line 745 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FTRUE"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((Yylex)yylexer).get_mybison().get_jflex().get_line()); - p.m_cmp = "true"; - p.m_left = null; - p.m_right = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 65: - if (yyn == 65) - -/* Line 353 of lalr1.java */ -/* Line 758 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FFALSE"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((Yylex)yylexer).get_mybison().get_jflex().get_line()); - p.m_cmp = "false"; - p.m_left = null; - p.m_right = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 66: - if (yyn == 66) - -/* Line 353 of lalr1.java */ -/* Line 771 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: IS cmp_value"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (2-(2)))))).ival); - p.m_cmp = "is"; - p.m_left = (syntree_node)((ParserVal)((yystack.valueAt (2-(2))))).obj; - p.m_right = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 67: - if (yyn == 67) - -/* Line 353 of lalr1.java */ -/* Line 784 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: NOT cmp_value"); - cmp_stmt p = ((Yylex)yylexer).new_node(cmp_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (2-(2)))))).ival); - p.m_cmp = "not"; - p.m_left = (syntree_node)((ParserVal)((yystack.valueAt (2-(2))))).obj; - p.m_right = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 68: - if (yyn == 68) - -/* Line 353 of lalr1.java */ -/* Line 799 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: explicit_value"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 69: - if (yyn == 69) - -/* Line 353 of lalr1.java */ -/* Line 805 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 70: - if (yyn == 70) - -/* Line 353 of lalr1.java */ -/* Line 811 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: expr"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 71: - if (yyn == 71) - -/* Line 353 of lalr1.java */ -/* Line 819 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: RETURN return_value_list"); - return_stmt p = ((Yylex)yylexer).new_node(return_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (2-(2)))))).ival); - p.m_returnlist = (return_value_list_node)((ParserVal)((yystack.valueAt (2-(2))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 72: - if (yyn == 72) - -/* Line 353 of lalr1.java */ -/* Line 830 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: RETURN"); - return_stmt p = ((Yylex)yylexer).new_node(return_stmt.class, ((Yylex)yylexer).get_mybison().get_jflex().get_line()); - p.m_returnlist = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 73: - if (yyn == 73) - -/* Line 353 of lalr1.java */ -/* Line 843 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: return_value_list ARG_SPLITTER return_value"); - return_value_list_node p = (return_value_list_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.add_arg((syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 74: - if (yyn == 74) - -/* Line 353 of lalr1.java */ -/* Line 854 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: return_value"); - return_value_list_node p = ((Yylex)yylexer).new_node(return_value_list_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).ival); - p.add_arg((syntree_node)((ParserVal)((yystack.valueAt (1-(1))))).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 75: - if (yyn == 75) - -/* Line 353 of lalr1.java */ -/* Line 867 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: explicit_value"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 76: - if (yyn == 76) - -/* Line 353 of lalr1.java */ -/* Line 873 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 77: - if (yyn == 77) - -/* Line 353 of lalr1.java */ -/* Line 879 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: expr"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 78: - if (yyn == 78) - -/* Line 353 of lalr1.java */ -/* Line 887 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: var ASSIGN assign_value"); - assign_stmt p = ((Yylex)yylexer).new_node(assign_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_var = (syntree_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_value = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - p.m_isnew = false; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 79: - if (yyn == 79) - -/* Line 353 of lalr1.java */ -/* Line 900 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: var NEW_ASSIGN assign_value"); - assign_stmt p = ((Yylex)yylexer).new_node(assign_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_var = (syntree_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_value = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - p.m_isnew = true; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 80: - if (yyn == 80) - -/* Line 353 of lalr1.java */ -/* Line 915 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: var_list ASSIGN function_call"); - multi_assign_stmt p = ((Yylex)yylexer).new_node(multi_assign_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_varlist = (var_list_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_value = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - p.m_isnew = false; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 81: - if (yyn == 81) - -/* Line 353 of lalr1.java */ -/* Line 928 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: var_list NEW_ASSIGN function_call"); - multi_assign_stmt p = ((Yylex)yylexer).new_node(multi_assign_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_varlist = (var_list_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_value = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - p.m_isnew = true; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 82: - if (yyn == 82) - -/* Line 353 of lalr1.java */ -/* Line 943 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: var_list ARG_SPLITTER var"); - var_list_node p = (var_list_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.add_arg((syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 83: - if (yyn == 83) - -/* Line 353 of lalr1.java */ -/* Line 954 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: var"); - var_list_node p = ((Yylex)yylexer).new_node(var_list_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).ival); - p.add_arg((syntree_node)((ParserVal)((yystack.valueAt (1-(1))))).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 84: - if (yyn == 84) - -/* Line 353 of lalr1.java */ -/* Line 967 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: explicit_value"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 85: - if (yyn == 85) - -/* Line 353 of lalr1.java */ -/* Line 973 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 86: - if (yyn == 86) - -/* Line 353 of lalr1.java */ -/* Line 979 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: expr"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 87: - if (yyn == 87) - -/* Line 353 of lalr1.java */ -/* Line 987 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable PLUS_ASSIGN assign_value"); - math_assign_stmt p = ((Yylex)yylexer).new_node(math_assign_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_var = (syntree_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_oper = "+="; - p.m_value = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 88: - if (yyn == 88) - -/* Line 353 of lalr1.java */ -/* Line 1000 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable MINUS_ASSIGN assign_value"); - math_assign_stmt p = ((Yylex)yylexer).new_node(math_assign_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_var = (syntree_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_oper = "-="; - p.m_value = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 89: - if (yyn == 89) - -/* Line 353 of lalr1.java */ -/* Line 1013 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable DIVIDE_ASSIGN assign_value"); - math_assign_stmt p = ((Yylex)yylexer).new_node(math_assign_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_var = (syntree_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_oper = "/="; - p.m_value = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 90: - if (yyn == 90) - -/* Line 353 of lalr1.java */ -/* Line 1026 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable MULTIPLY_ASSIGN assign_value"); - math_assign_stmt p = ((Yylex)yylexer).new_node(math_assign_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_var = (syntree_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_oper = "*="; - p.m_value = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 91: - if (yyn == 91) - -/* Line 353 of lalr1.java */ -/* Line 1039 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable DIVIDE_MOD_ASSIGN assign_value"); - math_assign_stmt p = ((Yylex)yylexer).new_node(math_assign_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_var = (syntree_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_oper = "%="; - p.m_value = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 92: - if (yyn == 92) - -/* Line 353 of lalr1.java */ -/* Line 1052 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable INC"); - explicit_value_node pp = ((Yylex)yylexer).new_node(explicit_value_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (2-(1)))))).ival); - pp.m_str = "1"; - pp.m_type = explicit_value_type.EVT_NUM; - - math_assign_stmt p = ((Yylex)yylexer).new_node(math_assign_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (2-(1)))))).ival); - p.m_var = (syntree_node)((ParserVal)((yystack.valueAt (2-(1))))).obj; - p.m_oper = "+="; - p.m_value = pp; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 93: - if (yyn == 93) - -/* Line 353 of lalr1.java */ -/* Line 1071 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: VAR_BEGIN IDENTIFIER"); - var_node p = ((Yylex)yylexer).new_node(var_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (2-(2)))))).ival); - p.m_str = ((ParserVal)((ParserVal)((yystack.valueAt (2-(2)))))).sval; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 94: - if (yyn == 94) - -/* Line 353 of lalr1.java */ -/* Line 1082 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 95: - if (yyn == 95) - -/* Line 353 of lalr1.java */ -/* Line 1090 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: IDENTIFIER"); - variable_node p = ((Yylex)yylexer).new_node(variable_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).ival); - p.m_str = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).sval; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 96: - if (yyn == 96) - -/* Line 353 of lalr1.java */ -/* Line 1101 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: IDENTIFIER OPEN_SQUARE_BRACKET expr_value CLOSE_SQUARE_BRACKET"); - container_get_node p = ((Yylex)yylexer).new_node(container_get_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (4-(1)))))).ival); - p.m_container = ((ParserVal)((ParserVal)((yystack.valueAt (4-(1)))))).sval; - p.m_key = (syntree_node)((ParserVal)((yystack.valueAt (4-(3))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 97: - if (yyn == 97) - -/* Line 353 of lalr1.java */ -/* Line 1113 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: IDENTIFIER_POINTER"); - struct_pointer_node p = ((Yylex)yylexer).new_node(struct_pointer_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).ival); - p.m_str = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).sval; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 98: - if (yyn == 98) - -/* Line 353 of lalr1.java */ -/* Line 1124 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: IDENTIFIER_DOT"); - variable_node p = ((Yylex)yylexer).new_node(variable_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).ival); - p.m_str = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).sval; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 99: - if (yyn == 99) - -/* Line 353 of lalr1.java */ -/* Line 1137 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: OPEN_BRACKET expr CLOSE_BRACKET"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (3-(2)))))); - }; - break; - - - case 100: - if (yyn == 100) - -/* Line 353 of lalr1.java */ -/* Line 1143 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: function_call"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 101: - if (yyn == 101) - -/* Line 353 of lalr1.java */ -/* Line 1149 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: math_expr"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 102: - if (yyn == 102) - -/* Line 353 of lalr1.java */ -/* Line 1157 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: OPEN_BRACKET math_expr CLOSE_BRACKET"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (3-(2)))))); - }; - break; - - - case 103: - if (yyn == 103) - -/* Line 353 of lalr1.java */ -/* Line 1163 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: expr_value PLUS expr_value"); - math_expr_node p = ((Yylex)yylexer).new_node(math_expr_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_oper = "+"; - p.m_left = (syntree_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_right = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 104: - if (yyn == 104) - -/* Line 353 of lalr1.java */ -/* Line 1176 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: expr_value MINUS expr_value"); - math_expr_node p = ((Yylex)yylexer).new_node(math_expr_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_oper = "-"; - p.m_left = (syntree_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_right = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 105: - if (yyn == 105) - -/* Line 353 of lalr1.java */ -/* Line 1189 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: expr_value MULTIPLY expr_value"); - math_expr_node p = ((Yylex)yylexer).new_node(math_expr_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_oper = "*"; - p.m_left = (syntree_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_right = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 106: - if (yyn == 106) - -/* Line 353 of lalr1.java */ -/* Line 1202 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: expr_value DIVIDE expr_value"); - math_expr_node p = ((Yylex)yylexer).new_node(math_expr_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_oper = "/"; - p.m_left = (syntree_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_right = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 107: - if (yyn == 107) - -/* Line 353 of lalr1.java */ -/* Line 1215 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: expr_value DIVIDE_MOD expr_value"); - math_expr_node p = ((Yylex)yylexer).new_node(math_expr_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_oper = "%"; - p.m_left = (syntree_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_right = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 108: - if (yyn == 108) - -/* Line 353 of lalr1.java */ -/* Line 1228 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: expr_value STRING_CAT expr_value"); - math_expr_node p = ((Yylex)yylexer).new_node(math_expr_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_oper = ".."; - p.m_left = (syntree_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_right = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 109: - if (yyn == 109) - -/* Line 353 of lalr1.java */ -/* Line 1243 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: math_expr"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 110: - if (yyn == 110) - -/* Line 353 of lalr1.java */ -/* Line 1249 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: explicit_value"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 111: - if (yyn == 111) - -/* Line 353 of lalr1.java */ -/* Line 1255 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: function_call"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 112: - if (yyn == 112) - -/* Line 353 of lalr1.java */ -/* Line 1261 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: variable"); - yyval = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))); - }; - break; - - - case 113: - if (yyn == 113) - -/* Line 353 of lalr1.java */ -/* Line 1269 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: BREAK"); - break_stmt p = ((Yylex)yylexer).new_node(break_stmt.class, ((Yylex)yylexer).get_mybison().get_jflex().get_line()); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 114: - if (yyn == 114) - -/* Line 353 of lalr1.java */ -/* Line 1281 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: CONTINUE"); - continue_stmt p = ((Yylex)yylexer).new_node(continue_stmt.class, ((Yylex)yylexer).get_mybison().get_jflex().get_line()); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 115: - if (yyn == 115) - -/* Line 353 of lalr1.java */ -/* Line 1293 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: SLEEP"); - sleep_stmt p = ((Yylex)yylexer).new_node(sleep_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (2-(2)))))).ival); - p.m_time = (syntree_node)((ParserVal)((yystack.valueAt (2-(2))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 116: - if (yyn == 116) - -/* Line 353 of lalr1.java */ -/* Line 1306 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: YIELD"); - yield_stmt p = ((Yylex)yylexer).new_node(yield_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (2-(2)))))).ival); - p.m_time = (syntree_node)((ParserVal)((yystack.valueAt (2-(2))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 117: - if (yyn == 117) - -/* Line 353 of lalr1.java */ -/* Line 1319 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: SWITCH cmp_value switch_case_list DEFAULT block END"); - switch_stmt p = ((Yylex)yylexer).new_node(switch_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (6-(2)))))).ival); - p.m_cmp = (syntree_node)((ParserVal)((yystack.valueAt (6-(2))))).obj; - p.m_caselist = (syntree_node)((ParserVal)((yystack.valueAt (6-(3))))).obj; - p.m_def = (syntree_node)((ParserVal)((yystack.valueAt (6-(5))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 118: - if (yyn == 118) - -/* Line 353 of lalr1.java */ -/* Line 1332 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: SWITCH cmp_value switch_case_list DEFAULT END"); - switch_stmt p = ((Yylex)yylexer).new_node(switch_stmt.class, ((ParserVal)((ParserVal)((yystack.valueAt (5-(2)))))).ival); - p.m_cmp = (syntree_node)((ParserVal)((yystack.valueAt (5-(2))))).obj; - p.m_caselist = (syntree_node)((ParserVal)((yystack.valueAt (5-(3))))).obj; - p.m_def = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 119: - if (yyn == 119) - -/* Line 353 of lalr1.java */ -/* Line 1347 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: switch_case_define"); - switch_caselist_node p = ((Yylex)yylexer).new_node(switch_caselist_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).ival); - p.add_case((syntree_node)((ParserVal)((yystack.valueAt (1-(1))))).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 120: - if (yyn == 120) - -/* Line 353 of lalr1.java */ -/* Line 1358 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: switch_case_list switch_case_define"); - switch_caselist_node p = (switch_caselist_node)((ParserVal)((yystack.valueAt (2-(1))))).obj; - p.add_case((syntree_node)((ParserVal)((yystack.valueAt (2-(2))))).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 121: - if (yyn == 121) - -/* Line 353 of lalr1.java */ -/* Line 1371 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: CASE cmp_value THEN block"); - switch_case_node p = ((Yylex)yylexer).new_node(switch_case_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (4-(2)))))).ival); - p.m_cmp = (syntree_node)((ParserVal)((yystack.valueAt (4-(2))))).obj; - p.m_block = (syntree_node)((ParserVal)((yystack.valueAt (4-(4))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 122: - if (yyn == 122) - -/* Line 353 of lalr1.java */ -/* Line 1383 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: CASE cmp_value THEN"); - switch_case_node p = ((Yylex)yylexer).new_node(switch_case_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(2)))))).ival); - p.m_cmp = (syntree_node)((ParserVal)((yystack.valueAt (3-(2))))).obj; - p.m_block = null; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 123: - if (yyn == 123) - -/* Line 353 of lalr1.java */ -/* Line 1397 of "YYParser.y" */ - { - }; - break; - - - case 124: - if (yyn == 124) - -/* Line 353 of lalr1.java */ -/* Line 1401 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: PACKAGE IDENTIFIER "); - ((Yylex)yylexer).get_mybison().set_package(((ParserVal)((yystack.valueAt (2-(2))))).sval); - }; - break; - - - case 125: - if (yyn == 125) - -/* Line 353 of lalr1.java */ -/* Line 1407 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: PACKAGE IDENTIFIER_DOT "); - ((Yylex)yylexer).get_mybison().set_package(((ParserVal)((yystack.valueAt (2-(2))))).sval); - }; - break; - - - case 126: - if (yyn == 126) - -/* Line 353 of lalr1.java */ -/* Line 1415 of "YYParser.y" */ - { - }; - break; - - - case 129: - if (yyn == 129) - -/* Line 353 of lalr1.java */ -/* Line 1425 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: INCLUDE STRING_DEFINITION "); - ((Yylex)yylexer).get_mybison().add_include(((ParserVal)((yystack.valueAt (2-(2))))).sval); - }; - break; - - - case 130: - if (yyn == 130) - -/* Line 353 of lalr1.java */ -/* Line 1433 of "YYParser.y" */ - { - }; - break; - - - case 133: - if (yyn == 133) - -/* Line 353 of lalr1.java */ -/* Line 1443 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: STRUCT IDENTIFIER struct_mem_declaration END "); - ((Yylex)yylexer).get_mybison().add_struct_desc(((ParserVal)((yystack.valueAt (4-(2))))).sval); - }; - break; - - - case 134: - if (yyn == 134) - -/* Line 353 of lalr1.java */ -/* Line 1451 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: struct_mem_declaration IDENTIFIER "); - }; - break; - - - case 135: - if (yyn == 135) - -/* Line 353 of lalr1.java */ -/* Line 1456 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: IDENTIFIER "); - }; - break; - - - case 136: - if (yyn == 136) - -/* Line 353 of lalr1.java */ -/* Line 1463 of "YYParser.y" */ - { - }; - break; - - - case 139: - if (yyn == 139) - -/* Line 353 of lalr1.java */ -/* Line 1473 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FCONST IDENTIFIER ASSIGN explicit_value "); - ((Yylex)yylexer).get_mybison().add_const_desc(((ParserVal)((yystack.valueAt (4-(2))))).sval, (syntree_node)((ParserVal)((yystack.valueAt (4-(4))))).obj); - }; - break; - - - case 140: - if (yyn == 140) - -/* Line 353 of lalr1.java */ -/* Line 1481 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: NULL "); - explicit_value_node p = ((Yylex)yylexer).new_node(explicit_value_node.class, ((Yylex)yylexer).get_mybison().get_jflex().get_line() + 1); - p.m_type = explicit_value_type.EVT_NULL; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 141: - if (yyn == 141) - -/* Line 353 of lalr1.java */ -/* Line 1492 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FTRUE "); - explicit_value_node p = ((Yylex)yylexer).new_node(explicit_value_node.class, ((Yylex)yylexer).get_mybison().get_jflex().get_line() + 1); - p.m_type = explicit_value_type.EVT_TRUE; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 142: - if (yyn == 142) - -/* Line 353 of lalr1.java */ -/* Line 1503 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FFALSE "); - explicit_value_node p = ((Yylex)yylexer).new_node(explicit_value_node.class, ((Yylex)yylexer).get_mybison().get_jflex().get_line() + 1); - p.m_type = explicit_value_type.EVT_FALSE; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 143: - if (yyn == 143) - -/* Line 353 of lalr1.java */ -/* Line 1514 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: NUMBER "); - explicit_value_node p = ((Yylex)yylexer).new_node(explicit_value_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).ival); - p.m_str = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).sval; - p.m_type = explicit_value_type.EVT_NUM; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 144: - if (yyn == 144) - -/* Line 353 of lalr1.java */ -/* Line 1526 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FKUUID "); - explicit_value_node p = ((Yylex)yylexer).new_node(explicit_value_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).ival); - p.m_str = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).sval; - p.m_type = explicit_value_type.EVT_UUID; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 145: - if (yyn == 145) - -/* Line 353 of lalr1.java */ -/* Line 1538 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: STRING_DEFINITION "); - explicit_value_node p = ((Yylex)yylexer).new_node(explicit_value_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).ival); - p.m_str = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).sval; - p.m_type = explicit_value_type.EVT_STR; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 146: - if (yyn == 146) - -/* Line 353 of lalr1.java */ -/* Line 1550 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: FKFLOAT "); - explicit_value_node p = ((Yylex)yylexer).new_node(explicit_value_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).ival); - p.m_str = ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).sval; - p.m_type = explicit_value_type.EVT_FLOAT; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 147: - if (yyn == 147) - -/* Line 353 of lalr1.java */ -/* Line 1562 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: OPEN_BIG_BRACKET const_map_list_value CLOSE_BIG_BRACKET "); - explicit_value_node p = ((Yylex)yylexer).new_node(explicit_value_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(2)))))).ival); - p.m_type = explicit_value_type.EVT_MAP; - p.m_v = (const_map_list_value_node)((ParserVal)((yystack.valueAt (3-(2))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 148: - if (yyn == 148) - -/* Line 353 of lalr1.java */ -/* Line 1574 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: OPEN_BIG_BRACKET const_array_list_value CLOSE_BIG_BRACKET "); - explicit_value_node p = ((Yylex)yylexer).new_node(explicit_value_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(2)))))).ival); - p.m_type = explicit_value_type.EVT_ARRAY; - p.m_v = (const_array_list_value_node)((ParserVal)((yystack.valueAt (3-(2))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 149: - if (yyn == 149) - -/* Line 353 of lalr1.java */ -/* Line 1589 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: empty "); - const_map_list_value_node p = ((Yylex)yylexer).new_node(const_map_list_value_node.class, ((Yylex)yylexer).get_mybison().get_jflex().get_line()); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 150: - if (yyn == 150) - -/* Line 353 of lalr1.java */ -/* Line 1599 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: const_map_value "); - const_map_list_value_node p = ((Yylex)yylexer).new_node(const_map_list_value_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).ival); - p.add_ele((const_map_value_node)((ParserVal)((yystack.valueAt (1-(1))))).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 151: - if (yyn == 151) - -/* Line 353 of lalr1.java */ -/* Line 1610 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: const_map_list_value const_map_value "); - const_map_list_value_node p = (const_map_list_value_node)((ParserVal)((yystack.valueAt (2-(1))))).obj; - p.add_ele((const_map_value_node)((ParserVal)((yystack.valueAt (2-(2))))).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 152: - if (yyn == 152) - -/* Line 353 of lalr1.java */ -/* Line 1624 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: explicit_value COLON explicit_value "); - const_map_value_node p = ((Yylex)yylexer).new_node(const_map_value_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (3-(1)))))).ival); - p.m_k = (syntree_node)((ParserVal)((yystack.valueAt (3-(1))))).obj; - p.m_v = (syntree_node)((ParserVal)((yystack.valueAt (3-(3))))).obj; - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 153: - if (yyn == 153) - -/* Line 353 of lalr1.java */ -/* Line 1638 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: empty "); - const_array_list_value_node p = ((Yylex)yylexer).new_node(const_array_list_value_node.class, ((Yylex)yylexer).get_mybison().get_jflex().get_line()); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 154: - if (yyn == 154) - -/* Line 353 of lalr1.java */ -/* Line 1648 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: explicit_value "); - const_array_list_value_node p = ((Yylex)yylexer).new_node(const_array_list_value_node.class, ((ParserVal)((ParserVal)((yystack.valueAt (1-(1)))))).ival); - p.add_ele((explicit_value_node)((ParserVal)((yystack.valueAt (1-(1))))).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - case 155: - if (yyn == 155) - -/* Line 353 of lalr1.java */ -/* Line 1659 of "YYParser.y" */ - { - types.log(((Yylex)yylexer).get_mybison().get_fake(), "[BISON]: const_array_list_value explicit_value "); - const_array_list_value_node p = (const_array_list_value_node)((ParserVal)((yystack.valueAt (2-(1))))).obj; - p.add_ele((explicit_value_node)((ParserVal)((yystack.valueAt (2-(2))))).obj); - - ParserVal ret = new ParserVal(p); - ret.ival = p.m_lno; - yyval = ret; - }; - break; - - - - -/* Line 353 of lalr1.java */ -/* Line 2738 of "YYParser.java" */ - default: break; - } - - yy_symbol_print ("-> $$ =", yyr1_[yyn], yyval); - - yystack.pop (yylen); - yylen = 0; - - /* Shift the result of the reduction. */ - yyn = yyr1_[yyn]; - int yystate = yypgoto_[yyn - yyntokens_] + yystack.stateAt (0); - if (0 <= yystate && yystate <= yylast_ - && yycheck_[yystate] == yystack.stateAt (0)) - yystate = yytable_[yystate]; - else - yystate = yydefgoto_[yyn - yyntokens_]; - - yystack.push (yystate, yyval); - return YYNEWSTATE; - } - - /* Return YYSTR after stripping away unnecessary quotes and - backslashes, so that it's suitable for yyerror. The heuristic is - that double-quoting is unnecessary unless the string contains an - apostrophe, a comma, or backslash (other than backslash-backslash). - YYSTR is taken from yytname. */ - private final String yytnamerr_ (String yystr) - { - if (yystr.charAt (0) == '"') - { - StringBuffer yyr = new StringBuffer (); - strip_quotes: for (int i = 1; i < yystr.length (); i++) - switch (yystr.charAt (i)) - { - case '\'': - case ',': - break strip_quotes; - - case '\\': - if (yystr.charAt(++i) != '\\') - break strip_quotes; - /* Fall through. */ - default: - yyr.append (yystr.charAt (i)); - break; - - case '"': - return yyr.toString (); - } - } - else if (yystr.equals ("$end")) - return "end of input"; - - return yystr; - } - - /*--------------------------------. - | Print this symbol on YYOUTPUT. | - `--------------------------------*/ - - private void yy_symbol_print (String s, int yytype, - Object yyvaluep ) - { - if (yydebug > 0) - yycdebug (s + (yytype < yyntokens_ ? " token " : " nterm ") - + yytname_[yytype] + " (" - + (yyvaluep == null ? "(null)" : yyvaluep.toString ()) + ")"); - } - - /** - * Parse input from the scanner that was specified at object construction - * time. Return whether the end of the input was reached successfully. - * - * @return true if the parsing succeeds. Note that this does not - * imply that there were no syntax errors. - */ - public boolean parse () throws java.io.IOException - { - /// Lookahead and lookahead in internal form. - int yychar = yyempty_; - int yytoken = 0; - - /* State. */ - int yyn = 0; - int yylen = 0; - int yystate = 0; - - YYStack yystack = new YYStack (); - - /* Error handling. */ - int yynerrs_ = 0; - - - /// Semantic value of the lookahead. - Object yylval = null; - - int yyresult; - - yycdebug ("Starting parse\n"); - yyerrstatus_ = 0; - - - /* Initialize the stack. */ - yystack.push (yystate, yylval); - - int label = YYNEWSTATE; - for (;;) - switch (label) - { - /* New state. Unlike in the C/C++ skeletons, the state is already - pushed when we come here. */ - case YYNEWSTATE: - yycdebug ("Entering state " + yystate + "\n"); - if (yydebug > 0) - yystack.print (yyDebugStream); - - /* Accept? */ - if (yystate == yyfinal_) - return true; - - /* Take a decision. First try without lookahead. */ - yyn = yypact_[yystate]; - if (yyn == yypact_ninf_) - { - label = YYDEFAULT; - break; - } - - /* Read a lookahead token. */ - if (yychar == yyempty_) - { - yycdebug ("Reading a token: "); - yychar = yylex (); - - yylval = yylexer.getLVal (); - } - - /* Convert token to internal form. */ - if (yychar <= EOF) - { - yychar = yytoken = EOF; - yycdebug ("Now at end of input.\n"); - } - else - { - yytoken = yytranslate_ (yychar); - yy_symbol_print ("Next token is", yytoken, - yylval); - } - - /* If the proper action on seeing token YYTOKEN is to reduce or to - detect an error, take that action. */ - yyn += yytoken; - if (yyn < 0 || yylast_ < yyn || yycheck_[yyn] != yytoken) - label = YYDEFAULT; - - /* <= 0 means reduce or error. */ - else if ((yyn = yytable_[yyn]) <= 0) - { - if (yyn == 0 || yyn == yytable_ninf_) - label = YYFAIL; - else - { - yyn = -yyn; - label = YYREDUCE; - } - } - - else - { - /* Shift the lookahead token. */ - yy_symbol_print ("Shifting", yytoken, - yylval); - - /* Discard the token being shifted. */ - yychar = yyempty_; - - /* Count tokens shifted since error; after three, turn off error - status. */ - if (yyerrstatus_ > 0) - --yyerrstatus_; - - yystate = yyn; - yystack.push (yystate, yylval); - label = YYNEWSTATE; - } - break; - - /*-----------------------------------------------------------. - | yydefault -- do the default action for the current state. | - `-----------------------------------------------------------*/ - case YYDEFAULT: - yyn = yydefact_[yystate]; - if (yyn == 0) - label = YYFAIL; - else - label = YYREDUCE; - break; - - /*-----------------------------. - | yyreduce -- Do a reduction. | - `-----------------------------*/ - case YYREDUCE: - yylen = yyr2_[yyn]; - label = yyaction (yyn, yystack, yylen); - yystate = yystack.stateAt (0); - break; - - /*------------------------------------. - | yyerrlab -- here on detecting error | - `------------------------------------*/ - case YYFAIL: - /* If not already recovering from an error, report this error. */ - if (yyerrstatus_ == 0) - { - ++yynerrs_; - yyerror (yysyntax_error (yystate, yytoken)); - } - - - if (yyerrstatus_ == 3) - { - /* If just tried and failed to reuse lookahead token after an - error, discard it. */ - - if (yychar <= EOF) - { - /* Return failure if at end of input. */ - if (yychar == EOF) - return false; - } - else - yychar = yyempty_; - } - - /* Else will try to reuse lookahead token after shifting the error - token. */ - label = YYERRLAB1; - break; - - /*---------------------------------------------------. - | errorlab -- error raised explicitly by YYERROR. | - `---------------------------------------------------*/ - case YYERROR: - - - /* Do not reclaim the symbols of the rule which action triggered - this YYERROR. */ - yystack.pop (yylen); - yylen = 0; - yystate = yystack.stateAt (0); - label = YYERRLAB1; - break; - - /*-------------------------------------------------------------. - | yyerrlab1 -- common code for both syntax error and YYERROR. | - `-------------------------------------------------------------*/ - case YYERRLAB1: - yyerrstatus_ = 3; /* Each real token shifted decrements this. */ - - for (;;) - { - yyn = yypact_[yystate]; - if (yyn != yypact_ninf_) - { - yyn += yyterror_; - if (0 <= yyn && yyn <= yylast_ && yycheck_[yyn] == yyterror_) - { - yyn = yytable_[yyn]; - if (0 < yyn) - break; - } - } - - /* Pop the current state because it cannot handle the error token. */ - if (yystack.height == 1) - return false; - - - yystack.pop (); - yystate = yystack.stateAt (0); - if (yydebug > 0) - yystack.print (yyDebugStream); - } - - - - /* Shift the error token. */ - yy_symbol_print ("Shifting", yystos_[yyn], - yylval); - - yystate = yyn; - yystack.push (yyn, yylval); - label = YYNEWSTATE; - break; - - /* Accept. */ - case YYACCEPT: - return true; - - /* Abort. */ - case YYABORT: - return false; - } - } - - // Generate an error message. - private String yysyntax_error (int yystate, int tok) - { - if (errorVerbose) - { - int yyn = yypact_[yystate]; - if (yypact_ninf_ < yyn && yyn <= yylast_) - { - StringBuffer res; - - /* Start YYX at -YYN if negative to avoid negative indexes in - YYCHECK. */ - int yyxbegin = yyn < 0 ? -yyn : 0; - - /* Stay within bounds of both yycheck and yytname. */ - int yychecklim = yylast_ - yyn + 1; - int yyxend = yychecklim < yyntokens_ ? yychecklim : yyntokens_; - int count = 0; - for (int x = yyxbegin; x < yyxend; ++x) - if (yycheck_[x + yyn] == x && x != yyterror_) - ++count; - - // FIXME: This method of building the message is not compatible - // with internationalization. - res = new StringBuffer ("syntax error, unexpected "); - res.append (yytnamerr_ (yytname_[tok])); - if (count < 5) - { - count = 0; - for (int x = yyxbegin; x < yyxend; ++x) - if (yycheck_[x + yyn] == x && x != yyterror_) - { - res.append (count++ == 0 ? ", expecting " : " or "); - res.append (yytnamerr_ (yytname_[x])); - } - } - return res.toString (); - } - } - - return "syntax error"; - } - - - /* YYPACT[STATE-NUM] -- Index in YYTABLE of the portion describing - STATE-NUM. */ - private static final short yypact_ninf_ = -210; - private static final short yypact_[] = - { - -33, -11, 49, 16, -210, -210, -210, 56, 103, -210, - -210, 79, -210, 107, -210, 87, 105, -210, -1, -210, - -210, 148, 122, 139, 158, -210, -210, -210, -210, 392, - 159, -210, -210, -210, -210, -210, -210, -210, 392, 392, - -210, -210, 174, -210, 197, 178, 345, -210, -210, -5, - -210, -210, -210, 392, -210, -210, 174, 511, -210, -210, - 195, 1223, -210, 22, 22, -210, 14, 1223, 1177, 46, - 190, -210, -210, 1232, 1232, 1223, 21, 577, -210, -210, - -210, -210, -210, -210, -210, -210, -210, 65, -210, 1, - 498, -210, 101, 344, -210, -210, -210, -210, -210, -210, - -210, 204, -210, 216, -210, 445, 169, 277, 22, 1223, - 1223, 58, 296, 216, -210, 445, 97, 1232, 1232, 186, - 198, 295, 595, 2, 17, 186, 1232, 1232, 17, -210, - 344, 344, 171, 1232, 214, -210, -210, 4, 46, 46, - 1223, 1223, 1223, 1223, 1223, 1223, 1223, 220, -210, 1232, - 1232, 1232, 1232, 1232, 1232, 1223, 257, 198, -210, -210, - 661, 22, 22, 1223, 1223, 1223, 1223, 1223, 1223, 679, - 53, -210, 344, 399, -210, -210, 22, 1223, 68, 208, - 1223, 156, -210, 125, 210, 201, -210, -210, -210, 17, - 17, -210, 216, -210, 445, -210, -210, -210, -210, -210, - -210, 217, 193, 264, 60, 10, -6, -210, -210, -210, - -210, 745, 95, 95, -210, -210, -210, -210, -210, -210, - 22, 679, -9, -210, 1232, -210, -210, 227, 189, -210, - 251, 811, -210, -210, 1232, 1232, -210, 134, -9, 1177, - -210, 266, -210, 1177, 1223, 1177, -210, 877, 160, 188, - 1177, 270, 1177, -210, 895, 245, 1177, -210, -210, -210, - 1177, -210, 961, 1232, -210, 1027, 0, -210, 1093, -210, - 1159, -210 - }; - - /* YYDEFACT[S] -- default rule to reduce with in state S when YYTABLE - doesn't specify something else to do. Zero means the default is an - error. */ - private static final short yydefact_[] = - { - 123, 0, 0, 126, 124, 125, 1, 0, 130, 127, - 129, 0, 128, 136, 131, 0, 0, 132, 3, 137, - 135, 0, 0, 0, 2, 4, 138, 133, 134, 0, - 0, 5, 141, 142, 145, 143, 146, 144, 153, 149, - 140, 139, 8, 154, 0, 0, 0, 150, 11, 0, - 10, 148, 155, 0, 147, 151, 0, 0, 152, 9, - 0, 72, 113, 0, 0, 7, 95, 0, 0, 0, - 98, 97, 114, 0, 0, 0, 100, 0, 22, 34, - 32, 33, 23, 24, 25, 26, 27, 0, 31, 83, - 112, 30, 101, 0, 28, 29, 35, 36, 37, 110, - 93, 71, 74, 76, 77, 75, 141, 142, 0, 0, - 0, 0, 0, 69, 70, 68, 0, 17, 0, 112, - 0, 101, 0, 83, 38, 0, 17, 0, 111, 109, - 116, 115, 0, 17, 0, 6, 21, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 92, 0, - 0, 0, 0, 0, 0, 0, 0, 70, 66, 67, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 47, - 0, 19, 20, 0, 99, 102, 0, 0, 0, 109, - 0, 0, 119, 0, 0, 95, 98, 82, 94, 80, - 81, 78, 85, 86, 84, 79, 87, 88, 89, 90, - 91, 0, 107, 103, 104, 106, 105, 108, 73, 55, - 44, 0, 56, 57, 59, 58, 61, 62, 60, 63, - 0, 47, 52, 49, 0, 12, 96, 0, 78, 13, - 0, 0, 120, 14, 17, 17, 43, 0, 52, 54, - 48, 0, 18, 0, 0, 122, 118, 0, 0, 0, - 51, 0, 53, 46, 0, 0, 121, 117, 16, 15, - 50, 45, 0, 0, 40, 0, 0, 39, 0, 42, - 0, 41 - }; - - /* YYPGOTO[NTERM-NUM]. */ - private static final short yypgoto_[] = - { - -210, -210, -210, 256, -210, 238, 181, -125, 73, -17, - 127, -210, -210, -210, -210, -210, 91, -209, 90, -58, - 267, -210, -210, 184, -210, -210, -210, 129, -210, -66, - 32, 332, 151, 219, -210, -210, -210, -210, -210, -210, - 162, -210, -210, 333, -210, 327, -210, -210, 326, -29, - -210, 300, -210 - }; - - /* YYDEFGOTO[NTERM-NUM]. */ - private static final short - yydefgoto_[] = - { - -1, 2, 24, 25, 49, 50, 76, 170, 171, 77, - 78, 79, 80, 81, 82, 83, 222, 223, 241, 111, - 112, 84, 101, 102, 85, 86, 87, 191, 88, 89, - 90, 91, 92, 93, 94, 95, 96, 97, 98, 181, - 182, 3, 8, 9, 13, 14, 21, 18, 19, 99, - 46, 47, 44 - }; - - /* YYTABLE[YYPACT[STATE-NUM]]. What to do in state STATE-NUM. If - positive, shift that token. If negative, reduce the rule which - number is the opposite. If zero, do what YYDEFACT says. */ - private static final short yytable_ninf_ = -113; - private static final short - yytable_[] = - { - 41, 178, 123, 239, 4, 23, 116, 60, 183, 43, - 45, 268, 149, 240, 56, 52, 1, 45, 149, 185, - 150, 151, 152, 153, 58, 140, 177, 57, 149, 240, - 106, 107, 105, 153, 115, 115, 34, 66, 35, -111, - 5, -111, -111, -111, -111, 117, 115, 16, 133, 6, - 156, 122, 133, 108, 220, 186, 71, 36, 134, 154, - 118, 66, 134, 141, 141, 154, 7, 37, 38, 160, - 10, 187, 224, 70, 71, 154, 109, 110, 149, 115, - 115, 115, 152, 153, 137, 225, -111, 224, 39, 138, - 40, 161, 162, 103, 15, 113, 113, 70, 71, 119, - 229, 125, 20, 212, 213, 119, 119, 113, 169, 248, - 249, 194, 194, 194, 194, 194, 194, 194, 227, -109, - 22, -109, -109, -109, -109, 154, 105, 139, 161, 162, - 161, 162, 115, 115, 115, 115, 115, 115, 115, 115, - 113, 113, 113, 211, 224, 250, 29, 115, 194, 119, - 119, 115, 221, 7, 30, 16, 11, 233, 119, 119, - 11, 27, 237, 28, 23, 119, -109, 161, 162, 188, - 125, 125, 192, 192, 192, 192, 192, 192, 192, 224, - -64, 119, 119, 119, 119, 119, 119, 103, -64, 48, - 42, 115, 258, 113, 113, 113, 113, 113, 113, 113, - 113, -64, -64, -64, 136, 32, 33, 224, 113, 192, - 100, 34, 113, 35, 247, 115, 180, 231, 121, 53, - 259, 126, 252, 155, 129, 129, 254, 147, 256, 184, - 174, 180, 36, 260, -112, 201, -112, -112, -112, -112, - 175, 234, 37, 38, 51, 265, 243, 118, 235, 136, - 124, 270, 113, 244, 128, 128, 119, 147, 154, 121, - 161, 162, 245, 39, 263, 40, 119, 119, 129, 129, - 195, 196, 197, 198, 199, 200, 113, 129, 179, 253, - 31, -112, 149, 261, 129, 151, 152, 153, -65, 209, - 161, 162, 130, 131, 59, 119, -65, 242, 128, 128, - 129, 129, 129, 129, 129, 129, 228, 128, 128, -65, - -65, -65, 238, -109, 128, -109, -109, -109, -109, 189, - 190, 163, 164, 165, 166, 167, 168, 175, 251, 154, - 128, 128, 128, 128, 128, 128, 172, 173, 136, 208, - 17, 12, 132, 232, 26, 172, 55, 0, 136, 0, - 0, 0, 172, 32, 33, 0, 0, 0, 0, 34, - -109, 35, 149, 0, 150, 151, 152, 153, 202, 203, - 204, 205, 206, 207, 136, 129, 158, 159, 0, 136, - 36, 136, 0, 136, 0, 129, 129, 136, 0, 0, - 37, 38, 136, 104, 0, 114, 114, 136, 0, 120, - 32, 33, 0, 0, 0, 128, 34, 114, 35, 154, - 0, 39, 54, 40, 129, 128, 128, 149, 0, 150, - 151, 152, 153, 0, 0, 0, 0, 36, 0, 0, - 214, 215, 216, 217, 218, 219, 0, 37, 38, 0, - 157, 114, 114, 172, 128, 0, 226, 230, 0, 0, - 0, 0, 0, 172, 172, 0, 0, 0, 39, 0, - 40, 0, 0, -110, 154, -110, -110, -110, -110, 0, - 0, 0, 193, 193, 193, 193, 193, 193, 193, 0, - 0, 0, 266, 0, 0, 0, 0, 104, 0, 0, - 0, 0, 0, 114, 114, 114, 114, 114, 114, 114, - 114, 0, 0, 0, 0, 0, 0, 0, 114, 193, - -110, 255, 114, 0, 60, 61, 62, -94, 63, 32, - 33, 64, -94, 0, 65, 34, 66, 35, 0, 0, - 0, 0, 0, 0, 142, 143, 144, 145, 146, 147, - 0, 148, 67, 0, 0, 0, 36, 0, 0, 0, - 0, 0, 114, 68, 0, 69, 37, 38, 0, 0, - -94, 0, 70, 71, 0, 0, 0, 72, 73, 74, - 75, 0, 0, 0, 0, 0, 114, 39, 0, 40, - 60, 61, 62, 0, 63, 32, 33, 64, 0, 0, - 135, 34, 66, 35, 0, 0, 0, 0, 60, 61, - 62, 0, 63, 32, 33, 64, 0, 0, 67, 34, - 66, 35, 36, 0, 176, 0, 0, 0, 0, 68, - 0, 69, 37, 38, 0, 0, 67, 0, 70, 71, - 36, 0, 0, 72, 73, 74, 75, 68, 0, 69, - 37, 38, 0, 39, 0, 40, 70, 71, 0, 0, - 0, 72, 73, 74, 75, 0, 0, 0, 0, 0, - 0, 39, 0, 40, 60, 61, 62, 0, 63, 32, - 33, 64, 0, 0, 210, 34, 66, 35, 0, 0, - 0, 0, 60, 61, 62, 0, 63, 32, 33, 64, - 0, 0, 67, 34, 66, 35, 36, 0, 0, 0, - 0, 0, 0, 68, 0, 69, 37, 38, 0, 0, - 67, 0, 70, 71, 36, 0, 0, 72, 73, 74, - 75, 68, 0, 69, 37, 38, 0, 39, 0, 40, - 70, 71, 0, 0, 0, 72, 73, 74, 75, 0, - 0, 0, 220, 0, 0, 39, 0, 40, 60, 61, - 62, 0, 63, 32, 33, 64, 0, 0, 236, 34, - 66, 35, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 67, 0, 0, 0, - 36, 0, 0, 0, 0, 0, 0, 68, 0, 69, - 37, 38, 0, 0, 0, 0, 70, 71, 0, 0, - 0, 72, 73, 74, 75, 0, 0, 0, 0, 0, - 0, 39, 0, 40, 60, 61, 62, 0, 63, 32, - 33, 64, 0, 0, 246, 34, 66, 35, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 67, 0, 0, 0, 36, 0, 0, 0, - 0, 0, 0, 68, 0, 69, 37, 38, 0, 0, - 0, 0, 70, 71, 0, 0, 0, 72, 73, 74, - 75, 0, 0, 0, 0, 0, 0, 39, 0, 40, - 60, 61, 62, 0, 63, 32, 33, 64, 0, 0, - 257, 34, 66, 35, 0, 0, 0, 0, 60, 61, - 62, 0, 63, 32, 33, 64, 262, 0, 67, 34, - 66, 35, 36, 0, 0, 0, 0, 0, 0, 68, - 0, 69, 37, 38, 0, 0, 67, 0, 70, 71, - 36, 0, 0, 72, 73, 74, 75, 68, 0, 69, - 37, 38, 0, 39, 0, 40, 70, 71, 0, 0, - 0, 72, 73, 74, 75, 0, 0, 0, 0, 0, - 0, 39, 0, 40, 60, 61, 62, 0, 63, 32, - 33, 64, 0, 0, 264, 34, 66, 35, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 67, 0, 0, 0, 36, 0, 0, 0, - 0, 0, 0, 68, 0, 69, 37, 38, 0, 0, - 0, 0, 70, 71, 0, 0, 0, 72, 73, 74, - 75, 0, 0, 0, 0, 0, 0, 39, 0, 40, - 60, 61, 62, 0, 63, 32, 33, 64, 0, 0, - 267, 34, 66, 35, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 67, 0, - 0, 0, 36, 0, 0, 0, 0, 0, 0, 68, - 0, 69, 37, 38, 0, 0, 0, 0, 70, 71, - 0, 0, 0, 72, 73, 74, 75, 0, 0, 0, - 0, 0, 0, 39, 0, 40, 60, 61, 62, 0, - 63, 32, 33, 64, 0, 0, 269, 34, 66, 35, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 67, 0, 0, 0, 36, 0, - 0, 0, 0, 0, 0, 68, 0, 69, 37, 38, - 0, 0, 0, 0, 70, 71, 0, 0, 0, 72, - 73, 74, 75, 0, 0, 0, 0, 0, 0, 39, - 0, 40, 60, 61, 62, 0, 63, 32, 33, 64, - 0, 0, 271, 34, 66, 35, 0, 0, 0, 0, - 60, 61, 62, 0, 63, 32, 33, 64, 0, 0, - 67, 34, 66, 35, 36, 0, 0, 0, 0, 0, - 0, 68, 0, 69, 37, 38, 0, 0, 67, 0, - 70, 71, 36, 0, 0, 72, 73, 74, 75, 68, - 0, 69, 37, 38, 0, 39, 0, 40, 70, 71, - 0, 32, 33, 72, 73, 74, 75, 34, 66, 35, - 32, 33, 0, 39, 0, 40, 34, 66, 35, 0, - 0, 0, 0, 0, 67, 0, 0, 0, 36, 0, - 0, 0, 0, 127, 0, 0, 0, 36, 37, 38, - 0, 0, 0, 0, 70, 71, 0, 37, 38, 0, - 0, 0, 0, 70, 71, 0, 0, 0, 0, 39, - 0, 40, 0, 0, 0, 0, 0, 0, 39, 0, - 40 - }; - - /* YYCHECK. */ - private static final short - yycheck_[] = - { - 29, 126, 68, 12, 15, 6, 64, 3, 133, 38, - 39, 11, 18, 222, 19, 44, 49, 46, 18, 15, - 20, 21, 22, 23, 53, 24, 24, 32, 18, 238, - 8, 9, 61, 23, 63, 64, 14, 15, 16, 18, - 51, 20, 21, 22, 23, 31, 75, 48, 31, 0, - 108, 68, 31, 31, 63, 51, 52, 35, 41, 65, - 46, 15, 41, 62, 62, 65, 50, 45, 46, 11, - 14, 137, 19, 51, 52, 65, 54, 55, 18, 108, - 109, 110, 22, 23, 19, 32, 65, 19, 66, 24, - 68, 33, 34, 61, 15, 63, 64, 51, 52, 67, - 32, 69, 15, 161, 162, 73, 74, 75, 11, 234, - 235, 140, 141, 142, 143, 144, 145, 146, 176, 18, - 15, 20, 21, 22, 23, 65, 155, 62, 33, 34, - 33, 34, 161, 162, 163, 164, 165, 166, 167, 168, - 108, 109, 110, 160, 19, 11, 24, 176, 177, 117, - 118, 180, 169, 50, 15, 48, 53, 32, 126, 127, - 53, 13, 220, 15, 6, 133, 65, 33, 34, 137, - 138, 139, 140, 141, 142, 143, 144, 145, 146, 19, - 11, 149, 150, 151, 152, 153, 154, 155, 19, 15, - 31, 220, 32, 161, 162, 163, 164, 165, 166, 167, - 168, 32, 33, 34, 77, 8, 9, 19, 176, 177, - 15, 14, 180, 16, 231, 244, 60, 61, 67, 41, - 32, 31, 239, 19, 73, 74, 243, 41, 245, 15, - 32, 60, 35, 250, 18, 15, 20, 21, 22, 23, - 32, 31, 45, 46, 47, 262, 19, 46, 31, 122, - 69, 268, 220, 64, 73, 74, 224, 41, 65, 108, - 33, 34, 11, 66, 19, 68, 234, 235, 117, 118, - 141, 142, 143, 144, 145, 146, 244, 126, 127, 13, - 24, 65, 18, 13, 133, 21, 22, 23, 11, 32, - 33, 34, 73, 74, 56, 263, 19, 224, 117, 118, - 149, 150, 151, 152, 153, 154, 177, 126, 127, 32, - 33, 34, 221, 18, 133, 20, 21, 22, 23, 138, - 139, 25, 26, 27, 28, 29, 30, 32, 238, 65, - 149, 150, 151, 152, 153, 154, 117, 118, 211, 155, - 13, 8, 75, 181, 18, 126, 46, -1, 221, -1, - -1, -1, 133, 8, 9, -1, -1, -1, -1, 14, - 65, 16, 18, -1, 20, 21, 22, 23, 149, 150, - 151, 152, 153, 154, 247, 224, 109, 110, -1, 252, - 35, 254, -1, 256, -1, 234, 235, 260, -1, -1, - 45, 46, 265, 61, -1, 63, 64, 270, -1, 67, - 8, 9, -1, -1, -1, 224, 14, 75, 16, 65, - -1, 66, 67, 68, 263, 234, 235, 18, -1, 20, - 21, 22, 23, -1, -1, -1, -1, 35, -1, -1, - 163, 164, 165, 166, 167, 168, -1, 45, 46, -1, - 108, 109, 110, 224, 263, -1, 47, 180, -1, -1, - -1, -1, -1, 234, 235, -1, -1, -1, 66, -1, - 68, -1, -1, 18, 65, 20, 21, 22, 23, -1, - -1, -1, 140, 141, 142, 143, 144, 145, 146, -1, - -1, -1, 263, -1, -1, -1, -1, 155, -1, -1, - -1, -1, -1, 161, 162, 163, 164, 165, 166, 167, - 168, -1, -1, -1, -1, -1, -1, -1, 176, 177, - 65, 244, 180, -1, 3, 4, 5, 19, 7, 8, - 9, 10, 24, -1, 13, 14, 15, 16, -1, -1, - -1, -1, -1, -1, 36, 37, 38, 39, 40, 41, - -1, 43, 31, -1, -1, -1, 35, -1, -1, -1, - -1, -1, 220, 42, -1, 44, 45, 46, -1, -1, - 62, -1, 51, 52, -1, -1, -1, 56, 57, 58, - 59, -1, -1, -1, -1, -1, 244, 66, -1, 68, - 3, 4, 5, -1, 7, 8, 9, 10, -1, -1, - 13, 14, 15, 16, -1, -1, -1, -1, 3, 4, - 5, -1, 7, 8, 9, 10, -1, -1, 31, 14, - 15, 16, 35, -1, 19, -1, -1, -1, -1, 42, - -1, 44, 45, 46, -1, -1, 31, -1, 51, 52, - 35, -1, -1, 56, 57, 58, 59, 42, -1, 44, - 45, 46, -1, 66, -1, 68, 51, 52, -1, -1, - -1, 56, 57, 58, 59, -1, -1, -1, -1, -1, - -1, 66, -1, 68, 3, 4, 5, -1, 7, 8, - 9, 10, -1, -1, 13, 14, 15, 16, -1, -1, - -1, -1, 3, 4, 5, -1, 7, 8, 9, 10, - -1, -1, 31, 14, 15, 16, 35, -1, -1, -1, - -1, -1, -1, 42, -1, 44, 45, 46, -1, -1, - 31, -1, 51, 52, 35, -1, -1, 56, 57, 58, - 59, 42, -1, 44, 45, 46, -1, 66, -1, 68, - 51, 52, -1, -1, -1, 56, 57, 58, 59, -1, - -1, -1, 63, -1, -1, 66, -1, 68, 3, 4, - 5, -1, 7, 8, 9, 10, -1, -1, 13, 14, - 15, 16, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, 31, -1, -1, -1, - 35, -1, -1, -1, -1, -1, -1, 42, -1, 44, - 45, 46, -1, -1, -1, -1, 51, 52, -1, -1, - -1, 56, 57, 58, 59, -1, -1, -1, -1, -1, - -1, 66, -1, 68, 3, 4, 5, -1, 7, 8, - 9, 10, -1, -1, 13, 14, 15, 16, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, 31, -1, -1, -1, 35, -1, -1, -1, - -1, -1, -1, 42, -1, 44, 45, 46, -1, -1, - -1, -1, 51, 52, -1, -1, -1, 56, 57, 58, - 59, -1, -1, -1, -1, -1, -1, 66, -1, 68, - 3, 4, 5, -1, 7, 8, 9, 10, -1, -1, - 13, 14, 15, 16, -1, -1, -1, -1, 3, 4, - 5, -1, 7, 8, 9, 10, 11, -1, 31, 14, - 15, 16, 35, -1, -1, -1, -1, -1, -1, 42, - -1, 44, 45, 46, -1, -1, 31, -1, 51, 52, - 35, -1, -1, 56, 57, 58, 59, 42, -1, 44, - 45, 46, -1, 66, -1, 68, 51, 52, -1, -1, - -1, 56, 57, 58, 59, -1, -1, -1, -1, -1, - -1, 66, -1, 68, 3, 4, 5, -1, 7, 8, - 9, 10, -1, -1, 13, 14, 15, 16, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, 31, -1, -1, -1, 35, -1, -1, -1, - -1, -1, -1, 42, -1, 44, 45, 46, -1, -1, - -1, -1, 51, 52, -1, -1, -1, 56, 57, 58, - 59, -1, -1, -1, -1, -1, -1, 66, -1, 68, - 3, 4, 5, -1, 7, 8, 9, 10, -1, -1, - 13, 14, 15, 16, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, 31, -1, - -1, -1, 35, -1, -1, -1, -1, -1, -1, 42, - -1, 44, 45, 46, -1, -1, -1, -1, 51, 52, - -1, -1, -1, 56, 57, 58, 59, -1, -1, -1, - -1, -1, -1, 66, -1, 68, 3, 4, 5, -1, - 7, 8, 9, 10, -1, -1, 13, 14, 15, 16, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, 31, -1, -1, -1, 35, -1, - -1, -1, -1, -1, -1, 42, -1, 44, 45, 46, - -1, -1, -1, -1, 51, 52, -1, -1, -1, 56, - 57, 58, 59, -1, -1, -1, -1, -1, -1, 66, - -1, 68, 3, 4, 5, -1, 7, 8, 9, 10, - -1, -1, 13, 14, 15, 16, -1, -1, -1, -1, - 3, 4, 5, -1, 7, 8, 9, 10, -1, -1, - 31, 14, 15, 16, 35, -1, -1, -1, -1, -1, - -1, 42, -1, 44, 45, 46, -1, -1, 31, -1, - 51, 52, 35, -1, -1, 56, 57, 58, 59, 42, - -1, 44, 45, 46, -1, 66, -1, 68, 51, 52, - -1, 8, 9, 56, 57, 58, 59, 14, 15, 16, - 8, 9, -1, 66, -1, 68, 14, 15, 16, -1, - -1, -1, -1, -1, 31, -1, -1, -1, 35, -1, - -1, -1, -1, 31, -1, -1, -1, 35, 45, 46, - -1, -1, -1, -1, 51, 52, -1, 45, 46, -1, - -1, -1, -1, 51, 52, -1, -1, -1, -1, 66, - -1, 68, -1, -1, -1, -1, -1, -1, 66, -1, - 68 - }; - - /* STOS_[STATE-NUM] -- The (internal number of the) accessing - symbol of state STATE-NUM. */ - private static final byte - yystos_[] = - { - 0, 49, 70, 110, 15, 51, 0, 50, 111, 112, - 14, 53, 112, 113, 114, 15, 48, 114, 116, 117, - 15, 115, 15, 6, 71, 72, 117, 13, 15, 24, - 15, 72, 8, 9, 14, 16, 35, 45, 46, 66, - 68, 118, 31, 118, 121, 118, 119, 120, 15, 73, - 74, 47, 118, 41, 67, 120, 19, 32, 118, 74, - 3, 4, 5, 7, 10, 13, 15, 31, 42, 44, - 51, 52, 56, 57, 58, 59, 75, 78, 79, 80, - 81, 82, 83, 84, 90, 93, 94, 95, 97, 98, - 99, 100, 101, 102, 103, 104, 105, 106, 107, 118, - 15, 91, 92, 99, 100, 118, 8, 9, 31, 54, - 55, 88, 89, 99, 100, 118, 88, 31, 46, 99, - 100, 101, 78, 98, 75, 99, 31, 31, 75, 101, - 102, 102, 89, 31, 41, 13, 79, 19, 24, 62, - 24, 62, 36, 37, 38, 39, 40, 41, 43, 18, - 20, 21, 22, 23, 65, 19, 88, 100, 89, 89, - 11, 33, 34, 25, 26, 27, 28, 29, 30, 11, - 76, 77, 102, 102, 32, 32, 19, 24, 76, 101, - 60, 108, 109, 76, 15, 15, 51, 98, 99, 75, - 75, 96, 99, 100, 118, 96, 96, 96, 96, 96, - 96, 15, 102, 102, 102, 102, 102, 102, 92, 32, - 13, 78, 88, 88, 89, 89, 89, 89, 89, 89, - 63, 78, 85, 86, 19, 32, 47, 88, 96, 32, - 89, 61, 109, 32, 31, 31, 13, 88, 85, 12, - 86, 87, 77, 19, 64, 11, 13, 78, 76, 76, - 11, 87, 78, 13, 78, 89, 78, 13, 32, 32, - 78, 13, 11, 19, 13, 78, 102, 13, 11, 13, - 78, 13 - }; - - /* TOKEN_NUMBER_[YYLEX-NUM] -- Internal symbol number corresponding - to YYLEX-NUM. */ - private static final short - yytoken_number_[] = - { - 0, 256, 257, 258, 259, 260, 261, 262, 263, 264, - 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, - 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, - 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, - 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, - 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, - 315, 316, 317, 318, 319, 320, 321, 322, 323 - }; - - /* YYR1[YYN] -- Symbol number of symbol that rule YYN derives. */ - private static final byte - yyr1_[] = - { - 0, 69, 70, 71, 71, 71, 72, 72, 73, 73, - 73, 74, 75, 75, 75, 75, 75, 76, 76, 76, - 77, 78, 78, 79, 79, 79, 79, 79, 79, 79, - 79, 79, 79, 79, 79, 79, 79, 79, 80, 81, - 81, 82, 82, 83, 83, 84, 84, 85, 85, 85, - 86, 86, 87, 87, 87, 88, 88, 88, 88, 88, - 88, 88, 88, 88, 88, 88, 88, 88, 89, 89, - 89, 90, 90, 91, 91, 92, 92, 92, 93, 93, - 94, 94, 95, 95, 96, 96, 96, 97, 97, 97, - 97, 97, 97, 98, 98, 99, 99, 99, 99, 100, - 100, 100, 101, 101, 101, 101, 101, 101, 101, 102, - 102, 102, 102, 103, 104, 105, 106, 107, 107, 108, - 108, 109, 109, 110, 110, 110, 111, 111, 111, 112, - 113, 113, 113, 114, 115, 115, 116, 116, 116, 117, - 118, 118, 118, 118, 118, 118, 118, 118, 118, 119, - 119, 119, 120, 121, 121, 121 - }; - - /* YYR2[YYN] -- Number of symbols composing right hand side of rule YYN. */ - private static final byte - yyr2_[] = - { - 0, 2, 5, 0, 1, 2, 7, 6, 0, 3, - 1, 1, 4, 4, 4, 6, 6, 0, 3, 1, - 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 2, 9, - 8, 11, 10, 5, 4, 7, 6, 0, 2, 1, - 4, 3, 0, 2, 1, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 1, 1, 2, 2, 1, 1, - 1, 2, 1, 3, 1, 1, 1, 1, 3, 3, - 3, 3, 3, 1, 1, 1, 1, 3, 3, 3, - 3, 3, 2, 2, 1, 1, 4, 1, 1, 3, - 1, 1, 3, 3, 3, 3, 3, 3, 3, 1, - 1, 1, 1, 1, 1, 2, 2, 6, 5, 1, - 2, 4, 3, 0, 2, 2, 0, 1, 2, 2, - 0, 1, 2, 4, 2, 1, 0, 1, 2, 4, - 1, 1, 1, 1, 1, 1, 1, 3, 3, 0, - 1, 2, 3, 0, 1, 2 - }; - - /* YYTNAME[SYMBOL-NUM] -- String name of the symbol SYMBOL-NUM. - First, the terminals, then, starting at \a yyntokens_, nonterminals. */ - private static final String yytname_[] = - { - "$end", "error", "$undefined", "VAR_BEGIN", "RETURN", "BREAK", "FUNC", - "WHILE", "FTRUE", "FFALSE", "IF", "THEN", "ELSE", "END", - "STRING_DEFINITION", "IDENTIFIER", "NUMBER", "SINGLE_LINE_COMMENT", - "DIVIDE_MOD", "ARG_SPLITTER", "PLUS", "MINUS", "DIVIDE", "MULTIPLY", - "ASSIGN", "MORE", "LESS", "MORE_OR_EQUAL", "LESS_OR_EQUAL", "EQUAL", - "NOT_EQUAL", "OPEN_BRACKET", "CLOSE_BRACKET", "AND", "OR", "FKFLOAT", - "PLUS_ASSIGN", "MINUS_ASSIGN", "DIVIDE_ASSIGN", "MULTIPLY_ASSIGN", - "DIVIDE_MOD_ASSIGN", "COLON", "FOR", "INC", "FAKE", "FKUUID", - "OPEN_SQUARE_BRACKET", "CLOSE_SQUARE_BRACKET", "FCONST", "PACKAGE", - "INCLUDE", "IDENTIFIER_DOT", "IDENTIFIER_POINTER", "STRUCT", "IS", "NOT", - "CONTINUE", "YIELD", "SLEEP", "SWITCH", "CASE", "DEFAULT", "NEW_ASSIGN", - "ELSEIF", "RIGHT_POINTER", "STRING_CAT", "OPEN_BIG_BRACKET", - "CLOSE_BIG_BRACKET", "NULL", "$accept", "program", "body", - "function_declaration", "function_declaration_arguments", "arg", - "function_call", "function_call_arguments", "arg_expr", "block", "stmt", - "fake_call_stmt", "for_stmt", "for_loop_stmt", "while_stmt", "if_stmt", - "elseif_stmt_list", "elseif_stmt", "else_stmt", "cmp", "cmp_value", - "return_stmt", "return_value_list", "return_value", "assign_stmt", - "multi_assign_stmt", "var_list", "assign_value", "math_assign_stmt", - "var", "variable", "expr", "math_expr", "expr_value", "break", - "continue", "sleep", "yield", "switch_stmt", "switch_case_list", - "switch_case_define", "package_head", "include_head", "include_define", - "struct_head", "struct_define", "struct_mem_declaration", "const_head", - "const_define", "explicit_value", "const_map_list_value", - "const_map_value", "const_array_list_value", null - }; - - /* YYRHS -- A `-1'-separated list of the rules' RHS. */ - private static final byte yyrhs_[] = - { - 70, 0, -1, 110, 111, 113, 116, 71, -1, -1, - 72, -1, 71, 72, -1, 6, 15, 31, 73, 32, - 78, 13, -1, 6, 15, 31, 73, 32, 13, -1, - -1, 73, 19, 74, -1, 74, -1, 15, -1, 15, - 31, 76, 32, -1, 51, 31, 76, 32, -1, 75, - 31, 76, 32, -1, 99, 41, 15, 31, 76, 32, - -1, 75, 41, 15, 31, 76, 32, -1, -1, 76, - 19, 77, -1, 77, -1, 102, -1, 78, 79, -1, - 79, -1, 83, -1, 84, -1, 90, -1, 93, -1, - 94, -1, 103, -1, 104, -1, 100, -1, 97, -1, - 81, -1, 82, -1, 80, -1, 105, -1, 106, -1, - 107, -1, 44, 75, -1, 42, 78, 19, 88, 19, - 78, 11, 78, 13, -1, 42, 78, 19, 88, 19, - 78, 11, 13, -1, 42, 98, 24, 96, 64, 89, - 19, 102, 11, 78, 13, -1, 42, 98, 24, 96, - 64, 89, 19, 102, 11, 13, -1, 7, 88, 11, - 78, 13, -1, 7, 88, 11, 13, -1, 10, 88, - 11, 78, 85, 87, 13, -1, 10, 88, 11, 85, - 87, 13, -1, -1, 85, 86, -1, 86, -1, 63, - 88, 11, 78, -1, 63, 88, 11, -1, -1, 12, - 78, -1, 12, -1, 31, 88, 32, -1, 88, 33, - 88, -1, 88, 34, 88, -1, 89, 26, 89, -1, - 89, 25, 89, -1, 89, 29, 89, -1, 89, 27, - 89, -1, 89, 28, 89, -1, 89, 30, 89, -1, - 8, -1, 9, -1, 54, 89, -1, 55, 89, -1, - 118, -1, 99, -1, 100, -1, 4, 91, -1, 4, - -1, 91, 19, 92, -1, 92, -1, 118, -1, 99, - -1, 100, -1, 98, 24, 96, -1, 98, 62, 96, - -1, 95, 24, 75, -1, 95, 62, 75, -1, 95, - 19, 98, -1, 98, -1, 118, -1, 99, -1, 100, - -1, 99, 36, 96, -1, 99, 37, 96, -1, 99, - 38, 96, -1, 99, 39, 96, -1, 99, 40, 96, - -1, 99, 43, -1, 3, 15, -1, 99, -1, 15, - -1, 15, 46, 102, 47, -1, 52, -1, 51, -1, - 31, 100, 32, -1, 75, -1, 101, -1, 31, 101, - 32, -1, 102, 20, 102, -1, 102, 21, 102, -1, - 102, 23, 102, -1, 102, 22, 102, -1, 102, 18, - 102, -1, 102, 65, 102, -1, 101, -1, 118, -1, - 75, -1, 99, -1, 5, -1, 56, -1, 58, 102, - -1, 57, 102, -1, 59, 89, 108, 61, 78, 13, - -1, 59, 89, 108, 61, 13, -1, 109, -1, 108, - 109, -1, 60, 89, 11, 78, -1, 60, 89, 11, - -1, -1, 49, 15, -1, 49, 51, -1, -1, 112, - -1, 111, 112, -1, 50, 14, -1, -1, 114, -1, - 113, 114, -1, 53, 15, 115, 13, -1, 115, 15, - -1, 15, -1, -1, 117, -1, 116, 117, -1, 48, - 15, 24, 118, -1, 68, -1, 8, -1, 9, -1, - 16, -1, 45, -1, 14, -1, 35, -1, 66, 119, - 67, -1, 46, 121, 47, -1, -1, 120, -1, 119, - 120, -1, 118, 41, 118, -1, -1, 118, -1, 121, - 118, -1 - }; - - /* YYPRHS[YYN] -- Index of the first RHS symbol of rule number YYN in - YYRHS. */ - private static final short yyprhs_[] = - { - 0, 0, 3, 9, 10, 12, 15, 23, 30, 31, - 35, 37, 39, 44, 49, 54, 61, 68, 69, 73, - 75, 77, 80, 82, 84, 86, 88, 90, 92, 94, - 96, 98, 100, 102, 104, 106, 108, 110, 112, 115, - 125, 134, 146, 157, 163, 168, 176, 183, 184, 187, - 189, 194, 198, 199, 202, 204, 208, 212, 216, 220, - 224, 228, 232, 236, 240, 242, 244, 247, 250, 252, - 254, 256, 259, 261, 265, 267, 269, 271, 273, 277, - 281, 285, 289, 293, 295, 297, 299, 301, 305, 309, - 313, 317, 321, 324, 327, 329, 331, 336, 338, 340, - 344, 346, 348, 352, 356, 360, 364, 368, 372, 376, - 378, 380, 382, 384, 386, 388, 391, 394, 401, 407, - 409, 412, 417, 421, 422, 425, 428, 429, 431, 434, - 437, 438, 440, 443, 448, 451, 453, 454, 456, 459, - 464, 466, 468, 470, 472, 474, 476, 478, 482, 486, - 487, 489, 492, 496, 497, 499 - }; - - /* YYRLINE[YYN] -- Source line where rule number YYN was defined. */ - private static final short yyrline_[] = - { - 0, 90, 90, 99, 102, 104, 108, 119, 132, 136, - 147, 160, 174, 188, 202, 216, 235, 257, 261, 272, - 285, 295, 306, 319, 325, 331, 337, 343, 349, 355, - 361, 367, 373, 379, 385, 391, 397, 403, 411, 424, - 438, 454, 469, 486, 498, 512, 526, 543, 551, 562, - 575, 587, 602, 610, 621, 634, 640, 653, 666, 679, - 692, 705, 718, 731, 744, 757, 770, 783, 798, 804, - 810, 818, 829, 842, 853, 866, 872, 878, 886, 899, - 914, 927, 942, 953, 966, 972, 978, 986, 999, 1012, - 1025, 1038, 1051, 1070, 1081, 1089, 1100, 1112, 1123, 1136, - 1142, 1148, 1156, 1162, 1175, 1188, 1201, 1214, 1227, 1242, - 1248, 1254, 1260, 1268, 1280, 1292, 1305, 1318, 1331, 1346, - 1357, 1370, 1382, 1397, 1400, 1406, 1415, 1418, 1420, 1424, - 1433, 1436, 1438, 1442, 1450, 1455, 1463, 1466, 1468, 1472, - 1480, 1491, 1502, 1513, 1525, 1537, 1549, 1561, 1573, 1589, - 1598, 1609, 1623, 1638, 1647, 1658 - }; - - // Report on the debug stream that the rule yyrule is going to be reduced. - private void yy_reduce_print (int yyrule, YYStack yystack) - { - if (yydebug == 0) - return; - - int yylno = yyrline_[yyrule]; - int yynrhs = yyr2_[yyrule]; - /* Print the symbols being reduced, and their result. */ - yycdebug ("Reducing stack by rule " + (yyrule - 1) - + " (line " + yylno + "), "); - - /* The symbols being reduced. */ - for (int yyi = 0; yyi < yynrhs; yyi++) - yy_symbol_print (" $" + (yyi + 1) + " =", - yyrhs_[yyprhs_[yyrule] + yyi], - ((yystack.valueAt (yynrhs-(yyi + 1))))); - } - - /* YYTRANSLATE(YYLEX) -- Bison symbol number corresponding to YYLEX. */ - private static final byte yytranslate_table_[] = - { - 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 2, 2, 1, 2, 3, 4, - 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, - 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, - 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, - 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, - 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, - 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, - 65, 66, 67, 68 - }; - - private static final byte yytranslate_ (int t) - { - if (t >= 0 && t <= yyuser_token_number_max_) - return yytranslate_table_[t]; - else - return yyundef_token_; - } - - private static final int yylast_ = 1300; - private static final int yynnts_ = 53; - private static final int yyempty_ = -2; - private static final int yyfinal_ = 6; - private static final int yyterror_ = 1; - private static final int yyerrcode_ = 256; - private static final int yyntokens_ = 69; - - private static final int yyuser_token_number_max_ = 323; - private static final int yyundef_token_ = 2; - -/* User implementation code. */ - -} - - -/* Line 879 of lalr1.java */ -/* Line 1670 of "YYParser.y" */ - - - diff --git a/src/main/java/com/github/esrrhs/fakescript/Yylex.java b/src/main/java/com/github/esrrhs/fakescript/Yylex.java deleted file mode 100644 index e5b293c..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/Yylex.java +++ /dev/null @@ -1,1171 +0,0 @@ -/* The following code was generated by JFlex 1.6.1 */ - -package com.github.esrrhs.fakescript; - -import com.github.esrrhs.fakescript.syntree.*; - - -/** - * This class is a scanner generated by - * JFlex 1.6.1 - * from the specification file jflex.flex - */ -class Yylex implements YYParser.Lexer { - - /** This character denotes the end of file */ - public static final int YYEOF = -1; - - /** initial size of the lookahead buffer */ - private static final int ZZ_BUFFERSIZE = 16384; - - /** lexical states */ - public static final int YYINITIAL = 0; - public static final int STRING = 2; - - /** - * ZZ_LEXSTATE[l] is the state in the DFA for the lexical state l - * ZZ_LEXSTATE[l+1] is the state in the DFA for the lexical state l - * at the beginning of a line - * l is of the form l = 2*k, k a non negative integer - */ - private static final int ZZ_LEXSTATE[] = { - 0, 0, 1, 1 - }; - - /** - * Translates characters to character classes - */ - private static final String ZZ_CMAP_PACKED = - "\11\0\1\0\1\2\1\0\1\0\1\1\22\0\1\0\1\50\1\5"+ - "\2\0\1\40\1\0\1\57\1\51\1\52\1\44\1\42\1\41\1\3"+ - "\1\35\1\43\12\34\1\45\1\0\1\47\1\46\1\36\2\0\4\33"+ - "\1\37\25\33\1\53\1\4\1\54\1\0\1\33\1\0\1\7\1\15"+ - "\1\20\1\27\1\11\1\17\1\31\1\22\1\23\1\33\1\16\1\24"+ - "\1\33\1\14\1\25\1\30\1\33\1\10\1\26\1\12\1\13\1\6"+ - "\1\21\1\33\1\32\1\33\1\55\1\0\1\56\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uffff\0\uff92\0"; - - /** - * Translates characters to character classes - */ - private static final char [] ZZ_CMAP = zzUnpackCMap(ZZ_CMAP_PACKED); - - /** - * Translates DFA states to action switch labels. - */ - private static final int [] ZZ_ACTION = zzUnpackAction(); - - private static final String ZZ_ACTION_PACKED_0 = - "\2\0\2\1\1\2\1\3\21\4\1\5\1\1\1\6"+ - "\1\7\1\10\1\11\1\12\1\13\1\14\1\15\1\16"+ - "\1\1\1\17\1\20\1\21\1\22\1\23\1\24\1\25"+ - "\1\1\1\26\1\27\1\5\1\30\1\31\1\0\1\4"+ - "\1\0\20\4\1\32\1\33\1\34\6\4\1\35\1\0"+ - "\1\36\1\37\1\40\1\41\1\42\1\43\1\44\1\45"+ - "\1\46\1\47\1\50\1\51\1\52\1\53\1\54\1\55"+ - "\1\56\1\57\1\60\2\27\1\0\1\61\1\62\1\63"+ - "\1\4\1\64\4\4\1\65\4\4\1\66\12\4\1\67"+ - "\1\70\1\4\1\71\1\72\1\73\1\74\1\4\1\75"+ - "\1\4\1\76\1\77\12\4\1\0\2\4\1\100\1\101"+ - "\1\4\1\102\1\103\3\4\1\104\2\4\1\105\1\0"+ - "\1\67\1\106\1\107\2\4\1\110\1\111\3\4\1\112"+ - "\1\113\1\114\1\115"; - - private static int [] zzUnpackAction() { - int [] result = new int[177]; - int offset = 0; - offset = zzUnpackAction(ZZ_ACTION_PACKED_0, offset, result); - return result; - } - - private static int zzUnpackAction(String packed, int offset, int [] result) { - int i = 0; /* index in packed string */ - int j = offset; /* index in unpacked array */ - int l = packed.length(); - while (i < l) { - int count = packed.charAt(i++); - int value = packed.charAt(i++); - do result[j++] = value; while (--count > 0); - } - return j; - } - - - /** - * Translates a state to a row index in the transition table - */ - private static final int [] ZZ_ROWMAP = zzUnpackRowMap(); - - private static final String ZZ_ROWMAP_PACKED_0 = - "\0\0\0\60\0\140\0\220\0\300\0\140\0\360\0\u0120"+ - "\0\u0150\0\u0180\0\u01b0\0\u01e0\0\u0210\0\u0240\0\u0270\0\u02a0"+ - "\0\u02d0\0\u0300\0\u0330\0\u0360\0\u0390\0\u03c0\0\u03f0\0\u0420"+ - "\0\u0450\0\u0480\0\u04b0\0\140\0\u04e0\0\u0510\0\u0540\0\u0570"+ - "\0\u05a0\0\u05d0\0\u0600\0\140\0\140\0\140\0\140\0\140"+ - "\0\140\0\u0630\0\u0660\0\140\0\u0690\0\u06c0\0\140\0\140"+ - "\0\u06f0\0\u0720\0\u0750\0\u0780\0\u07b0\0\u07e0\0\u0810\0\u0840"+ - "\0\u0870\0\u08a0\0\u08d0\0\u0900\0\u0930\0\u0960\0\u0990\0\u09c0"+ - "\0\u09f0\0\u0a20\0\u0a50\0\u01e0\0\u01e0\0\u01e0\0\u0a80\0\u0ab0"+ - "\0\u0ae0\0\u0b10\0\u0b40\0\u0b70\0\140\0\u0ba0\0\140\0\140"+ - "\0\140\0\140\0\140\0\140\0\140\0\140\0\140\0\140"+ - "\0\140\0\140\0\140\0\140\0\140\0\140\0\140\0\140"+ - "\0\140\0\u0bd0\0\140\0\u0c00\0\u01e0\0\u0c30\0\u01e0\0\u0c60"+ - "\0\u01e0\0\u0c90\0\u0cc0\0\u0cf0\0\u0d20\0\u01e0\0\u0d50\0\u0d80"+ - "\0\u0db0\0\u0de0\0\u01e0\0\u0e10\0\u0e40\0\u0e70\0\u0ea0\0\u0ed0"+ - "\0\u0f00\0\u0f30\0\u0f60\0\u0f90\0\u0fc0\0\u0ff0\0\u1020\0\u1050"+ - "\0\u1080\0\u01e0\0\u01e0\0\u01e0\0\u10b0\0\u01e0\0\u10e0\0\u01e0"+ - "\0\u01e0\0\u1110\0\u1140\0\u1170\0\u11a0\0\u11d0\0\u1200\0\u1230"+ - "\0\u1260\0\u1290\0\u12c0\0\u12f0\0\u1320\0\u1350\0\u01e0\0\u01e0"+ - "\0\u1380\0\u01e0\0\u01e0\0\u13b0\0\u13e0\0\u1410\0\u01e0\0\u1440"+ - "\0\u1470\0\u01e0\0\u14a0\0\u14a0\0\u01e0\0\u01e0\0\u14d0\0\u1500"+ - "\0\u01e0\0\u01e0\0\u1530\0\u1560\0\u1590\0\u01e0\0\u01e0\0\u01e0"+ - "\0\u01e0"; - - private static int [] zzUnpackRowMap() { - int [] result = new int[177]; - int offset = 0; - offset = zzUnpackRowMap(ZZ_ROWMAP_PACKED_0, offset, result); - return result; - } - - private static int zzUnpackRowMap(String packed, int offset, int [] result) { - int i = 0; /* index in packed string */ - int j = offset; /* index in unpacked array */ - int l = packed.length(); - while (i < l) { - int high = packed.charAt(i++) << 16; - result[j++] = high | packed.charAt(i++); - } - return j; - } - - /** - * The transition table of the DFA - */ - private static final int [] ZZ_TRANS = zzUnpackTrans(); - - private static final String ZZ_TRANS_PACKED_0 = - "\1\3\1\4\1\3\1\5\1\3\1\6\1\7\1\10"+ - "\1\11\1\12\1\13\1\14\1\15\1\16\1\14\1\17"+ - "\1\20\1\21\1\14\1\22\1\14\1\23\1\24\1\25"+ - "\1\26\1\14\1\27\1\14\1\30\1\31\1\32\1\14"+ - "\1\33\1\34\1\35\1\36\1\37\1\40\1\41\1\42"+ - "\1\43\1\44\1\45\1\46\1\47\1\50\1\51\1\3"+ - "\1\52\2\3\1\52\1\53\1\54\52\52\62\0\1\3"+ - "\60\0\1\55\30\0\1\56\1\0\1\57\7\0\1\60"+ - "\14\0\1\61\2\0\1\14\1\62\25\14\1\63\1\0"+ - "\1\14\23\0\1\61\2\0\6\14\1\64\20\14\1\63"+ - "\1\0\1\14\23\0\1\61\2\0\3\14\1\65\23\14"+ - "\1\63\1\0\1\14\23\0\1\61\2\0\6\14\1\66"+ - "\7\14\1\67\10\14\1\63\1\0\1\14\23\0\1\61"+ - "\2\0\2\14\1\70\11\14\1\71\12\14\1\63\1\0"+ - "\1\14\23\0\1\61\2\0\27\14\1\63\1\0\1\14"+ - "\23\0\1\61\2\0\5\14\1\72\11\14\1\73\7\14"+ - "\1\63\1\0\1\14\23\0\1\61\2\0\2\14\1\74"+ - "\24\14\1\63\1\0\1\14\23\0\1\61\2\0\1\14"+ - "\1\75\3\14\1\76\11\14\1\77\7\14\1\63\1\0"+ - "\1\14\23\0\1\61\2\0\1\14\1\100\15\14\1\101"+ - "\7\14\1\63\1\0\1\14\23\0\1\61\2\0\14\14"+ - "\1\102\12\14\1\63\1\0\1\14\23\0\1\61\2\0"+ - "\6\14\1\103\2\14\1\104\6\14\1\105\6\14\1\63"+ - "\1\0\1\14\23\0\1\61\2\0\2\14\1\106\24\14"+ - "\1\63\1\0\1\14\23\0\1\61\2\0\4\14\1\107"+ - "\6\14\1\110\2\14\1\111\10\14\1\63\1\0\1\14"+ - "\23\0\1\61\2\0\3\14\1\112\23\14\1\63\1\0"+ - "\1\14\23\0\1\61\2\0\1\14\1\113\25\14\1\63"+ - "\1\0\1\14\23\0\1\61\2\0\15\14\1\114\11\14"+ - "\1\63\1\0\1\14\33\0\1\115\20\0\1\30\1\116"+ - "\57\0\1\117\70\0\1\120\57\0\1\121\53\0\1\122"+ - "\3\0\1\123\57\0\1\124\57\0\1\125\57\0\1\126"+ - "\57\0\1\127\57\0\1\130\57\0\1\131\11\0\1\52"+ - "\2\0\1\52\2\0\52\52\4\0\1\132\1\133\2\0"+ - "\1\134\1\0\1\135\1\0\1\136\1\137\1\0\1\140"+ - "\37\0\1\141\1\55\1\142\1\143\55\55\34\0\1\56"+ - "\1\116\60\0\1\144\24\0\1\61\2\0\2\14\1\145"+ - "\24\14\1\63\1\0\1\14\26\0\26\146\3\0\1\146"+ - "\23\0\1\61\2\0\21\14\1\147\5\14\1\63\1\0"+ - "\1\14\23\0\1\61\2\0\4\14\1\150\22\14\1\63"+ - "\1\0\1\14\23\0\1\61\2\0\21\14\1\151\5\14"+ - "\1\63\1\0\1\14\23\0\1\61\2\0\20\14\1\152"+ - "\6\14\1\63\1\0\1\14\23\0\1\61\2\0\5\14"+ - "\1\153\21\14\1\63\1\0\1\14\23\0\1\61\2\0"+ - "\3\14\1\154\23\14\1\63\1\0\1\14\23\0\1\61"+ - "\2\0\16\14\1\155\10\14\1\63\1\0\1\14\23\0"+ - "\1\61\2\0\4\14\1\156\22\14\1\63\1\0\1\14"+ - "\23\0\1\61\2\0\3\14\1\157\23\14\1\63\1\0"+ - "\1\14\23\0\1\61\2\0\10\14\1\160\5\14\1\161"+ - "\10\14\1\63\1\0\1\14\23\0\1\61\2\0\6\14"+ - "\1\162\20\14\1\63\1\0\1\14\23\0\1\61\2\0"+ - "\2\14\1\163\24\14\1\63\1\0\1\14\23\0\1\61"+ - "\2\0\20\14\1\164\6\14\1\63\1\0\1\14\23\0"+ - "\1\61\2\0\6\14\1\165\20\14\1\63\1\0\1\14"+ - "\23\0\1\61\2\0\15\14\1\166\11\14\1\63\1\0"+ - "\1\14\23\0\1\61\2\0\12\14\1\167\14\14\1\63"+ - "\1\0\1\14\23\0\1\61\2\0\2\14\1\170\24\14"+ - "\1\63\1\0\1\14\23\0\1\61\2\0\15\14\1\171"+ - "\11\14\1\63\1\0\1\14\23\0\1\61\2\0\3\14"+ - "\1\172\23\14\1\63\1\0\1\14\23\0\1\61\2\0"+ - "\11\14\1\173\15\14\1\63\1\0\1\14\23\0\1\61"+ - "\2\0\12\14\1\174\14\14\1\63\1\0\1\14\23\0"+ - "\1\61\2\0\3\14\1\175\23\14\1\63\1\0\1\14"+ - "\54\0\1\176\25\0\1\143\63\0\26\177\3\0\1\177"+ - "\26\0\27\146\1\63\1\0\1\146\23\0\1\61\2\0"+ - "\5\14\1\200\21\14\1\63\1\0\1\14\23\0\1\61"+ - "\2\0\3\14\1\201\23\14\1\63\1\0\1\14\23\0"+ - "\1\61\2\0\3\14\1\202\23\14\1\63\1\0\1\14"+ - "\23\0\1\61\2\0\6\14\1\203\20\14\1\63\1\0"+ - "\1\14\23\0\1\61\2\0\16\14\1\204\10\14\1\63"+ - "\1\0\1\14\23\0\1\61\2\0\1\14\1\205\25\14"+ - "\1\63\1\0\1\14\23\0\1\61\2\0\3\14\1\206"+ - "\23\14\1\63\1\0\1\14\23\0\1\61\2\0\20\14"+ - "\1\207\6\14\1\63\1\0\1\14\23\0\1\61\2\0"+ - "\12\14\1\210\14\14\1\63\1\0\1\14\23\0\1\61"+ - "\2\0\3\14\1\211\23\14\1\63\1\0\1\14\23\0"+ - "\1\61\2\0\4\14\1\212\13\14\1\213\6\14\1\63"+ - "\1\0\1\14\23\0\1\61\2\0\16\14\1\214\10\14"+ - "\1\63\1\0\1\14\23\0\1\61\2\0\16\14\1\215"+ - "\10\14\1\63\1\0\1\14\23\0\1\61\2\0\5\14"+ - "\1\216\21\14\1\63\1\0\1\14\23\0\1\61\2\0"+ - "\4\14\1\217\22\14\1\63\1\0\1\14\23\0\1\61"+ - "\2\0\3\14\1\220\23\14\1\63\1\0\1\14\23\0"+ - "\1\61\2\0\1\14\1\221\25\14\1\63\1\0\1\14"+ - "\23\0\1\61\2\0\10\14\1\222\16\14\1\63\1\0"+ - "\1\14\23\0\1\61\2\0\16\14\1\223\10\14\1\63"+ - "\1\0\1\14\31\0\1\224\22\0\1\176\2\0\1\224"+ - "\23\0\1\61\2\0\27\177\2\0\1\177\23\0\1\61"+ - "\2\0\2\14\1\225\24\14\1\63\1\0\1\14\23\0"+ - "\1\61\2\0\15\14\1\226\11\14\1\63\1\0\1\14"+ - "\23\0\1\61\2\0\10\14\1\227\16\14\1\63\1\0"+ - "\1\14\23\0\1\61\2\0\3\14\1\230\23\14\1\63"+ - "\1\0\1\14\23\0\1\61\2\0\15\14\1\231\11\14"+ - "\1\63\1\0\1\14\23\0\1\61\2\0\4\14\1\232"+ - "\22\14\1\63\1\0\1\14\23\0\1\61\2\0\3\14"+ - "\1\233\23\14\1\63\1\0\1\14\23\0\1\61\2\0"+ - "\5\14\1\234\21\14\1\63\1\0\1\14\23\0\1\61"+ - "\2\0\12\14\1\235\14\14\1\63\1\0\1\14\23\0"+ - "\1\61\2\0\12\14\1\236\14\14\1\63\1\0\1\14"+ - "\23\0\1\61\2\0\22\14\1\237\4\14\1\63\1\0"+ - "\1\14\23\0\1\61\2\0\5\14\1\240\21\14\1\63"+ - "\1\0\1\14\23\0\1\61\2\0\1\14\1\241\25\14"+ - "\1\63\1\0\1\14\23\0\1\61\2\0\21\14\1\242"+ - "\5\14\1\63\1\0\1\14\23\0\1\243\30\0\1\244"+ - "\26\0\1\61\2\0\6\14\1\245\20\14\1\63\1\0"+ - "\1\14\23\0\1\61\2\0\11\14\1\246\15\14\1\63"+ - "\1\0\1\14\23\0\1\61\2\0\6\14\1\247\20\14"+ - "\1\63\1\0\1\14\23\0\1\61\2\0\21\14\1\250"+ - "\5\14\1\63\1\0\1\14\23\0\1\61\2\0\4\14"+ - "\1\251\22\14\1\63\1\0\1\14\23\0\1\61\2\0"+ - "\14\14\1\252\12\14\1\63\1\0\1\14\23\0\1\61"+ - "\2\0\16\14\1\253\10\14\1\63\1\0\1\14\23\0"+ - "\1\61\2\0\23\14\1\254\3\14\1\63\1\0\1\14"+ - "\54\0\1\244\26\0\1\61\2\0\5\14\1\255\21\14"+ - "\1\63\1\0\1\14\23\0\1\61\2\0\3\14\1\256"+ - "\23\14\1\63\1\0\1\14\23\0\1\61\2\0\4\14"+ - "\1\257\22\14\1\63\1\0\1\14\23\0\1\61\2\0"+ - "\3\14\1\260\23\14\1\63\1\0\1\14\23\0\1\61"+ - "\2\0\3\14\1\261\23\14\1\63\1\0\1\14\20\0"; - - private static int [] zzUnpackTrans() { - int [] result = new int[5568]; - int offset = 0; - offset = zzUnpackTrans(ZZ_TRANS_PACKED_0, offset, result); - return result; - } - - private static int zzUnpackTrans(String packed, int offset, int [] result) { - int i = 0; /* index in packed string */ - int j = offset; /* index in unpacked array */ - int l = packed.length(); - while (i < l) { - int count = packed.charAt(i++); - int value = packed.charAt(i++); - value--; - do result[j++] = value; while (--count > 0); - } - return j; - } - - - /* error codes */ - private static final int ZZ_UNKNOWN_ERROR = 0; - private static final int ZZ_NO_MATCH = 1; - private static final int ZZ_PUSHBACK_2BIG = 2; - - /* error messages for the codes above */ - private static final String ZZ_ERROR_MSG[] = { - "Unknown internal scanner error", - "Error: could not match input", - "Error: pushback value was too large" - }; - - /** - * ZZ_ATTRIBUTE[aState] contains the attributes of state aState - */ - private static final int [] ZZ_ATTRIBUTE = zzUnpackAttribute(); - - private static final String ZZ_ATTRIBUTE_PACKED_0 = - "\2\0\1\11\2\1\1\11\25\1\1\11\7\1\6\11"+ - "\2\1\1\11\2\1\2\11\1\0\1\1\1\0\31\1"+ - "\1\11\1\0\23\11\1\1\1\11\1\0\57\1\1\0"+ - "\16\1\1\0\16\1"; - - private static int [] zzUnpackAttribute() { - int [] result = new int[177]; - int offset = 0; - offset = zzUnpackAttribute(ZZ_ATTRIBUTE_PACKED_0, offset, result); - return result; - } - - private static int zzUnpackAttribute(String packed, int offset, int [] result) { - int i = 0; /* index in packed string */ - int j = offset; /* index in unpacked array */ - int l = packed.length(); - while (i < l) { - int count = packed.charAt(i++); - int value = packed.charAt(i++); - do result[j++] = value; while (--count > 0); - } - return j; - } - - /** the input device */ - private java.io.Reader zzReader; - - /** the current state of the DFA */ - private int zzState; - - /** the current lexical state */ - private int zzLexicalState = YYINITIAL; - - /** this buffer contains the current text to be matched and is - the source of the yytext() string */ - private char zzBuffer[] = new char[ZZ_BUFFERSIZE]; - - /** the textposition at the last accepting state */ - private int zzMarkedPos; - - /** the current text position in the buffer */ - private int zzCurrentPos; - - /** startRead marks the beginning of the yytext() string in the buffer */ - private int zzStartRead; - - /** endRead marks the last character in the buffer, that has been read - from input */ - private int zzEndRead; - - /** number of newlines encountered up to the start of the matched text */ - private int yyline; - - /** the number of characters up to the start of the matched text */ - private int yychar; - - /** - * the number of characters from the last newline up to the start of the - * matched text - */ - private int yycolumn; - - /** - * zzAtBOL == true <=> the scanner is currently at the beginning of a line - */ - private boolean zzAtBOL = true; - - /** zzAtEOF == true <=> the scanner is at the EOF */ - private boolean zzAtEOF; - - /** denotes if the user-EOF-code has already been executed */ - private boolean zzEOFDone; - - /** - * The number of occupied positions in zzBuffer beyond zzEndRead. - * When a lead/high surrogate has been read from the input stream - * into the final zzBuffer position, this will have a value of 1; - * otherwise, it will have a value of 0. - */ - private int zzFinalHighSurrogate = 0; - - /* user code: */ - fake m_f; - StringBuilder string = new StringBuilder(); - /* store a reference to the YYParser object */ - private YYParser yyparser; - - private ParserVal yylval; - - private mybison m_mybison; - - public fake get_fake() - { - return m_f; - } - - public void set_fake(fake f) - { - m_f = f; - } - - public void set_mybison(mybison mb) - { - m_mybison = mb; - } - - public mybison get_mybison() - { - return m_mybison; - } - - public T new_node(Class c, int lineno) - { - try - { - syntree_node t = null; - t = c.newInstance(); - t.m_lno = lineno; - return (T) t; - } - catch (Exception e) - { - return null; - } - } - - public int get_line() - { - return yyline; - } - - public Object getLVal() - { - return yylval; - } - - public void yyerror(String s) - { - m_mybison.lexer_error(s, yyline, yytext()); - } - - - /** - * Creates a new scanner - * - * @param in the java.io.Reader to read input from. - */ - Yylex(java.io.Reader in) { - this.zzReader = in; - } - - - /** - * Unpacks the compressed character translation table. - * - * @param packed the packed character translation table - * @return the unpacked character translation table - */ - private static char [] zzUnpackCMap(String packed) { - char [] map = new char[0x110000]; - int i = 0; /* index in packed string */ - int j = 0; /* index in unpacked array */ - while (i < 168) { - int count = packed.charAt(i++); - char value = packed.charAt(i++); - do map[j++] = value; while (--count > 0); - } - return map; - } - - - /** - * Refills the input buffer. - * - * @return false, iff there was new input. - * - * @exception java.io.IOException if any I/O-Error occurs - */ - private boolean zzRefill() throws java.io.IOException { - - /* first: make room (if you can) */ - if (zzStartRead > 0) { - zzEndRead += zzFinalHighSurrogate; - zzFinalHighSurrogate = 0; - System.arraycopy(zzBuffer, zzStartRead, - zzBuffer, 0, - zzEndRead-zzStartRead); - - /* translate stored positions */ - zzEndRead-= zzStartRead; - zzCurrentPos-= zzStartRead; - zzMarkedPos-= zzStartRead; - zzStartRead = 0; - } - - /* is the buffer big enough? */ - if (zzCurrentPos >= zzBuffer.length - zzFinalHighSurrogate) { - /* if not: blow it up */ - char newBuffer[] = new char[zzBuffer.length*2]; - System.arraycopy(zzBuffer, 0, newBuffer, 0, zzBuffer.length); - zzBuffer = newBuffer; - zzEndRead += zzFinalHighSurrogate; - zzFinalHighSurrogate = 0; - } - - /* fill the buffer with new input */ - int requested = zzBuffer.length - zzEndRead; - int numRead = zzReader.read(zzBuffer, zzEndRead, requested); - - /* not supposed to occur according to specification of java.io.Reader */ - if (numRead == 0) { - throw new java.io.IOException("Reader returned 0 characters. See JFlex examples for workaround."); - } - if (numRead > 0) { - zzEndRead += numRead; - /* If numRead == requested, we might have requested to few chars to - encode a full Unicode character. We assume that a Reader would - otherwise never return half characters. */ - if (numRead == requested) { - if (Character.isHighSurrogate(zzBuffer[zzEndRead - 1])) { - --zzEndRead; - zzFinalHighSurrogate = 1; - } - } - /* potentially more input available */ - return false; - } - - /* numRead < 0 ==> end of stream */ - return true; - } - - - /** - * Closes the input stream. - */ - public final void yyclose() throws java.io.IOException { - zzAtEOF = true; /* indicate end of file */ - zzEndRead = zzStartRead; /* invalidate buffer */ - - if (zzReader != null) - zzReader.close(); - } - - - /** - * Resets the scanner to read from a new input stream. - * Does not close the old reader. - * - * All internal variables are reset, the old input stream - * cannot be reused (internal buffer is discarded and lost). - * Lexical state is set to ZZ_INITIAL. - * - * Internal scan buffer is resized down to its initial length, if it has grown. - * - * @param reader the new input stream - */ - public final void yyreset(java.io.Reader reader) { - zzReader = reader; - zzAtBOL = true; - zzAtEOF = false; - zzEOFDone = false; - zzEndRead = zzStartRead = 0; - zzCurrentPos = zzMarkedPos = 0; - zzFinalHighSurrogate = 0; - yyline = yychar = yycolumn = 0; - zzLexicalState = YYINITIAL; - if (zzBuffer.length > ZZ_BUFFERSIZE) - zzBuffer = new char[ZZ_BUFFERSIZE]; - } - - - /** - * Returns the current lexical state. - */ - public final int yystate() { - return zzLexicalState; - } - - - /** - * Enters a new lexical state - * - * @param newState the new lexical state - */ - public final void yybegin(int newState) { - zzLexicalState = newState; - } - - - /** - * Returns the text matched by the current regular expression. - */ - public final String yytext() { - return new String( zzBuffer, zzStartRead, zzMarkedPos-zzStartRead ); - } - - - /** - * Returns the character at position pos from the - * matched text. - * - * It is equivalent to yytext().charAt(pos), but faster - * - * @param pos the position of the character to fetch. - * A value from 0 to yylength()-1. - * - * @return the character at position pos - */ - public final char yycharat(int pos) { - return zzBuffer[zzStartRead+pos]; - } - - - /** - * Returns the length of the matched text region. - */ - public final int yylength() { - return zzMarkedPos-zzStartRead; - } - - - /** - * Reports an error that occured while scanning. - * - * In a wellformed scanner (no or only correct usage of - * yypushback(int) and a match-all fallback rule) this method - * will only be called with things that "Can't Possibly Happen". - * If this method is called, something is seriously wrong - * (e.g. a JFlex bug producing a faulty scanner etc.). - * - * Usual syntax/scanner level error handling should be done - * in error fallback rules. - * - * @param errorCode the code of the errormessage to display - */ - private void zzScanError(int errorCode) { - String message; - try { - message = ZZ_ERROR_MSG[errorCode]; - } - catch (ArrayIndexOutOfBoundsException e) { - message = ZZ_ERROR_MSG[ZZ_UNKNOWN_ERROR]; - } - - throw new Error(message); - } - - - /** - * Pushes the specified amount of characters back into the input stream. - * - * They will be read again by then next call of the scanning method - * - * @param number the number of characters to be read again. - * This number must not be greater than yylength()! - */ - public void yypushback(int number) { - if ( number > yylength() ) - zzScanError(ZZ_PUSHBACK_2BIG); - - zzMarkedPos -= number; - } - - - /** - * Contains user EOF-code, which will be executed exactly once, - * when the end of file is reached - */ - private void zzDoEOF() throws java.io.IOException { - if (!zzEOFDone) { - zzEOFDone = true; - yyclose(); - } - } - - - /** - * Resumes scanning until the next regular expression is matched, - * the end of input is encountered or an I/O-Error occurs. - * - * @return the next token - * @exception java.io.IOException if any I/O-Error occurs - */ - public int yylex() throws java.io.IOException { - int zzInput; - int zzAction; - - // cached fields: - int zzCurrentPosL; - int zzMarkedPosL; - int zzEndReadL = zzEndRead; - char [] zzBufferL = zzBuffer; - char [] zzCMapL = ZZ_CMAP; - - int [] zzTransL = ZZ_TRANS; - int [] zzRowMapL = ZZ_ROWMAP; - int [] zzAttrL = ZZ_ATTRIBUTE; - - while (true) { - zzMarkedPosL = zzMarkedPos; - - boolean zzR = false; - int zzCh; - int zzCharCount; - for (zzCurrentPosL = zzStartRead ; - zzCurrentPosL < zzMarkedPosL ; - zzCurrentPosL += zzCharCount ) { - zzCh = Character.codePointAt(zzBufferL, zzCurrentPosL, zzMarkedPosL); - zzCharCount = Character.charCount(zzCh); - switch (zzCh) { - case '\u000B': - case '\u000C': - case '\u0085': - case '\u2028': - case '\u2029': - yyline++; - yycolumn = 0; - zzR = false; - break; - case '\r': - yyline++; - yycolumn = 0; - zzR = true; - break; - case '\n': - if (zzR) - zzR = false; - else { - yyline++; - yycolumn = 0; - } - break; - default: - zzR = false; - yycolumn += zzCharCount; - } - } - - if (zzR) { - // peek one character ahead if it is \n (if we have counted one line too much) - boolean zzPeek; - if (zzMarkedPosL < zzEndReadL) - zzPeek = zzBufferL[zzMarkedPosL] == '\n'; - else if (zzAtEOF) - zzPeek = false; - else { - boolean eof = zzRefill(); - zzEndReadL = zzEndRead; - zzMarkedPosL = zzMarkedPos; - zzBufferL = zzBuffer; - if (eof) - zzPeek = false; - else - zzPeek = zzBufferL[zzMarkedPosL] == '\n'; - } - if (zzPeek) yyline--; - } - zzAction = -1; - - zzCurrentPosL = zzCurrentPos = zzStartRead = zzMarkedPosL; - - zzState = ZZ_LEXSTATE[zzLexicalState]; - - // set up zzAction for empty match case: - int zzAttributes = zzAttrL[zzState]; - if ( (zzAttributes & 1) == 1 ) { - zzAction = zzState; - } - - - zzForAction: { - while (true) { - - if (zzCurrentPosL < zzEndReadL) { - zzInput = Character.codePointAt(zzBufferL, zzCurrentPosL, zzEndReadL); - zzCurrentPosL += Character.charCount(zzInput); - } - else if (zzAtEOF) { - zzInput = YYEOF; - break zzForAction; - } - else { - // store back cached positions - zzCurrentPos = zzCurrentPosL; - zzMarkedPos = zzMarkedPosL; - boolean eof = zzRefill(); - // get translated positions and possibly new buffer - zzCurrentPosL = zzCurrentPos; - zzMarkedPosL = zzMarkedPos; - zzBufferL = zzBuffer; - zzEndReadL = zzEndRead; - if (eof) { - zzInput = YYEOF; - break zzForAction; - } - else { - zzInput = Character.codePointAt(zzBufferL, zzCurrentPosL, zzEndReadL); - zzCurrentPosL += Character.charCount(zzInput); - } - } - int zzNext = zzTransL[ zzRowMapL[zzState] + zzCMapL[zzInput] ]; - if (zzNext == -1) break zzForAction; - zzState = zzNext; - - zzAttributes = zzAttrL[zzState]; - if ( (zzAttributes & 1) == 1 ) { - zzAction = zzState; - zzMarkedPosL = zzCurrentPosL; - if ( (zzAttributes & 8) == 8 ) break zzForAction; - } - - } - } - - // store back cached position - zzMarkedPos = zzMarkedPosL; - - if (zzInput == YYEOF && zzStartRead == zzCurrentPos) { - zzAtEOF = true; - zzDoEOF(); - switch (zzLexicalState) { - case YYINITIAL: { - return YYParser.EOF; - } - case 178: break; - default: - { - return YYParser.EOF; - } - } - } - else { - switch (zzAction < 0 ? zzAction : ZZ_ACTION[zzAction]) { - case 1: - { - } - case 78: break; - case 2: - { return YYParser.MINUS; - } - case 79: break; - case 3: - { yybegin(STRING); string.setLength(0); - } - case 80: break; - case 4: - { yylval = new ParserVal(yytext()); - yylval.ival = yyline + 1; - return YYParser.IDENTIFIER; - } - case 81: break; - case 5: - { yylval = new ParserVal(yytext()); - yylval.ival = yyline + 1; - return YYParser.NUMBER; - } - case 82: break; - case 6: - { return YYParser.MORE; - } - case 83: break; - case 7: - { return YYParser.DIVIDE_MOD; - } - case 84: break; - case 8: - { return YYParser.ARG_SPLITTER; - } - case 85: break; - case 9: - { return YYParser.PLUS; - } - case 86: break; - case 10: - { return YYParser.DIVIDE; - } - case 87: break; - case 11: - { return YYParser.MULTIPLY; - } - case 88: break; - case 12: - { return YYParser.COLON; - } - case 89: break; - case 13: - { return YYParser.ASSIGN; - } - case 90: break; - case 14: - { return YYParser.LESS; - } - case 91: break; - case 15: - { return YYParser.OPEN_BRACKET; - } - case 92: break; - case 16: - { return YYParser.CLOSE_BRACKET; - } - case 93: break; - case 17: - { return YYParser.OPEN_SQUARE_BRACKET; - } - case 94: break; - case 18: - { return YYParser.CLOSE_SQUARE_BRACKET; - } - case 95: break; - case 19: - { return YYParser.OPEN_BIG_BRACKET; - } - case 96: break; - case 20: - { return YYParser.CLOSE_BIG_BRACKET; - } - case 97: break; - case 21: - { string.append( yytext() ); - } - case 98: break; - case 22: - { yybegin(YYINITIAL); - yylval = new ParserVal(string.toString()); - yylval.ival = yyline + 1; - return YYParser.STRING_DEFINITION; - } - case 99: break; - case 23: - { /* ignore */ - } - case 100: break; - case 24: - { return YYParser.RIGHT_POINTER; - } - case 101: break; - case 25: - { return YYParser.MINUS_ASSIGN; - } - case 102: break; - case 26: - { return YYParser.IF; - } - case 103: break; - case 27: - { return YYParser.IS; - } - case 104: break; - case 28: - { return YYParser.OR; - } - case 105: break; - case 29: - { yylval = new ParserVal(yytext()); - yylval.ival = yyline + 1; - return YYParser.FKUUID; - } - case 106: break; - case 30: - { return YYParser.STRING_CAT; - } - case 107: break; - case 31: - { return YYParser.MORE_OR_EQUAL; - } - case 108: break; - case 32: - { return YYParser.DIVIDE_MOD_ASSIGN; - } - case 109: break; - case 33: - { return YYParser.INC; - } - case 110: break; - case 34: - { return YYParser.PLUS_ASSIGN; - } - case 111: break; - case 35: - { return YYParser.DIVIDE_ASSIGN; - } - case 112: break; - case 36: - { return YYParser.MULTIPLY_ASSIGN; - } - case 113: break; - case 37: - { return YYParser.NEW_ASSIGN; - } - case 114: break; - case 38: - { return YYParser.EQUAL; - } - case 115: break; - case 39: - { return YYParser.LESS_OR_EQUAL; - } - case 116: break; - case 40: - { return YYParser.NOT_EQUAL; - } - case 117: break; - case 41: - { string.append( '\\' ); - } - case 118: break; - case 42: - { string.append( '\"' ); - } - case 119: break; - case 43: - { string.append( '\r' ); - } - case 120: break; - case 44: - { string.append( '\t' ); - } - case 121: break; - case 45: - { string.append( '\n' ); - } - case 122: break; - case 46: - { string.append( '\b' ); - } - case 123: break; - case 47: - { string.append( '\f' ); - } - case 124: break; - case 48: - { string.append( '\'' ); - } - case 125: break; - case 49: - { return YYParser.VAR_BEGIN; - } - case 126: break; - case 50: - { yylval = new ParserVal(yytext()); - yylval.ival = yyline + 1; - return YYParser.IDENTIFIER_DOT; - } - case 127: break; - case 51: - { return YYParser.AND; - } - case 128: break; - case 52: - { return YYParser.END; - } - case 129: break; - case 53: - { return YYParser.NOT; - } - case 130: break; - case 54: - { return YYParser.FOR; - } - case 131: break; - case 55: - { yylval = new ParserVal(yytext()); - yylval.ival = yyline + 1; - return YYParser.FKFLOAT; - } - case 132: break; - case 56: - { yylval = new ParserVal(yytext()); - yylval.ival = yyline + 1; - return YYParser.IDENTIFIER_POINTER; - } - case 133: break; - case 57: - { return YYParser.ELSE; - } - case 134: break; - case 58: - { return YYParser.FTRUE; - } - case 135: break; - case 59: - { return YYParser.THEN; - } - case 136: break; - case 60: - { return YYParser.NULL; - } - case 137: break; - case 61: - { return YYParser.FAKE; - } - case 138: break; - case 62: - { return YYParser.FUNC; - } - case 139: break; - case 63: - { return YYParser.CASE; - } - case 140: break; - case 64: - { return YYParser.BREAK; - } - case 141: break; - case 65: - { return YYParser.FFALSE; - } - case 142: break; - case 66: - { return YYParser.FCONST; - } - case 143: break; - case 67: - { return YYParser.WHILE; - } - case 144: break; - case 68: - { return YYParser.SLEEP; - } - case 145: break; - case 69: - { return YYParser.YIELD; - } - case 146: break; - case 70: - { return YYParser.RETURN; - } - case 147: break; - case 71: - { return YYParser.ELSEIF; - } - case 148: break; - case 72: - { return YYParser.STRUCT; - } - case 149: break; - case 73: - { return YYParser.SWITCH; - } - case 150: break; - case 74: - { return YYParser.INCLUDE; - } - case 151: break; - case 75: - { return YYParser.DEFAULT; - } - case 152: break; - case 76: - { return YYParser.PACKAGE; - } - case 153: break; - case 77: - { return YYParser.CONTINUE; - } - case 154: break; - default: - zzScanError(ZZ_NO_MATCH); - } - } - } - } - - -} diff --git a/src/main/java/com/github/esrrhs/fakescript/bifunc.java b/src/main/java/com/github/esrrhs/fakescript/bifunc.java deleted file mode 100644 index aa5316d..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/bifunc.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.github.esrrhs.fakescript; - -import java.lang.reflect.Method; - -class bifunc -{ - Method m_m; - - public void call(fake f, interpreter inter) throws Exception - { - m_m.invoke(null, f, inter); - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/binary.java b/src/main/java/com/github/esrrhs/fakescript/binary.java deleted file mode 100644 index ef02689..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/binary.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.github.esrrhs.fakescript; - -import java.util.Map; - -class binary -{ - private fake m_f; - - public binary(fake f) - { - m_f = f; - } - - public String dump() - { - String ret = ""; - for (Map.Entry e : m_f.fm.get_funcmap().entrySet()) - { - funcunion f = e.getValue(); - if (f.m_havefb) - { - ret += f.m_fb.dump(-1); - } - } - return ret; - } - - public String dump(String str, int pos) - { - variant funcv = new variant(); - funcv.set_string(str); - - funcunion f = m_f.fm.get_func(funcv); - if (f != null && f.m_havefb) - { - return f.m_fb.dump(pos); - } - else - { - return "not find " + str; - } - } - - public void add_func(variant name, func_binary bin) - { - funcunion f = m_f.fm.get_func(name); - if (f != null && f.m_havefb && f.m_fb.m_use != 0) - { - types.log(m_f, "[binary] add_func func %s add back bin", name); - f.m_fb = bin; - } - else - { - types.log(m_f, "[binary] add_func func %s add bin", name); - m_f.fm.add_func(name, bin); - } - - types.log(m_f, "add func %s", name); - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/buildinfunc.java b/src/main/java/com/github/esrrhs/fakescript/buildinfunc.java deleted file mode 100644 index 6186d5a..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/buildinfunc.java +++ /dev/null @@ -1,420 +0,0 @@ -package com.github.esrrhs.fakescript; - -import java.util.Map; -import java.util.Set; - -class buildinfunc -{ - private fake m_f; - - public buildinfunc(fake f) - { - m_f = f; - } - - public void openbasefunc() - { - reg_func("print", "buildin_print"); - reg_func("format", "buildin_format"); - reg_func("array", "buildin_array"); - reg_func(interpreter.MAP_FUNC_NAME, "buildin_map"); - reg_func(interpreter.GMAP_FUNC_NAME, "buildin_gmap"); - reg_func("size", "buildin_size"); - reg_func("range", "buildin_range"); - reg_func("typeof", "buildin_typeof"); - reg_func("dumpallfunc", "buildin_dumpallfunc"); - reg_func("dumpfunc", "buildin_dumpfunc"); - reg_func("dofile", "buildin_dofile"); - reg_func("dostring", "buildin_dostring"); - reg_func("getcurfile", "buildin_getcurfile"); - reg_func("getcurline", "buildin_getcurline"); - reg_func("getcurfunc", "buildin_getcurfunc"); - reg_func("getcurcallstack", "buildin_getcurcallstack"); - reg_func("isfunc", "buildin_isfunc"); - reg_func("tonumber", "buildin_tonumber"); - reg_func("tostring", "buildin_tostring"); - reg_func("tolong", "buildin_tolong"); - reg_func("getconst", "buildin_getconst"); - reg_func("new", "buildin_new"); - } - - public static void buildin_new(fake f, interpreter inter) throws Exception - { - BIF_CHECK_ARG_NUM(f, 1); - - String str = (String) fk.pspop(f); - - try - { - Class c = Class.forName(str); - Object o = c.newInstance(); - fk.pspush(f, o); - } - catch (Exception e) - { - fk.pspush(f, e.toString()); - } - } - - public static void buildin_getconst(fake f, interpreter inter) throws Exception - { - BIF_CHECK_ARG_NUM(f, 1); - - String str = (String) fk.pspop(f); - - variant v = f.ps.push_and_get(); - - variant gcv = f.pa.get_const_define(str); - if (gcv != null) - { - v.copy_from(gcv); - } - else - { - v.set_nil(); - } - } - - public static void buildin_tostring(fake f, interpreter inter) throws Exception - { - BIF_CHECK_ARG_NUM(f, 1); - - // container - variant v = f.ps.pop_and_get(); - if (v != null) - { - fk.pspush(f, v.toString()); - } - else - { - fk.pspush(f, ""); - } - } - - public static void buildin_tonumber(fake f, interpreter inter) throws Exception - { - BIF_CHECK_ARG_NUM(f, 1); - - variant v = f.ps.pop_and_get(); - double ret = 0; - if (v.m_type == variant_type.STRING) - { - ret = Double.valueOf((String) v.m_data); - } - else if (v.m_type == variant_type.REAL) - { - ret = (double) (Double) v.m_data; - } - else if (v.m_type == variant_type.UUID) - { - ret = (double) (long) (Long) v.m_data; - } - fk.pspush(f, ret); - } - - public static void buildin_tolong(fake f, interpreter inter) throws Exception - { - BIF_CHECK_ARG_NUM(f, 1); - - variant v = f.ps.pop_and_get(); - long ret = 0; - if (v.m_type == variant_type.STRING) - { - ret = Long.valueOf((String) v.m_data); - } - else if (v.m_type == variant_type.REAL) - { - ret = (long) (double) (Double) v.m_data; - } - else if (v.m_type == variant_type.UUID) - { - ret = (long) (Long) v.m_data; - } - fk.pspush(f, ret); - } - - public static void buildin_isfunc(fake f, interpreter inter) throws Exception - { - BIF_CHECK_ARG_NUM(f, 1); - - String str = (String) fk.pspop(f); - boolean ret = fk.isfunc(f, str); - fk.pspush(f, ret); - } - - public static void buildin_getcurcallstack(fake f, interpreter inter) - { - String str = fk.getcurcallstack(f); - fk.pspush(f, str); - } - - public static void buildin_getcurfunc(fake f, interpreter inter) - { - String str = fk.getcurfunc(f); - fk.pspush(f, str); - } - - public static void buildin_getcurline(fake f, interpreter inter) - { - int line = fk.getcurline(f); - fk.pspush(f, line); - } - - public static void buildin_getcurfile(fake f, interpreter inter) - { - String str = fk.getcurfile(f); - fk.pspush(f, str); - } - - public static void buildin_dofile(fake f, interpreter inter) throws Exception - { - BIF_CHECK_ARG_NUM(f, 1); - - String file = (String) fk.pspop(f); - boolean ret = fk.parse(f, file); - fk.pspush(f, ret); - } - - public static void buildin_dostring(fake f, interpreter inter) throws Exception - { - BIF_CHECK_ARG_NUM(f, 1); - - String str = (String) fk.pspop(f); - boolean ret = fk.parsestr(f, str); - fk.pspush(f, ret); - } - - public static void buildin_dumpfunc(fake f, interpreter inter) throws Exception - { - BIF_CHECK_ARG_NUM(f, 1); - - String func = (String) fk.pspop(f); - String str = f.bin.dump(func, -1); - fk.pspush(f, str); - } - - public static void buildin_dumpallfunc(fake f, interpreter inter) - { - String str = f.bin.dump(); - fk.pspush(f, str); - } - - public static void buildin_typeof(fake f, interpreter inter) throws Exception - { - BIF_CHECK_ARG_NUM(f, 1); - - variant v = f.ps.pop_and_get(); - String name = v.m_type.name(); - fk.pspush(f, name); - } - - public static void buildin_range(fake f, interpreter inter) throws Exception - { - BIF_CHECK_ARG_NUM(f, 2); - - // pos - int pos = (int) (Integer) fk.trans(fk.pspop(f), Integer.TYPE); - - // container - variant v = f.ps.pop_and_get(); - - if (v.m_type == variant_type.STRING) - { - if (pos >= 0 && pos < v.get_string().length()) - { - String ret = v.get_string().substring(pos, pos + 1); - fk.pspush(f, ret); - } - else - { - fk.pspush(f, ""); - } - } - else if (v.m_type == variant_type.ARRAY) - { - if (pos >= 0 && pos < v.get_array().m_va.size()) - { - variant ret = f.ps.push_and_get(); - if (v.get_array().m_va.get(pos) != null) - { - ret.copy_from(v.get_array().m_va.get(pos)); - } - else - { - ret.set_nil(); - } - } - else - { - fk.pspush(f, false); - } - } - else if (v.m_type == variant_type.MAP) - { - if (pos >= 0 && pos < v.get_map().m_vm.size()) - { - variant key = f.ps.push_and_get(); - - variant value = f.ps.push_and_get(); - - Set> set = v.get_map().m_vm.entrySet(); - - Map.Entry e = (Map.Entry) set.toArray()[pos]; - key.copy_from(e.getKey()); - value.copy_from(e.getValue()); - } - else - { - fk.pspush(f, false); - fk.pspush(f, false); - } - } - else - { - fk.pspush(f, false); - } - } - - public static void buildin_size(fake f, interpreter inter) throws Exception - { - BIF_CHECK_ARG_NUM(f, 1); - - variant v = f.ps.pop_and_get(); - int len = 0; - if (v.m_type == variant_type.STRING) - { - len = v.get_string().length(); - } - else if (v.m_type == variant_type.ARRAY) - { - len = v.get_array().m_va.size(); - } - else if (v.m_type == variant_type.MAP) - { - len = v.get_map().m_vm.size(); - } - fk.pspush(f, len); - } - - public static void buildin_map(fake f, interpreter inter) - { - variant_map m = new variant_map(); - variant v = f.ps.push_and_get(); - v.set_map(m); - } - - public static void buildin_gmap(fake f, interpreter inter) - { - variant_map m = f.rn.get_gmap(); - variant v = f.ps.push_and_get(); - v.set_map(m); - } - - public static void buildin_array(fake f, interpreter inter) - { - variant_array a = new variant_array(); - variant v = f.ps.push_and_get(); - v.set_array(a); - } - - public static void buildin_format(fake f, interpreter inter) - { - String formatstr = ""; - if (f.ps.size() > 0) - { - formatstr = f.ps.get(0).toString(); - } - - String str = ""; - int j = 1; - for (int i = 0; i < (int) formatstr.length(); i++) - { - if (formatstr.getBytes()[i] == '$') - { - if (i + 1 < (int) formatstr.length() && formatstr.getBytes()[i + 1] == '$') - { - str += formatstr.substring(i, i + 1); - i++; - } - else - { - if (j < (int) f.ps.size()) - { - str += f.ps.get(j).toString(); - j++; - } - } - } - else - { - str += formatstr.substring(i, i + 1); - } - } - - f.ps.clear(); - // ret - fk.pspush(f, str); - } - - public static void buildin_print(fake f, interpreter inter) - { - String str = ""; - - for (int i = 0; i < (int) f.ps.size(); i++) - { - str += f.ps.get(i).toString(); - } - - // printf - f.cb.on_print(f, str); - - f.ps.clear(); - - // ret - fk.pspush(f, 1); - } - - public static void BIF_CHECK_ARG_NUM(fake f, int n) throws Exception - { - if (f.ps.size() != n) - { - throw new Exception("buildin func param not match, give " + f.ps.size() + " need " + n); - } - } - - public void reg_func(String regname, String funcname) - { - synchronized (fk.class) - { - variant v = null; - bifunc bif = null; - if (fk.regName.get(regname) != null) - { - v = fk.regName.get(regname); - bif = fk.regBindFunc.get(regname); - } - else - { - v = new variant(); - v.set_string(regname); - - bif = new bifunc(); - - try - { - bif.m_m = this.getClass().getDeclaredMethod(funcname, fake.class, interpreter.class); - } - catch (Exception e) - { - System.out.println(e); - } - - fk.regName.put(regname, v); - fk.regBindFunc.put(regname, bif); - } - - m_f.fm.add_func(v, bif); - } - } - -} diff --git a/src/main/java/com/github/esrrhs/fakescript/callback.java b/src/main/java/com/github/esrrhs/fakescript/callback.java deleted file mode 100644 index 5d74bf7..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/callback.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.esrrhs.fakescript; - -public interface callback -{ - public void on_error(fake f, String file, int lineno, String func, - String str); - - public void on_print(fake f, String str); -} diff --git a/src/main/java/com/github/esrrhs/fakescript/codegen.java b/src/main/java/com/github/esrrhs/fakescript/codegen.java deleted file mode 100644 index 0b76f6e..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/codegen.java +++ /dev/null @@ -1,207 +0,0 @@ -package com.github.esrrhs.fakescript; - -import java.util.ArrayList; - -class codegen -{ - class block_identifiers - { - public String m_name; - public int m_pos; - - public block_identifiers(String m_name, int m_pos) - { - this.m_name = m_name; - this.m_pos = m_pos; - } - } - - private fake m_f; - private ArrayList> m_block_identifiers_stack = new ArrayList>(); - private ArrayList m_debug_block_identifiers_list = new ArrayList(); - private int m_stackpos; - private int m_maxstackpos; - private ArrayList m_byte_code_list = new ArrayList(); - private ArrayList m_byte_lineno_list = new ArrayList(); - private ArrayList m_const_list = new ArrayList(); - private ArrayList m_containeraddr_list = new ArrayList(); - - public codegen(fake f) - { - m_f = f; - } - - public void push_stack_identifiers() - { - m_block_identifiers_stack.add(new ArrayList()); - } - - public void pop_stack_identifiers() - { - ArrayList list = m_block_identifiers_stack.get(m_block_identifiers_stack.size() - 1); - int stacksize = list.size(); - m_block_identifiers_stack.remove(m_block_identifiers_stack.size() - 1); - m_stackpos -= stacksize; - } - - public int add_stack_identifier(String name, int line) - { - if (get_cur_variable_pos(name) != -1) - { - return -1; - } - ArrayList list = m_block_identifiers_stack.get(m_block_identifiers_stack.size() - 1); - list.add(new block_identifiers(name, m_stackpos)); - int ret = m_stackpos; - m_stackpos++; - if (m_stackpos > m_maxstackpos) - { - m_maxstackpos = m_stackpos; - } - - stack_variant_info tmp = new stack_variant_info(); - tmp.m_name = name; - tmp.m_line = line; - tmp.m_pos = ret; - m_debug_block_identifiers_list.add(tmp); - - return ret; - } - - public int get_cur_variable_pos(String name) - { - ArrayList list = m_block_identifiers_stack.get(m_block_identifiers_stack.size() - 1); - for (int i = 0; i < (int) list.size(); i++) - { - if (name.equals(list.get(i).m_name)) - { - return list.get(i).m_pos; - } - } - return -1; - } - - public int byte_code_size() - { - return m_byte_code_list.size(); - } - - public void push(long code, int lineno) - { - m_byte_code_list.add(code); - m_byte_lineno_list.add(lineno); - } - - public void set(int pos, long code) - { - m_byte_code_list.set(pos, code); - } - - public int getconst(variant v) - { - for (int i = 0; i < (int) m_const_list.size(); i++) - { - variant vv = m_const_list.get(i); - if (vv.equals(v)) - { - return i; - } - } - - int pos = m_const_list.size(); - m_const_list.add(v); - return pos; - } - - public int alloc_stack_identifier() - { - int ret = m_stackpos; - ArrayList list = m_block_identifiers_stack.get(m_block_identifiers_stack.size() - 1); - list.add(new block_identifiers("", m_stackpos)); - m_stackpos++; - if (m_stackpos > m_maxstackpos) - { - m_maxstackpos = m_stackpos; - } - return ret; - } - - public int getvariable(String name) - { - // 从下往上找 - for (int i = (int) m_block_identifiers_stack.size() - 1; i >= 0; i--) - { - ArrayList list = m_block_identifiers_stack.get(i); - for (int j = 0; j < (int) list.size(); j++) - { - if (name.equals(list.get(j).m_name)) - { - return list.get(j).m_pos; - } - } - } - return -1; - } - - public int getcontaineraddr(long con, long key) - { - for (int i = 0; i < (int) m_containeraddr_list.size(); i++) - { - container_addr pc = m_containeraddr_list.get(i); - if (con == pc.m_con && key == pc.m_key) - { - return i; - } - } - int pos = m_containeraddr_list.size(); - container_addr tmp = new container_addr(); - tmp.m_con = con; - tmp.m_key = key; - m_containeraddr_list.add(tmp); - return pos; - } - - public void output(String filename, String packagename, String name, func_binary bin) - { - bin.m_filename = filename; - bin.m_packagename = packagename; - bin.m_name = name; - - bin.m_maxstack = m_maxstackpos; - - bin.m_buff = new long[m_byte_code_list.size()]; - for (int i = 0; i < m_byte_code_list.size(); i++) - { - bin.m_buff[i] = m_byte_code_list.get(i); - } - - bin.m_lineno_buff = new int[m_byte_lineno_list.size()]; - for (int i = 0; i < m_byte_lineno_list.size(); i++) - { - bin.m_lineno_buff[i] = m_byte_lineno_list.get(i); - } - - bin.m_const_list = new variant[m_const_list.size()]; - for (int i = 0; i < m_const_list.size(); i++) - { - bin.m_const_list[i] = m_const_list.get(i); - } - - bin.m_container_addr_list = new container_addr[m_containeraddr_list.size()]; - for (int i = 0; i < m_containeraddr_list.size(); i++) - { - bin.m_container_addr_list[i] = m_containeraddr_list.get(i); - } - - bin.m_debug_stack_variant_info = new stack_variant_info[m_debug_block_identifiers_list.size()]; - for (int i = 0; i < m_debug_block_identifiers_list.size(); i++) - { - bin.m_debug_stack_variant_info[i] = m_debug_block_identifiers_list.get(i); - } - - bin.m_fresh++; - - types.log(m_f, "codegen out %s %d", name, m_maxstackpos); - } - -} diff --git a/src/main/java/com/github/esrrhs/fakescript/command.java b/src/main/java/com/github/esrrhs/fakescript/command.java deleted file mode 100644 index b5fca03..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/command.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.github.esrrhs.fakescript; - -public class command -{ - public static final long EMPTY_CMD = -1; - - public static final int COMMAND_OPCODE = 0; - public static final int COMMAND_ADDR = 1; - public static final int COMMAND_POS = 2; - - public static final int OPCODE_ASSIGN = 0; - public static final int OPCODE_PLUS = 1; - public static final int OPCODE_MINUS = 2; - public static final int OPCODE_MULTIPLY = 3; - public static final int OPCODE_DIVIDE = 4; - public static final int OPCODE_DIVIDE_MOD = 5; - public static final int OPCODE_STRING_CAT = 6; - public static final int OPCODE_PLUS_ASSIGN = 7; - public static final int OPCODE_MINUS_ASSIGN = 8; - public static final int OPCODE_MULTIPLY_ASSIGN = 9; - public static final int OPCODE_DIVIDE_ASSIGN = 10; - public static final int OPCODE_DIVIDE_MOD_ASSIGN = 11; - public static final int OPCODE_RETURN = 12; - public static final int OPCODE_JNE = 13; - public static final int OPCODE_JMP = 14; - public static final int OPCODE_FORBEGIN = 15; - public static final int OPCODE_FORLOOP = 16; - public static final int OPCODE_AND = 17; - public static final int OPCODE_OR = 18; - public static final int OPCODE_LESS = 19; - public static final int OPCODE_MORE = 20; - public static final int OPCODE_EQUAL = 21; - public static final int OPCODE_MOREEQUAL = 22; - public static final int OPCODE_LESSEQUAL = 23; - public static final int OPCODE_NOTEQUAL = 24; - public static final int OPCODE_NOT = 25; - public static final int OPCODE_AND_JNE = 26; - public static final int OPCODE_OR_JNE = 27; - public static final int OPCODE_LESS_JNE = 28; - public static final int OPCODE_MORE_JNE = 29; - public static final int OPCODE_EQUAL_JNE = 30; - public static final int OPCODE_MOREEQUAL_JNE = 31; - public static final int OPCODE_LESSEQUAL_JNE = 32; - public static final int OPCODE_NOTEQUAL_JNE = 33; - public static final int OPCODE_NOT_JNE = 34; - public static final int OPCODE_CALL = 35; - public static final int OPCODE_SLEEP = 36; - public static final int OPCODE_YIELD = 37; - public static final int OPCODE_MAX = 38; - - public static final int ADDR_STACK = 0; - public static final int ADDR_CONST = 1; - public static final int ADDR_CONTAINER = 2; - - public static final int CALL_NORMAL = 0; - public static final int CALL_FAKE = 1; - public static final int CALL_CLASSMEM = 2; - - public static long MAKEINT64(int high, int low) - { - return ((long) ((low) | ((long) (high)) << 32)); - } - - public static int HIINT32(long i) - { - return ((int) (((long) (i) >> 32) & 0xFFFFFFFF)); - } - - public static int LOINT32(long i) - { - return ((int) (i)); - } - - public static int MAKEINT32(int high, int low) - { - return ((int) (((short) (low)) | ((int) ((short) (high))) << 16)); - } - - public static short HIINT16(int i) - { - return ((short) (((int) (i) >> 16) & 0xFFFF)); - } - - public static short LOINT16(int i) - { - return ((short) (i)); - } - - public static long MAKE_COMMAND(int type, int code) - { - return MAKEINT64(type, code); - } - - public static long MAKE_OPCODE(int op) - { - return MAKE_COMMAND(COMMAND_OPCODE, op); - } - - public static long MAKE_POS(int pos) - { - return MAKE_COMMAND(COMMAND_POS, pos); - } - - public static long MAKE_ADDR(int addrtype, int pos) - { - return MAKE_COMMAND(COMMAND_ADDR, MAKEINT32(addrtype, pos)); - } - - public static int COMMAND_TYPE(long cmd) - { - return HIINT32(cmd); - } - - public static int COMMAND_CODE(long cmd) - { - return LOINT32(cmd); - } - - public static short ADDR_TYPE(int code) - { - return HIINT16(code); - } - - public static short ADDR_POS(int code) - { - return LOINT16(code); - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/compiler.java b/src/main/java/com/github/esrrhs/fakescript/compiler.java deleted file mode 100644 index a317a8d..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/compiler.java +++ /dev/null @@ -1,1744 +0,0 @@ -package com.github.esrrhs.fakescript; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map.Entry; - -import com.github.esrrhs.fakescript.syntree.*; - -class compiler -{ - private fake m_f; - private mybison m_mbs; - private String m_cur_compile_func; - private ArrayList> m_loop_break_pos_stack = new ArrayList>(); - private ArrayList m_loop_continue_pos_stack = new ArrayList(); - private ArrayList> m_continue_end_pos_stack = new ArrayList>(); - private boolean m_cmp_jne; - private long m_cur_addr; - private ArrayList m_cur_addrs = new ArrayList(); - private int m_cmp_deps; - private boolean m_new_var; - private int m_func_ret_num = 1; - - public compiler(fake f, mybison mbs) - { - m_f = f; - m_mbs = mbs; - } - - public boolean compile() throws Exception - { - if (!compile_const_head()) - { - return false; - } - - if (!compile_body()) - { - return false; - } - - return true; - } - - public boolean compile_const_head() throws Exception - { - types.log(m_f, "[compiler] compile_const_head"); - - // 注册全局常量表 - HashMap evm = m_mbs.get_const_map(); - Iterator> it = evm.entrySet().iterator(); - while (it.hasNext()) - { - Entry entry = it.next(); - String name = (String) entry.getKey(); - explicit_value_node ev = (explicit_value_node) entry.getValue(); - - variant v = compile_explicit_value_node_to_variant(ev); - if (v == null) - { - types.log(m_f, "[compiler] compile_explicit_value_node_to_variant %s fail", name); - return false; - } - - String constname = types.gen_package_name(m_mbs.get_package(), name); - - m_f.pa.reg_const_define(constname, v, ev.lineno()); - - types.log(m_f, "[compiler] reg_const_define %s %s", constname, v); - - } - return true; - } - - public boolean compile_body() throws Exception - { - for (func_desc_node funcnode : m_mbs.get_func_list()) - { - if (!compile_func(funcnode)) - { - types.log(m_f, "[compiler] compile_body %s fail", funcnode.m_funcname); - return false; - } - } - - if (m_f.cfg.open_debug_log != 0) - { - types.log(m_f, "[compiler] compile_body funclist %d ok dump \n%s", m_mbs.get_func_list().size(), - m_f.bin.dump()); - } - types.log(m_f, "[compiler] compile_body funcmap %d ok dump \n%s", m_f.fm.size(), m_f.fm.dump()); - - return true; - } - - public void compile_seterror(syntree_node node, String formatstr, Object... args) - { - String str = String.format(formatstr, args); - types.seterror(m_f, m_mbs.get_filename(), node.lineno(), m_cur_compile_func, str); - } - - public boolean compile_func(func_desc_node funcnode) throws Exception - { - m_cur_compile_func = funcnode.m_funcname; - - codegen cg = new codegen(m_f); - func_binary bin = new func_binary(); - bin.m_end_lineno = funcnode.m_endline; - - // 压栈 - cg.push_stack_identifiers(); - - // 参数入栈 - if (funcnode.m_arglist != null) - { - ArrayList arglist = funcnode.m_arglist.m_arglist; - for (int i = 0; i < (int) arglist.size(); i++) - { - String arg = arglist.get(i); - if (cg.add_stack_identifier(arg, funcnode.m_arglist.lineno()) == -1) - { - compile_seterror(funcnode.m_arglist, "double %s identifier error", arg); - return false; - } - } - bin.m_paramnum = arglist.size(); - } - - // 编译函数体 - if (funcnode.m_block != null) - { - if (!compile_block(cg, funcnode.m_block)) - { - return false; - } - } - - // break必须为空 - if (!m_loop_break_pos_stack.isEmpty()) - { - compile_seterror(funcnode, "compile extra break error"); - return false; - } - - // 编译成功 - String funcname = types.gen_package_name(m_mbs.get_package(), funcnode.m_funcname); - cg.output(m_mbs.get_filename(), m_mbs.get_package(), funcname, bin); - - // 优化 - m_f.opt.optimize(bin); - - // 看立即更新还是延迟更新 - variant fv = new variant(); - fv.set_string(funcname); - m_f.bin.add_func(fv, bin); - - types.log(m_f, "[compiler] compile_func func %s OK", funcname); - - return true; - } - - public boolean compile_block(codegen cg, block_node blocknode) throws Exception - { - for (int i = 0; i < (int) blocknode.m_stmtlist.size(); i++) - { - syntree_node stmt = blocknode.m_stmtlist.get(i); - if (!compile_node(cg, stmt)) - { - return false; - } - } - - return true; - } - - public boolean compile_node(codegen cg, syntree_node node) throws Exception - { - esyntreetype type = node.gettype(); - switch (type) - { - case est_block: - { - block_node bn = (block_node) (node); - if (!compile_block(cg, bn)) - { - return false; - } - break; - } - case est_while_stmt: - { - while_stmt ws = (while_stmt) (node); - if (!compile_while_stmt(cg, ws)) - { - return false; - } - break; - } - case est_for_stmt: - { - for_stmt fs = (for_stmt) (node); - if (!compile_for_stmt(cg, fs)) - { - return false; - } - break; - } - case est_multi_assign_stmt: - { - multi_assign_stmt as = (multi_assign_stmt) (node); - if (!compile_multi_assign_stmt(cg, as)) - { - return false; - } - break; - } - case est_cmp_stmt: - { - cmp_stmt cs = (cmp_stmt) (node); - if (!compile_cmp_stmt(cg, cs)) - { - return false; - } - break; - } - case est_if_stmt: - { - if_stmt is = (if_stmt) (node); - if (!compile_if_stmt(cg, is)) - { - return false; - } - break; - } - case est_explicit_value: - { - explicit_value_node ev = (explicit_value_node) (node); - if (!compile_explicit_value(cg, ev)) - { - return false; - } - break; - } - case est_return_stmt: - { - return_stmt rs = (return_stmt) (node); - if (!compile_return_stmt(cg, rs)) - { - return false; - } - break; - } - case est_return_value_list: - { - return_value_list_node rn = (return_value_list_node) (node); - if (!compile_return_value_list(cg, rn)) - { - return false; - } - break; - } - case est_assign_stmt: - { - assign_stmt as = (assign_stmt) (node); - if (!compile_assign_stmt(cg, as)) - { - return false; - } - break; - } - case est_math_assign_stmt: - { - math_assign_stmt ms = (math_assign_stmt) (node); - if (!compile_math_assign_stmt(cg, ms)) - { - return false; - } - break; - } - case est_variable: - { - variable_node vn = (variable_node) (node); - if (!compile_variable_node(cg, vn)) - { - return false; - } - break; - } - case est_var: - { - var_node vn = (var_node) (node); - if (!compile_var_node(cg, vn)) - { - return false; - } - break; - } - case est_function_call: - { - function_call_node fn = (function_call_node) (node); - if (!compile_function_call_node(cg, fn)) - { - return false; - } - break; - } - case est_break: - { - break_stmt bs = (break_stmt) (node); - if (!compile_break_stmt(cg, bs)) - { - return false; - } - break; - } - case est_continue: - { - continue_stmt cs = (continue_stmt) (node); - if (!compile_continue_stmt(cg, cs)) - { - return false; - } - break; - } - case est_math_expr: - { - math_expr_node mn = (math_expr_node) (node); - if (!compile_math_expr_node(cg, mn)) - { - return false; - } - break; - } - case est_container_get: - { - container_get_node cn = (container_get_node) (node); - if (!compile_container_get(cg, cn)) - { - return false; - } - break; - } - case est_struct_pointer: - { - struct_pointer_node cn = (struct_pointer_node) (node); - if (!compile_struct_pointer(cg, cn)) - { - return false; - } - break; - } - case est_sleep: - { - sleep_stmt ss = (sleep_stmt) (node); - if (!compile_sleep_stmt(cg, ss)) - { - return false; - } - break; - } - case est_yield: - { - yield_stmt ys = (yield_stmt) (node); - if (!compile_yield_stmt(cg, ys)) - { - return false; - } - break; - } - case est_switch_stmt: - { - switch_stmt ss = (switch_stmt) (node); - if (!compile_switch_stmt(cg, ss)) - { - return false; - } - break; - } - case est_for_loop_stmt: - { - for_loop_stmt fs = (for_loop_stmt) (node); - if (!compile_for_loop_stmt(cg, fs)) - { - return false; - } - break; - } - default: - { - compile_seterror(node, "compile node type error %s", type.toString()); - return false; - } - } - - return true; - } - - public variant compile_explicit_value_node_to_variant(explicit_value_node ev) throws Exception - { - variant v = new variant(); - switch (ev.m_type) - { - case EVT_NULL: - v.set_pointer(null); - break; - case EVT_TRUE: - v.set_real(1); - break; - case EVT_FALSE: - v.set_real(0); - break; - case EVT_NUM: - v.set_real(Integer.valueOf(ev.m_str)); - break; - case EVT_STR: - v.set_string(ev.m_str); - break; - case EVT_FLOAT: - v.set_real(Double.valueOf(ev.m_str)); - break; - case EVT_UUID: - v.set_uuid(Long.valueOf(ev.m_str.substring(0, ev.m_str.length() - 1))); - break; - case EVT_MAP: - { - const_map_list_value_node cml = (const_map_list_value_node) ev.m_v; - variant_map vm = new variant_map(); - vm.m_isconst = true; - - for (int i = 0; i < cml.m_lists.size(); i++) - { - const_map_value_node cmv = (const_map_value_node) cml.m_lists.get(i); - - explicit_value_node kn = (explicit_value_node) cmv.m_k; - explicit_value_node vn = (explicit_value_node) cmv.m_v; - - variant kv = compile_explicit_value_node_to_variant(kn); - variant vv = compile_explicit_value_node_to_variant(vn); - - variant des = vm.con_map_get(kv); - des.copy_from(vv); - } - - v.set_map(vm); - } - break; - case EVT_ARRAY: - { - const_array_list_value_node cal = (const_array_list_value_node) ev.m_v; - variant_array va = new variant_array(); - va.m_isconst = true; - for (int i = 0; i < cal.m_lists.size(); i++) - { - explicit_value_node vn = (explicit_value_node) cal.m_lists.get(i); - variant kv = new variant(); - kv.set_real(i); - - variant vv = compile_explicit_value_node_to_variant(vn); - - variant des = va.con_array_get(kv); - des.copy_from(vv); - } - - v.set_array(va); - } - break; - - default: - throw new Exception("compile explicit value type error " + ev.m_type.toString()); - } - return v; - } - - public boolean compile_while_stmt(codegen cg, while_stmt ws) throws Exception - { - int startpos = 0; - int jnepos = 0; - - m_loop_break_pos_stack.add(new ArrayList()); - - startpos = cg.byte_code_size(); - - m_loop_continue_pos_stack.add(startpos); - - // 条件 - cg.push_stack_identifiers(); - if (!compile_node(cg, ws.m_cmp)) - { - return false; - } - cg.pop_stack_identifiers(); - - // cmp与jne结合 - if (m_cmp_jne) - { - cg.push(command.EMPTY_CMD, ws.m_cmp.lineno()); // 先塞个位置 - jnepos = cg.byte_code_size() - 1; - } - else - { - cg.push(command.MAKE_OPCODE(command.OPCODE_JNE), ws.lineno()); - cg.push(m_cur_addr, ws.lineno()); - cg.push(command.EMPTY_CMD, ws.lineno()); // 先塞个位置 - jnepos = cg.byte_code_size() - 1; - } - m_cmp_deps = 0; - m_cmp_jne = false; - - // block块 - if (ws.m_block != null) - { - cg.push_stack_identifiers(); - if (!compile_node(cg, ws.m_block)) - { - return false; - } - cg.pop_stack_identifiers(); - } - - // 跳回判断地方 - cg.push(command.MAKE_OPCODE(command.OPCODE_JMP), ws.lineno()); - cg.push(command.MAKE_POS(startpos), ws.lineno()); - - // 跳转出block块 - cg.set(jnepos, command.MAKE_POS(cg.byte_code_size())); - - // 替换掉break - ArrayList bplist = m_loop_break_pos_stack.get(m_loop_break_pos_stack.size() - 1); - for (int i = 0; i < (int) bplist.size(); i++) - { - cg.set(bplist.get(i), command.MAKE_POS(cg.byte_code_size())); - } - m_loop_break_pos_stack.remove(m_loop_break_pos_stack.size() - 1); - - m_loop_continue_pos_stack.remove(m_loop_continue_pos_stack.size() - 1); - - return true; - } - - public boolean compile_for_stmt(codegen cg, for_stmt fs) throws Exception - { - int startpos = 0; - int jnepos = 0; - int continuepos = 0; - - m_loop_break_pos_stack.add(new ArrayList()); - m_continue_end_pos_stack.add(new ArrayList()); - - // 开始语句,这个作用域是全for都有效的 - cg.push_stack_identifiers(); - if (fs.m_beginblock != null) - { - if (!compile_node(cg, fs.m_beginblock)) - { - return false; - } - } - - startpos = cg.byte_code_size(); - // 需要continue end - m_loop_continue_pos_stack.add(-1); - - // 条件 - cg.push_stack_identifiers(); - if (!compile_node(cg, fs.m_cmp)) - { - return false; - } - cg.pop_stack_identifiers(); - - // cmp与jne结合 - if (m_cmp_jne) - { - cg.push(command.EMPTY_CMD, fs.m_cmp.lineno()); // 先塞个位置 - jnepos = cg.byte_code_size() - 1; - } - else - { - cg.push(command.MAKE_OPCODE(command.OPCODE_JNE), fs.lineno()); - cg.push(m_cur_addr, fs.lineno()); - cg.push(command.EMPTY_CMD, fs.lineno()); // 先塞个位置 - jnepos = cg.byte_code_size() - 1; - } - m_cmp_deps = 0; - m_cmp_jne = false; - - // block块 - if (fs.m_block != null) - { - cg.push_stack_identifiers(); - if (!compile_node(cg, fs.m_block)) - { - return false; - } - cg.pop_stack_identifiers(); - } - - continuepos = cg.byte_code_size(); - - // 结束 - if (fs.m_endblock != null) - { - cg.push_stack_identifiers(); - if (!compile_node(cg, fs.m_endblock)) - { - return false; - } - cg.pop_stack_identifiers(); - } - - // 跳回判断地方 - cg.push(command.MAKE_OPCODE(command.OPCODE_JMP), fs.lineno()); - cg.push(command.MAKE_POS(startpos), fs.lineno()); - - // 跳转出block块 - cg.set(jnepos, command.MAKE_POS(cg.byte_code_size())); - - // 替换掉break - ArrayList bplist = m_loop_break_pos_stack.get(m_loop_break_pos_stack.size() - 1); - for (int i = 0; i < (int) bplist.size(); i++) - { - cg.set(bplist.get(i), command.MAKE_POS(cg.byte_code_size())); - } - m_loop_break_pos_stack.remove(m_loop_break_pos_stack.size() - 1); - - // 替换掉continue - ArrayList cplist = m_continue_end_pos_stack.get(m_continue_end_pos_stack.size() - 1); - for (int i = 0; i < (int) cplist.size(); i++) - { - cg.set(cplist.get(i), command.MAKE_POS(continuepos)); - } - m_continue_end_pos_stack.remove(m_continue_end_pos_stack.size() - 1); - - m_loop_continue_pos_stack.remove(m_loop_continue_pos_stack.size() - 1); - - // 离开作用域 - cg.pop_stack_identifiers(); - - return true; - } - - public boolean compile_if_stmt(codegen cg, if_stmt is) throws Exception - { - int jnepos = 0; - ArrayList jmpifpos = new ArrayList(); - - // 条件 - cg.push_stack_identifiers(); - if (!compile_node(cg, is.m_cmp)) - { - return false; - } - cg.pop_stack_identifiers(); - - // cmp与jne结合 - if (m_cmp_jne) - { - cg.push(command.EMPTY_CMD, is.m_cmp.lineno()); // 先塞个位置 - jnepos = cg.byte_code_size() - 1; - } - else - { - cg.push(command.MAKE_OPCODE(command.OPCODE_JNE), is.lineno()); - cg.push(m_cur_addr, is.lineno()); - cg.push(command.EMPTY_CMD, is.lineno()); // 先塞个位置 - jnepos = cg.byte_code_size() - 1; - } - m_cmp_deps = 0; - m_cmp_jne = false; - - // if块 - if (is.m_block != null) - { - cg.push_stack_identifiers(); - if (!compile_node(cg, is.m_block)) - { - return false; - } - cg.pop_stack_identifiers(); - } - - // 跳出if块 - if (is.m_elseifs != null || (is.m_elses != null && is.m_elses.m_block != null)) - { - cg.push(command.MAKE_OPCODE(command.OPCODE_JMP), is.lineno()); - cg.push(command.EMPTY_CMD, is.lineno()); // 先塞个位置 - jmpifpos.add(cg.byte_code_size() - 1); - } - - // 开始处理elseif的 - if (is.m_elseifs != null) - { - ArrayList list = is.m_elseifs.m_stmtlist; - for (int i = 0; i < (int) list.size(); i++) - { - elseif_stmt eis = (elseif_stmt) (list.get(i)); - - // 跳转到else if - cg.set(jnepos, command.MAKE_POS(cg.byte_code_size())); - - // 条件 - cg.push_stack_identifiers(); - if (!compile_node(cg, eis.m_cmp)) - { - return false; - } - cg.pop_stack_identifiers(); - - // cmp与jne结合 - if (m_cmp_jne) - { - cg.push(command.EMPTY_CMD, eis.m_cmp.lineno()); // 先塞个位置 - jnepos = cg.byte_code_size() - 1; - } - else - { - cg.push(command.MAKE_OPCODE(command.OPCODE_JNE), eis.lineno()); - cg.push(m_cur_addr, eis.lineno()); - cg.push(command.EMPTY_CMD, eis.lineno()); // 先塞个位置 - jnepos = cg.byte_code_size() - 1; - } - m_cmp_deps = 0; - m_cmp_jne = false; - - // else if块 - if (eis.m_block != null) - { - cg.push_stack_identifiers(); - if (!compile_node(cg, eis.m_block)) - { - return false; - } - cg.pop_stack_identifiers(); - } - - // 跳出if块 - cg.push(command.MAKE_OPCODE(command.OPCODE_JMP), eis.lineno()); - cg.push(command.EMPTY_CMD, eis.lineno()); // 先塞个位置 - jmpifpos.add(cg.byte_code_size() - 1); - } - } - - // 跳转到else - cg.set(jnepos, command.MAKE_POS(cg.byte_code_size())); - - // else块 - if (is.m_elses != null && is.m_elses.m_block != null) - { - cg.push_stack_identifiers(); - if (!compile_node(cg, is.m_elses.m_block)) - { - return false; - } - cg.pop_stack_identifiers(); - } - - // 跳转到结束 - for (int i = 0; i < (int) jmpifpos.size(); i++) - { - cg.set(jmpifpos.get(i), command.MAKE_POS(cg.byte_code_size())); - } - - return true; - } - - public boolean compile_return_stmt(codegen cg, return_stmt rs) throws Exception - { - if (rs.m_returnlist != null) - { - if (!compile_node(cg, rs.m_returnlist)) - { - return false; - } - - cg.push(command.MAKE_OPCODE(command.OPCODE_RETURN), rs.lineno()); - cg.push(command.MAKE_POS(rs.m_returnlist.m_returnlist.size()), rs.lineno()); - for (int i = 0; i < (int) rs.m_returnlist.m_returnlist.size(); i++) - { - cg.push(m_cur_addrs.get(i), rs.lineno()); - } - } - else - { - cg.push(command.MAKE_OPCODE(command.OPCODE_RETURN), rs.lineno()); - cg.push(command.MAKE_POS(0), rs.lineno()); - } - - return true; - } - - public boolean compile_assign_stmt(codegen cg, assign_stmt as) throws Exception - { - long var = 0; - long value = 0; - - if (!compile_node(cg, as.m_value)) - { - return false; - } - value = m_cur_addr; - - m_new_var = as.m_isnew; - if (!compile_node(cg, as.m_var)) - { - return false; - } - m_new_var = false; - var = m_cur_addr; - - cg.push(command.MAKE_OPCODE(command.OPCODE_ASSIGN), as.lineno()); - cg.push(var, as.lineno()); - cg.push(value, as.lineno()); - - return true; - } - - public boolean compile_multi_assign_stmt(codegen cg, multi_assign_stmt as) throws Exception - { - // 目前多重赋值只支持a,b,c = myfunc1(),需要告诉func1多返回几个值 - m_func_ret_num = as.m_varlist.m_varlist.size(); - - // 编译value - if (!compile_node(cg, as.m_value)) - { - return false; - } - - // 挨个编译var - ArrayList varlist = new ArrayList(); - for (int i = 0; i < (int) as.m_varlist.m_varlist.size(); i++) - { - m_new_var = as.m_isnew; - if (!compile_node(cg, as.m_varlist.m_varlist.get(i))) - { - return false; - } - m_new_var = false; - varlist.add(m_cur_addr); - } - - // 挨个赋值 - for (int i = 0; i < (int) as.m_varlist.m_varlist.size(); i++) - { - long var = 0; - long value = 0; - - var = varlist.get(i); - value = m_cur_addrs.get(i); - - cg.push(command.MAKE_OPCODE(command.OPCODE_ASSIGN), as.lineno()); - cg.push(var, as.lineno()); - cg.push(value, as.lineno()); - } - - return true; - } - - public boolean compile_math_assign_stmt(codegen cg, math_assign_stmt ms) throws Exception - { - long oper = 0; - long var = 0; - long value = 0; - - if (ms.m_oper.equals("+=")) - { - oper = command.MAKE_OPCODE(command.OPCODE_PLUS_ASSIGN); - } - else if (ms.m_oper.equals("-=")) - { - oper = command.MAKE_OPCODE(command.OPCODE_MINUS_ASSIGN); - } - else if (ms.m_oper.equals("*=")) - { - oper = command.MAKE_OPCODE(command.OPCODE_MULTIPLY_ASSIGN); - } - else if (ms.m_oper.equals("/=")) - { - oper = command.MAKE_OPCODE(command.OPCODE_DIVIDE_ASSIGN); - } - else if (ms.m_oper.equals("%=")) - { - oper = command.MAKE_OPCODE(command.OPCODE_DIVIDE_MOD_ASSIGN); - } - else - { - compile_seterror(ms, "compile math assign oper type %s error", ms.m_oper); - return false; - } - - // value - if (!compile_node(cg, ms.m_value)) - { - return false; - } - value = m_cur_addr; - - // var - if (!compile_node(cg, ms.m_var)) - { - return false; - } - var = m_cur_addr; - - cg.push(oper, ms.lineno()); - cg.push(var, ms.lineno()); - cg.push(value, ms.lineno()); - - return true; - } - - public boolean compile_break_stmt(codegen cg, break_stmt bs) throws Exception - { - cg.push(command.MAKE_OPCODE(command.OPCODE_JMP), bs.lineno()); - cg.push(command.EMPTY_CMD, bs.lineno()); // 先塞个位置 - int jmppos = cg.byte_code_size() - 1; - - ArrayList bplist = m_loop_break_pos_stack.get(m_loop_break_pos_stack.size() - 1); - bplist.add(jmppos); - - return true; - } - - public boolean compile_continue_stmt(codegen cg, continue_stmt cs) throws Exception - { - if (m_loop_continue_pos_stack.isEmpty()) - { - compile_seterror(cs, "no loop to continue"); - return false; - } - - int continuepos = m_loop_continue_pos_stack.get(m_loop_continue_pos_stack.size() - 1); - - cg.push(command.MAKE_OPCODE(command.OPCODE_JMP), cs.lineno()); - cg.push(command.MAKE_POS(continuepos), cs.lineno()); - - if (continuepos == -1) - { - // 一会统一设置 - int pos = cg.byte_code_size() - 1; - ArrayList cplist = m_continue_end_pos_stack.get(m_continue_end_pos_stack.size() - 1); - cplist.add(pos); - } - - return true; - } - - public boolean compile_cmp_stmt(codegen cg, cmp_stmt cs) throws Exception - { - int deps = m_cmp_deps; - m_cmp_deps++; - - long oper = 0; - long left = 0; - long right = 0; - long dest = 0; - - if (!cs.m_cmp.equals("not")) - { - // oper - if (cs.m_cmp.equals("&&")) - { - oper = deps == 0 ? command.MAKE_OPCODE(command.OPCODE_AND_JNE) - : command.MAKE_OPCODE(command.OPCODE_AND); - } - else if (cs.m_cmp.equals("||")) - { - oper = deps == 0 ? command.MAKE_OPCODE(command.OPCODE_OR_JNE) : command.MAKE_OPCODE(command.OPCODE_OR); - } - else if (cs.m_cmp.equals("<")) - { - oper = deps == 0 ? command.MAKE_OPCODE(command.OPCODE_LESS_JNE) - : command.MAKE_OPCODE(command.OPCODE_LESS); - } - else if (cs.m_cmp.equals(">")) - { - oper = deps == 0 ? command.MAKE_OPCODE(command.OPCODE_MORE_JNE) - : command.MAKE_OPCODE(command.OPCODE_MORE); - } - else if (cs.m_cmp.equals("==")) - { - oper = deps == 0 ? command.MAKE_OPCODE(command.OPCODE_EQUAL_JNE) - : command.MAKE_OPCODE(command.OPCODE_EQUAL); - } - else if (cs.m_cmp.equals(">=")) - { - oper = deps == 0 ? command.MAKE_OPCODE(command.OPCODE_MOREEQUAL_JNE) - : command.MAKE_OPCODE(command.OPCODE_MOREEQUAL); - } - else if (cs.m_cmp.equals("<=")) - { - oper = deps == 0 ? command.MAKE_OPCODE(command.OPCODE_LESSEQUAL_JNE) - : command.MAKE_OPCODE(command.OPCODE_LESSEQUAL); - } - else if (cs.m_cmp.equals("!=")) - { - oper = deps == 0 ? command.MAKE_OPCODE(command.OPCODE_NOTEQUAL_JNE) - : command.MAKE_OPCODE(command.OPCODE_NOTEQUAL); - } - else if (cs.m_cmp.equals("true")) - { - variant v = new variant(); - v.set_real(1); - int pos = cg.getconst(v); - m_cur_addr = command.MAKE_ADDR(command.ADDR_CONST, pos); - - m_cmp_deps--; - m_cmp_jne = false; - - return true; - } - else if (cs.m_cmp.equals("false")) - { - variant v = new variant(); - v.set_real(0); - int pos = cg.getconst(v); - m_cur_addr = command.MAKE_ADDR(command.ADDR_CONST, pos); - - m_cmp_deps--; - m_cmp_jne = false; - - return true; - } - else if (cs.m_cmp.equals("is")) - { - // left - if (!compile_node(cg, cs.m_left)) - { - return false; - } - - m_cmp_deps--; - m_cmp_jne = false; - - return true; - } - else - { - compile_seterror(cs, "cmp error %s", cs.m_cmp); - return false; - } - - // left - if (!compile_node(cg, cs.m_left)) - { - return false; - } - left = m_cur_addr; - - // right - if (!compile_node(cg, cs.m_right)) - { - return false; - } - right = m_cur_addr; - - // result - int despos = cg.alloc_stack_identifier(); - dest = command.MAKE_ADDR(command.ADDR_STACK, despos); - m_cur_addr = dest; - - cg.push(oper, cs.lineno()); - cg.push(left, cs.lineno()); - cg.push(right, cs.lineno()); - cg.push(dest, cs.lineno()); - } - /* "not" */ - else - { - oper = deps == 0 ? command.MAKE_OPCODE(command.OPCODE_NOT_JNE) : command.MAKE_OPCODE(command.OPCODE_NOT); - - // left - if (!compile_node(cg, cs.m_left)) - { - return false; - } - left = m_cur_addr; - - int despos = cg.alloc_stack_identifier(); - dest = command.MAKE_ADDR(command.ADDR_STACK, despos); - m_cur_addr = dest; - - cg.push(oper, cs.lineno()); - cg.push(left, cs.lineno()); - cg.push(dest, cs.lineno()); - } - - m_cmp_deps--; - if (deps == 0) - { - m_cmp_jne = true; - } - - return true; - } - - public boolean compile_explicit_value(codegen cg, explicit_value_node ev) throws Exception - { - variant v = compile_explicit_value_node_to_variant(ev); - - int pos = cg.getconst(v); - m_cur_addr = command.MAKE_ADDR(command.ADDR_CONST, pos); - - return true; - } - - public boolean compile_variable_node(codegen cg, variable_node vn) throws Exception - { - // 看看是否是全局常量定义 - String constname = types.gen_package_name(m_mbs.get_package(), vn.m_str); - variant gcv = m_f.pa.get_const_define(constname); - if (gcv != null) - { - int pos = cg.getconst(gcv); - m_cur_addr = command.MAKE_ADDR(command.ADDR_CONST, pos); - - return true; - } - - gcv = m_f.pa.get_const_define(vn.m_str); - if (gcv != null) - { - int pos = cg.getconst(gcv); - m_cur_addr = command.MAKE_ADDR(command.ADDR_CONST, pos); - - return true; - } - - // 从当前堆栈往上找 - int pos = cg.getvariable(vn.m_str); - if (pos == -1) - { - // 是不是需要new出来 - if (m_new_var) - { - var_node tmp = new var_node(); - tmp.m_str = vn.m_str; - return compile_var_node(cg, tmp); - } - else - { - compile_seterror(vn, "variable %s not found", vn.m_str); - return false; - } - } - m_cur_addr = command.MAKE_ADDR(command.ADDR_STACK, pos); - - return true; - } - - public boolean compile_var_node(codegen cg, var_node vn) throws Exception - { - // 确保当前block没有 - if (cg.get_cur_variable_pos(vn.m_str) != -1) - { - compile_seterror(vn, "variable %s has define", vn.m_str); - return false; - } - - // 看看是否是常量定义 - HashMap evm = m_mbs.get_const_map(); - if (evm.get(vn.m_str) != null) - { - compile_seterror(vn, "variable %s has defined const", vn.m_str); - return false; - } - - // 看看是否是全局常量定义 - variant gcv = m_f.pa.get_const_define(vn.m_str); - if (gcv != null) - { - compile_seterror(vn, "variable %s has defined global const", vn.m_str); - return false; - } - - // 申请栈上空间 - int pos = cg.add_stack_identifier(vn.m_str, vn.lineno()); - if (pos == -1) - { - compile_seterror(vn, "double %s identifier error", vn.m_str); - return false; - } - m_cur_addr = command.MAKE_ADDR(command.ADDR_STACK, pos); - - return true; - } - - public boolean compile_function_call_node(codegen cg, function_call_node fn) throws Exception - { - int ret_num = m_func_ret_num; - m_func_ret_num = 1; - - // 参数 - ArrayList arglist = new ArrayList(); - if (fn.m_arglist != null) - { - for (int i = 0; i < (int) fn.m_arglist.m_arglist.size(); i++) - { - syntree_node sn = fn.m_arglist.m_arglist.get(i); - if (!compile_node(cg, sn)) - { - return false; - } - arglist.add(m_cur_addr); - } - } - - // 调用位置 - long callpos; - if (fn.m_prefuc == null) - { - String func = fn.m_fuc; - // 1 检查变量 - int pos = cg.getvariable(func); - if (pos != -1) - { - // 是用变量来调用函数 - callpos = command.MAKE_ADDR(command.ADDR_STACK, pos); - } - // 2 检查struct - else if (m_mbs.is_have_struct(func)) - { - // 直接替换成map - variant v = new variant(); - v.set_string(interpreter.MAP_FUNC_NAME); - pos = cg.getconst(v); - callpos = command.MAKE_ADDR(command.ADDR_CONST, pos); - } - // 3 检查本地函数 - else if (m_mbs.is_have_func(func)) - { - // 申请字符串变量 - variant v = new variant(); - // 拼上包名 - String pname = types.gen_package_name(m_mbs.get_package(), func); - v.set_string(pname); - pos = cg.getconst(v); - callpos = command.MAKE_ADDR(command.ADDR_CONST, pos); - } - // 4 直接字符串使用 - else - { - // 申请字符串变量 - variant v = new variant(); - v.set_string(func); - pos = cg.getconst(v); - callpos = command.MAKE_ADDR(command.ADDR_CONST, pos); - } - } - else - { - // 函数名从前面的函数得来 - if (!compile_node(cg, fn.m_prefuc)) - { - return false; - } - - callpos = m_cur_addr; - } - - // oper - long oper; - oper = command.MAKE_OPCODE(command.OPCODE_CALL); - - // 调用类型 - long calltype; - if (fn.m_fakecall) - { - calltype = command.MAKE_POS(command.CALL_FAKE); - } - else if (fn.m_classmem_call) - { - calltype = command.MAKE_POS(command.CALL_CLASSMEM); - } - else - { - calltype = command.MAKE_POS(command.CALL_NORMAL); - } - - // 参数个数 - long argnum; - argnum = command.MAKE_POS(arglist.size()); - - // 返回值个数 - long retnum; - retnum = command.MAKE_POS(ret_num); - - int oldretsize = m_cur_addrs.size(); - for (int i = 0; i < ret_num - oldretsize; i++) - { - m_cur_addrs.add(new Long(0)); - } - - // 返回值 - ArrayList ret = new ArrayList(); - for (int i = 0; i < ret_num; i++) - { - int retpos = cg.alloc_stack_identifier(); - ret.add(command.MAKE_ADDR(command.ADDR_STACK, retpos)); - - m_cur_addrs.set(i, ret.get(i)); - } - m_cur_addr = ret.size() > 0 ? ret.get(0) : 0; - - cg.push(oper, fn.lineno()); - cg.push(calltype, fn.lineno()); - cg.push(callpos, fn.lineno()); - cg.push(retnum, fn.lineno()); - for (int i = 0; i < ret_num; i++) - { - cg.push(ret.get(i), fn.lineno()); - } - cg.push(argnum, fn.lineno()); - for (int i = 0; i < (int) arglist.size(); i++) - { - cg.push(arglist.get(i), fn.lineno()); - } - - return true; - } - - public boolean compile_math_expr_node(codegen cg, math_expr_node mn) throws Exception - { - long oper = 0; - long left = 0; - long right = 0; - long dest = 0; - - if (mn.m_oper.equals("+")) - { - oper = command.MAKE_OPCODE(command.OPCODE_PLUS); - } - else if (mn.m_oper.equals("-")) - { - oper = command.MAKE_OPCODE(command.OPCODE_MINUS); - } - else if (mn.m_oper.equals("*")) - { - oper = command.MAKE_OPCODE(command.OPCODE_MULTIPLY); - } - else if (mn.m_oper.equals("/")) - { - oper = command.MAKE_OPCODE(command.OPCODE_DIVIDE); - } - else if (mn.m_oper.equals("%")) - { - oper = command.MAKE_OPCODE(command.OPCODE_DIVIDE_MOD); - } - else if (mn.m_oper.equals("..")) - { - oper = command.MAKE_OPCODE(command.OPCODE_STRING_CAT); - } - else - { - compile_seterror(mn, "compile math oper type %s error", mn.m_oper); - return false; - } - - // left - if (!compile_node(cg, mn.m_left)) - { - return false; - } - left = m_cur_addr; - - // right - if (!compile_node(cg, mn.m_right)) - { - return false; - } - right = m_cur_addr; - - // result - int despos = cg.alloc_stack_identifier(); - dest = command.MAKE_ADDR(command.ADDR_STACK, despos); - m_cur_addr = dest; - - cg.push(oper, mn.lineno()); - cg.push(left, mn.lineno()); - cg.push(right, mn.lineno()); - cg.push(dest, mn.lineno()); - - return true; - } - - public boolean compile_return_value_list(codegen cg, return_value_list_node rn) throws Exception - { - ArrayList tmp = new ArrayList(); - for (int i = 0; i < (int) rn.m_returnlist.size(); i++) - { - if (!compile_node(cg, rn.m_returnlist.get(i))) - { - return false; - } - tmp.add(i, m_cur_addr); - } - m_cur_addrs = tmp; - m_cur_addr = m_cur_addrs.get(0); - - return true; - } - - public boolean compile_container_get(codegen cg, container_get_node cn) throws Exception - { - // 编译con - long con = 0; - - // 看看是否是全局常量定义 - variant gcv = m_f.pa.get_const_define(cn.m_container); - if (gcv != null) - { - int pos = cg.getconst(gcv); - con = command.MAKE_ADDR(command.ADDR_CONST, pos); - } - else - { - int pos = cg.getvariable(cn.m_container); - if (pos == -1) - { - compile_seterror(cn, "variable %s not found", cn.m_container); - return false; - } - con = command.MAKE_ADDR(command.ADDR_STACK, pos); - } - - // 编译key - long key = 0; - if (!compile_node(cg, cn.m_key)) - { - return false; - } - key = m_cur_addr; - - // 返回 - int addrpos = cg.getcontaineraddr(con, key); - m_cur_addr = command.MAKE_ADDR(command.ADDR_CONTAINER, addrpos); - - return true; - } - - public boolean compile_struct_pointer(codegen cg, struct_pointer_node sn) throws Exception - { - String name = sn.m_str; - ArrayList tmp = new ArrayList(); - do - { - int pos = name.indexOf("->"); - if (pos == -1) - { - tmp.add(name); - break; - } - tmp.add(name.substring(0, pos)); - name = name.substring(pos + 2); - } - while (true); - - if (tmp.size() < 2) - { - return false; - } - - String connname = tmp.get(0); - - // 编译con - long con = 0; - - // 看看是否是全局常量定义 - variant gcv = m_f.pa.get_const_define(connname); - if (gcv != null) - { - int pos = cg.getconst(gcv); - con = command.MAKE_ADDR(command.ADDR_CONST, pos); - } - else - { - int pos = cg.getvariable(connname); - if (pos == -1) - { - compile_seterror(sn, "variable %s not found", connname); - return false; - } - con = command.MAKE_ADDR(command.ADDR_STACK, pos); - } - - for (int i = 1; i < (int) tmp.size(); i++) - { - String keystr = tmp.get(i); - - // 编译key - variant v = new variant(); - v.set_string(keystr); - int pos = cg.getconst(v); - long key = command.MAKE_ADDR(command.ADDR_CONST, pos); - - // 获取容器的位置 - int addrpos = cg.getcontaineraddr(con, key); - m_cur_addr = command.MAKE_ADDR(command.ADDR_CONTAINER, addrpos); - con = m_cur_addr; - } - - return true; - } - - public boolean compile_sleep_stmt(codegen cg, sleep_stmt ss) throws Exception - { - // 编译time - long time = 0; - if (!compile_node(cg, ss.m_time)) - { - return false; - } - time = m_cur_addr; - - cg.push(command.MAKE_OPCODE(command.OPCODE_SLEEP), ss.lineno()); - cg.push(time, ss.lineno()); - - return true; - } - - public boolean compile_yield_stmt(codegen cg, yield_stmt ys) throws Exception - { - // 编译time - long time = 0; - if (!compile_node(cg, ys.m_time)) - { - return false; - } - time = m_cur_addr; - - cg.push(command.MAKE_OPCODE(command.OPCODE_YIELD), ys.lineno()); - cg.push(time, ys.lineno()); - - return true; - } - - public boolean compile_switch_stmt(codegen cg, switch_stmt ss) throws Exception - { - long caseleft; - long caseresult; - - cg.push_stack_identifiers(); - - // caseleft - if (!compile_node(cg, ss.m_cmp)) - { - return false; - } - caseleft = m_cur_addr; - - // caseresult - int despos = cg.alloc_stack_identifier(); - caseresult = command.MAKE_ADDR(command.ADDR_STACK, despos); - - switch_caselist_node scln = (switch_caselist_node) (ss.m_caselist); - - ArrayList jmpswitchposlist = new ArrayList(); - - // 挨个和case的比较 - for (int i = 0; i < (int) scln.m_list.size(); i++) - { - long oper = command.MAKE_OPCODE(command.OPCODE_EQUAL); - long left = caseleft; - long right = 0; - long dest = caseresult; - - switch_case_node scn = (switch_case_node) (scln.m_list.get(i)); - - // right - if (!compile_node(cg, scn.m_cmp)) - { - return false; - } - right = m_cur_addr; - - // push case - cg.push(oper, scn.lineno()); - cg.push(left, scn.lineno()); - cg.push(right, scn.lineno()); - cg.push(dest, scn.lineno()); - - // push jmp - cg.push(command.MAKE_OPCODE(command.OPCODE_JNE), scn.lineno()); - cg.push(dest, scn.lineno()); - cg.push(command.EMPTY_CMD, scn.lineno()); // 先塞个位置 - int jnepos = cg.byte_code_size() - 1; - - // build block - if (scn.m_block != null) - { - cg.push_stack_identifiers(); - if (!compile_node(cg, scn.m_block)) - { - return false; - } - cg.pop_stack_identifiers(); - } - - // 跳出switch块 - cg.push(command.MAKE_OPCODE(command.OPCODE_JMP), scn.lineno()); - cg.push(command.EMPTY_CMD, scn.lineno()); // 先塞个位置 - int jmpswitchpos = cg.byte_code_size() - 1; - jmpswitchposlist.add(jmpswitchpos); - - // 跳转出case块 - cg.set(jnepos, command.MAKE_POS(cg.byte_code_size())); - - } - - // default - if (ss.m_def != null) - { - cg.push_stack_identifiers(); - if (!compile_node(cg, ss.m_def)) - { - return false; - } - cg.pop_stack_identifiers(); - } - - cg.pop_stack_identifiers(); - - // 塞跳出的 - for (int i = 0; i < (int) jmpswitchposlist.size(); i++) - { - cg.set(jmpswitchposlist.get(i), command.MAKE_POS(cg.byte_code_size())); - } - - return true; - } - - public boolean compile_for_loop_stmt(codegen cg, for_loop_stmt fs) throws Exception - { - int startpos = 0; - int jnepos = 0; - int continuepos = 0; - - m_loop_break_pos_stack.add(new ArrayList()); - m_continue_end_pos_stack.add(new ArrayList()); - - // 初始值 - if (!compile_node(cg, fs.m_begin)) - { - return false; - } - long begin = m_cur_addr; - - // 循环变量,这个作用域是全for都有效的 - cg.push_stack_identifiers(); - if (!compile_node(cg, fs.m_var)) - { - return false; - } - long var = m_cur_addr; - - // 最大值 - if (!compile_node(cg, fs.m_end)) - { - return false; - } - long end = m_cur_addr; - - // 变化值 - if (!compile_node(cg, fs.m_add)) - { - return false; - } - long add = m_cur_addr; - - // 塞for头 - cg.push(command.MAKE_OPCODE(command.OPCODE_FORBEGIN), fs.lineno()); - cg.push(var, fs.lineno()); - cg.push(begin, fs.lineno()); - cg.push(end, fs.lineno()); - cg.push(add, fs.lineno()); - cg.push(command.EMPTY_CMD, fs.lineno()); // 先塞个位置 - jnepos = cg.byte_code_size() - 1; - - // 需要continue end - m_loop_continue_pos_stack.add(-1); - - startpos = cg.byte_code_size(); - - // block块 - if (fs.m_block != null) - { - cg.push_stack_identifiers(); - if (!compile_node(cg, fs.m_block)) - { - return false; - } - cg.pop_stack_identifiers(); - } - - continuepos = cg.byte_code_size(); - - // 最大值 - if (!compile_node(cg, fs.m_end)) - { - return false; - } - end = m_cur_addr; - - // 变化值 - if (!compile_node(cg, fs.m_add)) - { - return false; - } - add = m_cur_addr; - - // 塞for loop - cg.push(command.MAKE_OPCODE(command.OPCODE_FORLOOP), fs.lineno()); - cg.push(var, fs.lineno()); - cg.push(end, fs.lineno()); - cg.push(add, fs.lineno()); - cg.push(command.MAKE_POS(startpos), fs.lineno()); - - // 跳转出block块 - cg.set(jnepos, command.MAKE_POS(cg.byte_code_size())); - - // 替换掉break - ArrayList bplist = m_loop_break_pos_stack.get(m_loop_break_pos_stack.size() - 1); - for (int i = 0; i < (int) bplist.size(); i++) - { - cg.set(bplist.get(i), command.MAKE_POS(cg.byte_code_size())); - } - m_loop_break_pos_stack.remove(m_loop_break_pos_stack.size() - 1); - - // 替换掉continue - ArrayList cplist = m_continue_end_pos_stack.get(m_continue_end_pos_stack.size() - 1); - for (int i = 0; i < (int) cplist.size(); i++) - { - cg.set(cplist.get(i), command.MAKE_POS(continuepos)); - } - m_continue_end_pos_stack.remove(m_continue_end_pos_stack.size() - 1); - - m_loop_continue_pos_stack.remove(m_loop_continue_pos_stack.size() - 1); - - // 离开作用域 - cg.pop_stack_identifiers(); - - return true; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/container_addr.java b/src/main/java/com/github/esrrhs/fakescript/container_addr.java deleted file mode 100644 index 137645a..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/container_addr.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.github.esrrhs.fakescript; - -class container_addr -{ - public long m_con; - public long m_key; -} diff --git a/src/main/java/com/github/esrrhs/fakescript/debuging.java b/src/main/java/com/github/esrrhs/fakescript/debuging.java deleted file mode 100644 index fb7963c..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/debuging.java +++ /dev/null @@ -1,769 +0,0 @@ -package com.github.esrrhs.fakescript; - -import java.io.BufferedInputStream; -import java.io.DataInputStream; -import java.util.ArrayList; - -class breakpoint -{ - boolean enable; - int id; - String file; - int line; -}; - -class debuging -{ - fake m_f; - variant m_ret = new variant(); - - static final int debug_next = 0; - static final int debug_step = 1; - static final int debug_next_instruction = 2; - static final int debug_step_instruction = 3; - static final int debug_continue = 4; - static final int debug_breakpoint = 5; - static final int debug_enable = 6; - static final int debug_disable = 7; - static final int debug_delete = 8; - static final int debug_info = 9; - static final int debug_finish = 10; - static final int debug_list = 11; - static final int debug_print = 12; - static final int debug_set = 13; - static final int debug_watch = 14; - static final int debug_backtrace = 15; - static final int debug_frame = 16; - static final int debug_disa = 17; - static final int debug_routine = 18; - - public debuging(fake f) - { - m_f = f; - } - - void debug() throws Exception - { - fake f = m_f; - boolean isend = false; - int range = 0; - int command = debug_next; - ArrayList paramvec = new ArrayList(); - String lastfunc = ""; - ArrayList blist = new ArrayList(); - int bindex = 0; - int frame = 0; - int lastrid = fk.getcurroutineid(f); - int rid = lastrid; - ArrayList watchvec = new ArrayList(); - boolean firsttime = true; - boolean isgoto = false; - while (true) - { - if (!isgoto) - { - show_watch_variant(f, rid, frame, watchvec); - warper lastridw = new warper(lastrid); - warper lastfuncw = new warper(lastfunc); - check_show_func_header(f, rid, frame, lastridw, lastfuncw); - lastrid = (int) (Integer) lastridw.d; - lastfunc = (String) lastfuncw.d; - show_debug_code(f, rid, frame, range); - } - isgoto = false; - - command = get_debug_command(f, command, paramvec); - switch (command) - { - case debug_next: - { - frame = 0; - rid = fk.getcurroutineid(f); - - if (firsttime) - { - if (check_trigger_breakpoint(f, blist)) - { - firsttime = false; - break; - } - } - firsttime = false; - - String lastfile = fk.getcurfile(f); - int lastline = fk.getcurline(f); - int laststacklength = fk.getcurcallstacklength(f); - int lastridex = rid; - String curfile = fk.getcurfile(f); - int curline = fk.getcurline(f); - int curstacklength = fk.getcurcallstacklength(f); - int currid = rid; - while (currid == lastridex - && (curstacklength > laststacklength || (lastfile.equals(curfile) && lastline == curline))) - { - isend = resume(isend); - if (isend) - { - break; - } - curfile = fk.getcurfile(f); - curline = fk.getcurline(f); - curstacklength = fk.getcurcallstacklength(f); - currid = fk.getcurroutineid(f); - if (check_trigger_breakpoint(f, blist)) - { - break; - } - } - - rid = fk.getcurroutineid(f); - } - break; - case debug_step: - { - frame = 0; - rid = fk.getcurroutineid(f); - - if (firsttime) - { - if (check_trigger_breakpoint(f, blist)) - { - firsttime = false; - break; - } - } - firsttime = false; - - String lastfile = fk.getcurfile(f); - int lastline = fk.getcurline(f); - int lastridex = rid; - String curfile = fk.getcurfile(f); - int curline = fk.getcurline(f); - int currid = rid; - while (currid == lastridex && (lastfile.equals(curfile) && lastline == curline)) - { - isend = resume(isend); - if (isend) - { - break; - } - curfile = fk.getcurfile(f); - curline = fk.getcurline(f); - currid = fk.getcurroutineid(f); - if (check_trigger_breakpoint(f, blist)) - { - break; - } - } - - rid = fk.getcurroutineid(f); - } - break; - case debug_next_instruction: - { - frame = 0; - rid = fk.getcurroutineid(f); - - if (firsttime) - { - if (check_trigger_breakpoint(f, blist)) - { - firsttime = false; - break; - } - } - firsttime = false; - - int laststacklength = fk.getcurcallstacklength(f); - int lastridex = rid; - int curstacklength = fk.getcurcallstacklength(f); - int currid = rid; - do - { - isend = resume(isend); - if (isend) - { - break; - } - curstacklength = fk.getcurcallstacklength(f); - currid = fk.getcurroutineid(f); - if (check_trigger_breakpoint(f, blist)) - { - break; - } - } - while (currid == lastridex && curstacklength > laststacklength); - - rid = fk.getcurroutineid(f); - } - break; - case debug_step_instruction: - { - frame = 0; - rid = fk.getcurroutineid(f); - - if (firsttime) - { - if (check_trigger_breakpoint(f, blist)) - { - firsttime = false; - break; - } - } - firsttime = false; - - isend = resume(isend); - - rid = fk.getcurroutineid(f); - if (check_trigger_breakpoint(f, blist)) - { - break; - } - } - break; - case debug_continue: - { - frame = 0; - rid = fk.getcurroutineid(f); - - if (firsttime) - { - if (check_trigger_breakpoint(f, blist)) - { - firsttime = false; - break; - } - } - firsttime = false; - - while (true) - { - isend = resume(isend); - rid = fk.getcurroutineid(f); - if (check_trigger_breakpoint(f, blist)) - { - break; - } - if (isend) - { - break; - } - } - } - break; - case debug_breakpoint: - { - breakpoint tmp = new breakpoint(); - if (paramvec.isEmpty()) - { - tmp.file = fk.getcurfilebyroutinebyframe(f, rid, frame); - tmp.line = fk.getcurlinebyroutinebyframe(f, rid, frame); - } - else - { - String str = paramvec.get(0); - int subpos = str.indexOf(':'); - if (subpos != -1) - { - String filestr = str.substring(0, subpos); - String linestr = str.substring(subpos + 1); - - tmp.file = filestr; - tmp.line = Integer.parseInt(linestr); - } - else - { - boolean isnumber = true; - for (int i = 0; i < (int) str.length(); i++) - { - if (!Character.isDigit(str.charAt(i))) - { - isnumber = false; - break; - } - } - - if (isnumber) - { - tmp.file = fk.getcurfilebyroutinebyframe(f, rid, frame); - tmp.line = Integer.parseInt(str); - } - else - { - if (!fk.isfunc(f, str)) - { - System.out.printf("%s is not func\n", str); - continue; - } - - tmp.file = fk.getfuncfile(f, str); - tmp.line = fk.getfuncstartline(f, str); - } - } - } - tmp.enable = true; - tmp.id = bindex; - - if ((int) tmp.file.lastIndexOf('/') != -1) - { - tmp.file = tmp.file.substring(tmp.file.lastIndexOf('/') + 1); - } - - blist.add(tmp); - bindex++; - System.out.printf("Breakpoint %d at file %s, line %d total %d\n", tmp.id, tmp.file, tmp.line, - (int) blist.size()); - } - break; - case debug_enable: - { - if (paramvec.isEmpty()) - { - for (int i = 0; i < (int) blist.size(); i++) - { - breakpoint tmp = blist.get(i); - tmp.enable = true; - } - } - else - { - int id = Integer.parseInt(paramvec.get(0)); - for (int i = 0; i < (int) blist.size(); i++) - { - breakpoint tmp = blist.get(i); - if (tmp.id == id) - { - tmp.enable = true; - } - } - } - } - break; - case debug_disable: - { - if (paramvec.isEmpty()) - { - for (int i = 0; i < (int) blist.size(); i++) - { - breakpoint tmp = blist.get(i); - tmp.enable = false; - } - } - else - { - int id = Integer.parseInt(paramvec.get(0)); - for (int i = 0; i < (int) blist.size(); i++) - { - breakpoint tmp = blist.get(i); - if (tmp.id == id) - { - tmp.enable = false; - } - } - } - } - break; - case debug_delete: - { - if (paramvec.isEmpty()) - { - blist.clear(); - watchvec.clear(); - continue; - } - else - { - int id = Integer.parseInt(paramvec.get(0)); - for (int i = 0; i < (int) blist.size(); i++) - { - breakpoint tmp = blist.get(i); - if (tmp.id == id) - { - blist.remove(i); - } - } - } - } - break; - case debug_info: - { - if (paramvec.isEmpty()) - { - System.out.printf("need arg, useage: i b, i r\n"); - continue; - } - - if (paramvec.get(0).equals("b")) - { - System.out.printf("Id\tEnb\twhere\n"); - for (int i = 0; i < (int) blist.size(); i++) - { - breakpoint tmp = blist.get(i); - System.out.printf("%d\t%s\tfile %s, line %d\n", tmp.id, tmp.enable ? "y" : "n", tmp.file, - tmp.line); - } - } - else if (paramvec.get(0).equals("r")) - { - for (int i = 0; i < (int) fk.getcurroutinenum(f); i++) - { - System.out.printf("%s%s%s\n", fk.getroutineidbyindex(f, i) == rid ? "*" : "", - fk.getroutineidbyindex(f, i) == fk.getcurroutineid(f) ? "->" : "", - fk.getcurroutinebyindex(f, i)); - } - } - - System.out.printf("\n"); - } - break; - case debug_finish: - { - frame = 0; - rid = fk.getcurroutineid(f); - - int laststacklength = fk.getcurcallstacklength(f); - int curstacklength = fk.getcurcallstacklength(f); - do - { - isend = resume(isend); - if (isend) - { - break; - } - curstacklength = fk.getcurcallstacklength(f); - } - while (curstacklength >= laststacklength); - } - break; - case debug_list: - { - int listrange = 3; - if (!paramvec.isEmpty()) - { - listrange = Integer.parseInt(paramvec.get(0)); - } - show_debug_code(f, rid, frame, listrange); - isgoto = true; - } - break; - case debug_print: - { - if (paramvec.isEmpty()) - { - System.out.printf("need arg, useage: p variant\n"); - continue; - } - - String name = paramvec.get(0); - System.out.printf("%s\n", fk.getcurvaiantbyroutinebyframe(f, rid, frame, name, -1)); - } - break; - case debug_set: - { - if (paramvec.size() < 2) - { - System.out.printf("need arg, useage: set variant value\n"); - continue; - } - - String name = paramvec.get(0); - String value = paramvec.get(1); - fk.setcurvaiantbyroutinebyframe(f, rid, frame, name, value, -1); - System.out.printf("%s\n", fk.getcurvaiantbyroutinebyframe(f, rid, frame, name, -1)); - } - break; - case debug_watch: - { - if (paramvec.isEmpty()) - { - System.out.printf("need arg, useage: wa variant\n"); - continue; - } - - String name = paramvec.get(0); - watchvec.add(name); - } - break; - case debug_backtrace: - { - int length = fk.getcurcallstacklengthbyroutine(f, rid); - for (int i = 0; i < length; i++) - { - System.out.printf("%s%s\n", i == frame ? "*" : " ", - fk.getcurcallstackbyroutinebyframe(f, rid, i)); - } - isgoto = true; - } - break; - case debug_frame: - { - if (paramvec.isEmpty()) - { - frame = 0; - } - else - { - int theframe = Integer.parseInt(paramvec.get(0)); - if (theframe < 0 || theframe >= fk.getcurcallstacklengthbyroutine(f, rid)) - { - System.out.printf("%d is invalid\n", theframe); - } - frame = theframe; - } - } - break; - case debug_disa: - { - int pos = fk.getcurbytecodeposbyroutine(f, rid); - String func = fk.getcurfuncbyroutinebyframe(f, rid, frame); - System.out.printf("%s\n", fk.dumpfunc(f, func, pos)); - } - break; - case debug_routine: - { - if (paramvec.isEmpty()) - { - System.out.printf("need arg, useage: r rid\n"); - continue; - } - - int id = Integer.parseInt(paramvec.get(0)); - if (!fk.ishaveroutine(f, id)) - { - System.out.printf("no routine %d\n", id); - continue; - } - - rid = id; - } - break; - default: - continue; - } - if (isend) - { - break; - } - } - System.out.printf("end\n"); - - // ret - { - fk.psclear(f); - variant ret = f.ps.push_and_get(); - ret.copy_from(m_ret); - } - } - - boolean resume(boolean isend) throws Exception - { - fake f = m_f; - fk.psclear(f); - isend = fk.resumeps(f, isend); - variant ret = f.ps.pop_and_get(); - m_ret.copy_from(ret); - return isend; - } - - void show_debug_code(fake f, int rid, int frame, int range) - { - int curline = fk.getcurlinebyroutinebyframe(f, rid, frame); - for (int i = curline - range; i <= curline + range; i++) - { - if (i > 0) - { - String code = fk.getfilecode(f, fk.getcurfilebyroutinebyframe(f, rid, frame), i); - if (!code.isEmpty()) - { - System.out.printf("%s%d\t%s\n", curline == i ? "*" : "", i, code); - } - } - } - System.out.printf("\n"); - } - - void show_watch_variant(fake f, int rid, int frame, ArrayList watchvec) - { - for (int i = 0; i < (int) watchvec.size(); i++) - { - System.out.printf("%s\n", fk.getcurvaiantbyroutinebyframe(f, rid, frame, watchvec.get(i), -1)); - } - } - - void check_show_func_header(fake f, int rid, int frame, warper lastrid, warper lastfunc) - { - String curfunc = fk.getcurfuncbyroutinebyframe(f, rid, frame); - if (rid != (int) (Integer) lastrid.d) - { - System.out.printf("routine %s\n", fk.getcurroutinebyid(f, rid)); - lastrid.d = rid; - } - if (!curfunc.equals((String) lastfunc.d)) - { - System.out.printf("file %s, line %d, func %s\n", fk.getcurfilebyroutinebyframe(f, rid, frame), - fk.getcurlinebyroutinebyframe(f, rid, frame), curfunc); - lastfunc.d = curfunc; - } - } - - boolean check_trigger_breakpoint(fake f, ArrayList blist) - { - String curfile = fk.getcurfile(f); - if ((int) curfile.lastIndexOf('/') != -1) - { - curfile = curfile.substring(curfile.lastIndexOf('/') + 1); - } - int line = fk.getcurline(f); - for (int i = 0; i < (int) blist.size(); i++) - { - breakpoint tmp = blist.get(i); - if (tmp.enable && tmp.line == line && tmp.file.equals(curfile)) - { - System.out.printf("Trigger Breakpoint %d at file %s, line %d\n", tmp.id, tmp.file, tmp.line); - return true; - } - } - - return false; - } - - void show_debug_help(fake f) - { - System.out.printf("h help\n" + "n\tnext\n" + "s\tstep\n" + "ni\tnext bytecode\n" + "si\tstep bytecode\n" - + "c\tcontinue\n" + "l\tlist\n" + "p\tprint\n" + "set\tset\n" + "wa\twatch\n" + "b\tbreakpoint\n" - + "en\tenable\n" + "dis\tdisable\n" + "d\tdelete\n" + "i\tinfo\n" + "bt\tbacktrace\n" + "f\tframe\n" - + "fin\tfinish\n" + "r\troutine\n" + "disa\tview bytecode\n"); - } - - int get_debug_command(fake f, int command, ArrayList paramvec) - { - while (true) - { - System.out.printf("(fake) "); - - String s = ""; - try - { - DataInputStream in = new DataInputStream(new BufferedInputStream(System.in)); - s = in.readLine().trim(); - } - catch (Exception e) - { - } - - if (s.isEmpty()) - { - if (command == -1) - { - continue; - } - return command; - } - - paramvec.clear(); - String[] pv = s.split(" "); - for (String p : pv) - { - paramvec.add(p); - } - - if (paramvec.isEmpty()) - { - return command; - } - - String strcommand = paramvec.get(0); - paramvec.remove(0); - - if (strcommand.equals("n")) - { - command = debug_next; - } - else if (strcommand.equals("s")) - { - command = debug_step; - } - else if (strcommand.equals("ni")) - { - command = debug_next_instruction; - } - else if (strcommand.equals("si")) - { - command = debug_step_instruction; - } - else if (strcommand.equals("c")) - { - command = debug_continue; - } - else if (strcommand.equals("b")) - { - command = debug_breakpoint; - } - else if (strcommand.equals("dis")) - { - command = debug_disable; - } - else if (strcommand.equals("en")) - { - command = debug_enable; - } - else if (strcommand.equals("d")) - { - command = debug_delete; - } - else if (strcommand.equals("i")) - { - command = debug_info; - } - else if (strcommand.equals("fin")) - { - command = debug_finish; - } - else if (strcommand.equals("l")) - { - command = debug_list; - } - else if (strcommand.equals("wa")) - { - command = debug_watch; - } - else if (strcommand.equals("p")) - { - command = debug_print; - } - else if (strcommand.equals("set")) - { - command = debug_set; - } - else if (strcommand.equals("bt")) - { - command = debug_backtrace; - } - else if (strcommand.equals("f")) - { - command = debug_frame; - } - else if (strcommand.equals("disa")) - { - command = debug_disa; - } - else if (strcommand.equals("r")) - { - command = debug_routine; - } - else if (strcommand.equals("h")) - { - show_debug_help(f); - continue; - } - else - { - System.out.printf("use h to get help\n"); - continue; - } - - return command; - } - } - -} diff --git a/src/main/java/com/github/esrrhs/fakescript/fake.java b/src/main/java/com/github/esrrhs/fakescript/fake.java deleted file mode 100644 index 6844f79..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/fake.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.github.esrrhs.fakescript; - -public class fake -{ - protected boolean error = false; - protected String errorstr = ""; - protected callback cb = null; - - // 配置 - protected fkconfig cfg = new fkconfig(); - - // 解析 - protected parser pa = new parser(this); - - // 参数栈 - protected paramstack ps = new paramstack(this); - - // 二进制 - protected binary bin = new binary(this); - - // 函数索引 - protected funcmap fm = new funcmap(this); - - // 性能检测 - protected profile pf = new profile(this); - - // 内建的函数集合 - protected buildinfunc bif = new buildinfunc(this); - - // 当前运行状态 - protected running rn = new running(this); - - // debug容器 - protected debuging dbg = new debuging(this); - - // 优化 - protected optimizer opt = new optimizer(this); - - protected fake clonef() - { - fake nf = new fake(); - - nf.cfg = this.cfg; - nf.cb = this.cb; - nf.pa = this.pa.clonef(this); - nf.ps = new paramstack(this); - nf.bin = new binary(this); - nf.fm = this.fm.clonef(this); - nf.pf = new profile(this); - nf.bif = new buildinfunc(this); - nf.rn = new running(this); - nf.dbg = new debuging(this); - nf.opt = new optimizer(this); - - return nf; - } - - public void clearerr() - { - error = false; - errorstr = ""; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/fakescript.java b/src/main/java/com/github/esrrhs/fakescript/fakescript.java deleted file mode 100644 index dbcd67e..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/fakescript.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.github.esrrhs.fakescript; - -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -@Retention (RUNTIME) -@Target (METHOD) -public @interface fakescript -{ - String name() default ""; -} diff --git a/src/main/java/com/github/esrrhs/fakescript/fk.java b/src/main/java/com/github/esrrhs/fakescript/fk.java deleted file mode 100644 index 0c28328..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/fk.java +++ /dev/null @@ -1,1436 +0,0 @@ -package com.github.esrrhs.fakescript; - -import java.io.*; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -public class fk -{ - /** - * 版本号 - */ - public static final String version = "1.0.3"; - - // 节省内存 - protected static final HashMap regName = new HashMap(); - protected static final HashMap regFunctor = new HashMap(); - protected static final HashMap regBindFunc = new HashMap(); - - /** - * 创建fake对象 - *

- * fake为上下文环境
- * 所有接口在fake中执行 - * - * @param config - * 具体的参数 - * @return fake对象 - */ - public static fake newfake(fkconfig config) - { - fake f = new fake(); - if (config != null) - { - f.cfg = config; - } - return f; - } - - /** - * 复制fake对象 - *

- * fake为上下文环境
- * 所有接口在fake中执行 - * - * @param f - * fake对象 - * @return fake对象 - */ - public static fake clone(fake f) - { - return f.clonef(); - } - - /** - * 上一次执行是否失败 - *

- * fake为上下文环境
- * 所有接口在fake中执行 - * - * @param f - * fake对象 - * @return 是否失败 - */ - public static boolean error(fake f) - { - return f.error; - } - - /** - * 绑定java函数 - *

- * 遍历package下所有类
- * 绑定标记fakescript的函数 - * - * @param f - * 上下文环境 - * - * @param packagename - * 包的名字 - * - */ - public static void reg(fake f, String packagename) - { - List> tmp = packagehelper.getClasses(f, packagename); - for (Class c : tmp) - { - Method[] ms = c.getMethods(); - for (Method m : ms) - { - if (m.isAnnotationPresent(fakescript.class)) - { - fakescript fn = (fakescript) m.getAnnotation(fakescript.class); - - String name = fn.name(); - if (name.equals("")) - { - name = m.getName(); - } - - reg_method(f, name, c, m); - } - } - } - - } - - /** - * 绑定java函数 - *

- * 遍历package下所有类
- * 绑定所有的函数 - * - * @param f - * 上下文环境 - * - * @param packagename - * 包的名字 - * - */ - public static void regall(fake f, String packagename) - { - List> tmp = packagehelper.getClasses(f, packagename); - for (Class c : tmp) - { - regclass(f, c); - } - } - - /** - * 绑定java函数 - *

- * 遍历类下所有函数
- * 绑定类所有的函数 - * - * @param f - * 上下文环境 - * - * @param c - * class - * - */ - public static void regclass(fake f, Class c) - { - Class[] cs = c.getDeclaredClasses(); - for (Class cc : cs) - { - regclass(f, cc); - } - - Method[] ms = c.getMethods(); - for (Method m : ms) - { - String name = m.getName(); - if (m.isAnnotationPresent(fakescript.class)) - { - fakescript fn = (fakescript) m.getAnnotation(fakescript.class); - if (fn != null && !fn.name().isEmpty()) - { - name = fn.name(); - } - } - - reg_method(f, name, c, m); - } - } - - /** - * 设置回调函数 - *

- * 如错误处理
- * 打印函数 - * - * @param f - * 上下文环境 - * - * @param cb - * 回调类 - * - */ - public static void set_callback(fake f, callback cb) - { - f.cb = cb; - } - - /** - * 解析文件 - *

- * 解析脚本
- * 编译成字节码 - * - * @param f - * 上下文环境 - * - * @param filename - * 文件名 - * - * @return 无 - */ - public static boolean parse(fake f, String filename) - { - f.pa.clear(); - return f.pa.parse(filename); - } - - /** - * 解析代码 - *

- * 解析文本字符串代码
- * 编译成字节码 - * - * @param f - * 上下文环境 - * - * @param str - * 文件名 - * - * @return 无 - */ - public static boolean parsestr(fake f, String str) - { - f.pa.clear(); - return f.pa.parsestr(str); - } - - /** - * 执行脚本 - *

- * 执行指定脚本函数
- * 结果通过Object返回,注意内部数值都是用double,所以转换时需要注意下 - * - * @param f - * 上下文环境 - * - * @param func - * 函数名 - * - * @param args - * 参数 - * - * @return 无 - */ - public static Object run(fake f, String func, Object... args) - { - psclear(f); - for (Object arg : args) - { - pspush(f, arg); - } - runps(f, func); - return pspop(f); - } - - public static Object debugrun(fake f, String func, Object... args) - { - psclear(f); - for (Object arg : args) - { - pspush(f, arg); - } - openstepmod(f); - rundebugps(f, func); - closestepmod(f); - return pspop(f); - } - - public static void openstepmod(fake f) - { - f.rn.set_stepmod(true); - } - - public static void closestepmod(fake f) - { - f.rn.set_stepmod(false); - } - - /** - * 获取当前文件 - *

- * - * @param f - * 上下文环境 - * - * @return 无 - */ - public static String getcurfile(fake f) - { - processor p = f.rn.cur_pro(); - if (p != null && p.get_curroutine() != null) - { - return p.get_curroutine().get_interpreter().get_running_file_name(); - } - return "nil"; - } - - /** - * 获取当前文件行号 - *

- * - * @param f - * 上下文环境 - * - * @return 无 - */ - public static int getcurline(fake f) - { - processor p = f.rn.cur_pro(); - if (p != null && p.get_curroutine() != null) - { - return p.get_curroutine().get_interpreter().get_running_file_line(); - } - return 0; - } - - /** - * 获取当前函数 - *

- * - * @param f - * 上下文环境 - * - * @return 无 - */ - public static String getcurfunc(fake f) - { - processor p = f.rn.cur_pro(); - if (p != null && p.get_curroutine() != null) - { - return p.get_curroutine().get_interpreter().get_running_func_name(); - } - return "nil"; - } - - /** - * 获取当前调用堆栈 - *

- * - * @param f - * 上下文环境 - * - * @return 无 - */ - public static String getcurcallstack(fake f) - { - processor p = f.rn.cur_pro(); - if (p != null && p.get_curroutine() != null) - { - return p.get_curroutine().get_interpreter().get_running_call_stack(); - } - return "nil"; - } - - /** - * 获取当前协程信息 - *

- * - * @param f - * 上下文环境 - * - * @return 无 - */ - public static String getcurroutine(fake f) - { - processor p = f.rn.cur_pro(); - if (p != null && p.get_curroutine() != null) - { - return p.get_routine_info(); - } - return "nil"; - } - - public static int getcurroutineid(fake f) - { - processor p = f.rn.cur_pro(); - if (p != null && p.get_curroutine() != null) - { - return p.get_curroutine().get_id(); - } - return 0; - } - - public static String getcurvaiantbyroutinebyframe(fake f, int rid, int frame, String name, int line) - { - processor p = f.rn.cur_pro(); - if (p != null && p.get_routine_by_id(rid) != null) - { - warper ret = new warper(new String()); - warper retline = new warper(new Integer(0)); - p.get_routine_by_id(rid).get_interpreter().get_running_vaiant(frame, name, line, ret, retline); - return (String) ret.d; - } - return ""; - } - - public static void setcurvaiantbyroutinebyframe(fake f, int rid, int frame, String name, String value, int line) - { - processor p = f.rn.cur_pro(); - if (p != null && p.get_routine_by_id(rid) != null) - { - p.get_routine_by_id(rid).get_interpreter().set_running_vaiant(frame, name, line, value); - } - } - - public static String getcurfuncbyroutinebyframe(fake f, int rid, int frame) - { - processor p = f.rn.cur_pro(); - if (p != null && p.get_routine_by_id(rid) != null) - { - warper stackinfo = new warper(new String()); - warper func = new warper(new String()); - warper file = new warper(new String()); - warper line = new warper(new Integer(0)); - p.get_routine_by_id(rid).get_interpreter().get_running_call_stack_frame_info(frame, stackinfo, func, file, - line); - return (String) func.d; - } - return "nil"; - } - - public static String getcurroutinebyid(fake f, int rid) - { - processor p = f.rn.cur_pro(); - if (p != null) - { - return p.get_routine_info_by_id(rid); - } - return ""; - } - - public static int getcurcallstacklength(fake f) - { - processor p = f.rn.cur_pro(); - if (p != null && p.get_curroutine() != null) - { - return p.get_curroutine().get_interpreter().get_running_call_stack_length(); - } - return 0; - } - - public static String getcurfilebyroutinebyframe(fake f, int rid, int frame) - { - processor p = f.rn.cur_pro(); - if (p != null && p.get_routine_by_id(rid) != null) - { - warper stackinfo = new warper(new String()); - warper func = new warper(new String()); - warper file = new warper(new String()); - warper line = new warper(new Integer(0)); - p.get_routine_by_id(rid).get_interpreter().get_running_call_stack_frame_info(frame, stackinfo, func, file, - line); - return (String) file.d; - } - return "nil"; - } - - public static int getcurlinebyroutinebyframe(fake f, int rid, int frame) - { - processor p = f.rn.cur_pro(); - if (p != null && p.get_routine_by_id(rid) != null) - { - warper stackinfo = new warper(new String()); - warper func = new warper(new String()); - warper file = new warper(new String()); - warper line = new warper(new Integer(0)); - p.get_routine_by_id(rid).get_interpreter().get_running_call_stack_frame_info(frame, stackinfo, func, file, - line); - return (int) (Integer) line.d; - } - return 0; - } - - public static String getcurcallstackbyroutinebyframe(fake f, int rid, int frame) - { - processor p = f.rn.cur_pro(); - if (p != null && p.get_routine_by_id(rid) != null) - { - warper stackinfo = new warper(new String()); - warper func = new warper(new String()); - warper file = new warper(new String()); - warper line = new warper(new Integer(0)); - p.get_routine_by_id(rid).get_interpreter().get_running_call_stack_frame_info(frame, stackinfo, func, file, - line); - return (String) stackinfo.d; - } - return "nil"; - } - - public static String getfuncfile(fake f, String func) - { - variant funcv = new variant(); - funcv.set_string(func); - funcunion ff = f.fm.get_func(funcv); - if (ff != null && ff.m_havefb) - { - return ff.m_fb.m_filename; - } - return ""; - } - - public static int getfuncstartline(fake f, String func) - { - variant funcv = new variant(); - funcv.set_string(func); - funcunion ff = f.fm.get_func(funcv); - if (ff != null && ff.m_havefb) - { - return ff.m_fb.get_binary_lineno(0); - } - return 0; - } - - public static int getcurroutinenum(fake f) - { - processor p = f.rn.cur_pro(); - if (p != null) - { - return p.get_routine_num(); - } - return 0; - } - - public static int getroutineidbyindex(fake f, int index) - { - processor p = f.rn.cur_pro(); - if (p != null && p.get_routine_by_index(index) != null) - { - return p.get_routine_by_index(index).get_id(); - } - return 0; - } - - public static String getcurroutinebyindex(fake f, int index) - { - processor p = f.rn.cur_pro(); - if (p != null) - { - return p.get_routine_info_by_index(index); - } - return ""; - } - - public static int getcurcallstacklengthbyroutine(fake f, int rid) - { - processor p = f.rn.cur_pro(); - if (p != null && p.get_routine_by_id(rid) != null) - { - return p.get_routine_by_id(rid).get_interpreter().get_running_call_stack_length(); - } - return 0; - } - - public static int getcurbytecodeposbyroutine(fake f, int rid) - { - processor p = f.rn.cur_pro(); - if (p != null && p.get_routine_by_id(rid) != null) - { - return p.get_routine_by_id(rid).get_interpreter().get_running_bytecode_pos(); - } - return -1; - } - - public static String dumpfunc(fake f, String func, int pos) - { - return f.bin.dump(func, pos); - } - - public static boolean ishaveroutine(fake f, int rid) - { - processor p = f.rn.cur_pro(); - if (p != null) - { - return p.get_routine_by_id(rid) != null; - } - return false; - } - - public static String getfilecode(fake f, String filename, int line) - { - if (filename.isEmpty() || line <= 0) - { - return ""; - } - - try - { - String encoding = "utf-8"; - Reader reader = new InputStreamReader(new FileInputStream(filename), encoding); - BufferedReader bufferedReader = new BufferedReader(reader); - - int i = 0; - String ret = ""; - while (true) - { - String str = bufferedReader.readLine(); - if (str == null) - { - break; - } - - i++; - if (i >= line) - { - ret = str; - break; - } - } - return ret; - } - catch (Exception e) - { - return e.getMessage(); - } - } - - /** - * 打开基本的内置函数 - *

- * - * @param f - * 上下文环境 - * - */ - public static void openbaselib(fake f) - { - f.bif.openbasefunc(); - } - - /** - * 是否有某个函数 - *

- * 注意类的非静态成员函数在绑定的时候会在前面加上类名
- * 如test.A类的aaa函数,他的实际函数名是test.Aaaa - * - * @param f - * 上下文环境 - * - * @param name - * 函数名 - * - * @return 无 - */ - public static boolean isfunc(fake f, String name) - { - variant funcv = new variant(); - funcv.set_string(name); - return f.fm.get_func(funcv) != null; - } - - /** - * 打开性能监控 - *

- * - * @param f - * 上下文环境 - * - */ - public static void openprofile(fake f) - { - f.pf.open(); - } - - /** - * 关闭性能监控 - *

- * - * @param f - * 上下文环境 - * - */ - public static void closeprofile(fake f) - { - f.pf.close(); - } - - /** - * 打开优化 - *

- * - * @param f - * 上下文环境 - * - */ - public static void openoptimize(fake f) - { - f.opt.open(); - } - - /** - * 关闭优化 - *

- * - * @param f - * 上下文环境 - * - */ - public static void closeoptimize(fake f) - { - f.opt.close(); - } - - /** - * 打印性能监控数据 - *

- * - * @param f - * 上下文环境 - * - * @return dump - */ - public static String dumpprofile(fake f) - { - return f.pf.dump(); - } - - protected static void psclear(fake f) - { - f.ps.clear(); - } - - protected static void pspush(fake f, Object arg) - { - variant v = f.ps.push_and_get(); - - if (arg == null) - { - v.set_pointer(null); - return; - } - - Class c = arg.getClass(); - if (c == Byte.class) - { - Byte b = (Byte) arg; - v.set_real(b); - } - else if (c == Short.class) - { - Short b = (Short) arg; - v.set_real(b); - } - else if (c == Integer.class) - { - Integer b = (Integer) arg; - v.set_real(b); - } - else if (c == Long.class) - { - long b = (long) (Long) arg; - v.set_uuid(b); - } - else if (c == Float.class) - { - Float b = (Float) arg; - v.set_real(b); - } - else if (c == Double.class) - { - Double b = (Double) arg; - v.set_real(b); - } - else if (c == Boolean.class) - { - Boolean b = (Boolean) arg; - v.set_real(b ? 1 : 0); - } - else if (c == String.class) - { - String b = (String) arg; - v.set_string(b); - } - else - { - v.set_pointer(arg); - } - } - - protected static Object psget(fake f, int i) - { - if (f.ps.size() == 0) - { - return null; - } - - variant v = f.ps.get(i); - if (v.m_type == variant_type.NIL) - { - return null; - } - else if (v.m_type == variant_type.REAL) - { - double b = (double) (Double) v.m_data; - return b; - } - else if (v.m_type == variant_type.STRING) - { - String b = (String) v.m_data; - return b; - } - else if (v.m_type == variant_type.POINTER) - { - return v.m_data; - } - else if (v.m_type == variant_type.UUID) - { - long b = (long) (Long) v.m_data; - return b; - } - else - { - return null; - } - } - - protected static Object pspop(fake f) - { - if (f.ps.size() == 0) - { - return null; - } - - variant v = f.ps.pop_and_get(); - if (v.m_type == variant_type.NIL) - { - return null; - } - else if (v.m_type == variant_type.REAL) - { - double b = (double) (Double) v.m_data; - return b; - } - else if (v.m_type == variant_type.STRING) - { - String b = (String) v.m_data; - return b; - } - else if (v.m_type == variant_type.POINTER) - { - return v.m_data; - } - else if (v.m_type == variant_type.UUID) - { - long b = (long) (Long) v.m_data; - return b; - } - else - { - return null; - } - } - - protected static Object trans(Object src, Class c) - { - if (src == null) - { - return null; - } - - Class srcc = src.getClass(); - - if (c == Byte.class || c == Byte.TYPE) - { - if (srcc == Byte.class) - { - return (byte) (byte) (Byte) src; - } - else if (srcc == Short.class) - { - return (byte) (short) (Short) src; - } - else if (srcc == Integer.class) - { - return (byte) (int) (Integer) src; - } - else if (srcc == Long.class) - { - return (byte) (long) (Long) src; - } - else if (srcc == Float.class) - { - return (byte) (float) (Float) src; - } - else if (srcc == Double.class) - { - return (byte) (double) (Double) src; - } - else if (srcc == Boolean.class) - { - return (boolean) (Boolean) src ? (byte) 1 : (byte) 0; - } - else if (srcc == String.class) - { - return Byte.valueOf((String) src); - } - else - { - return (byte) 0; - } - } - else if (c == Short.class || c == Short.TYPE) - { - if (srcc == Byte.class) - { - return (short) (byte) (Byte) src; - } - else if (srcc == Short.class) - { - return (short) (short) (Short) src; - } - else if (srcc == Integer.class) - { - return (short) (int) (Integer) src; - } - else if (srcc == Long.class) - { - return (short) (long) (Long) src; - } - else if (srcc == Float.class) - { - return (short) (float) (Float) src; - } - else if (srcc == Double.class) - { - return (short) (double) (Double) src; - } - else if (srcc == Boolean.class) - { - return (boolean) (Boolean) src ? (short) 1 : (short) 0; - } - else if (srcc == String.class) - { - return Short.valueOf((String) src); - } - else - { - return (short) 0; - } - } - else if (c == Integer.class || c == Integer.TYPE) - { - if (srcc == Byte.class) - { - return (int) (byte) (Byte) src; - } - else if (srcc == Short.class) - { - return (int) (short) (Short) src; - } - else if (srcc == Integer.class) - { - return (int) (int) (Integer) src; - } - else if (srcc == Long.class) - { - return (int) (long) (Long) src; - } - else if (srcc == Float.class) - { - return (int) (float) (Float) src; - } - else if (srcc == Double.class) - { - return (int) (double) (Double) src; - } - else if (srcc == Boolean.class) - { - return (boolean) (Boolean) src ? (int) 1 : (int) 0; - } - else if (srcc == String.class) - { - return Integer.valueOf((String) src); - } - else - { - return (int) 0; - } - } - else if (c == Long.class || c == Long.TYPE) - { - if (srcc == Byte.class) - { - return (long) (byte) (Byte) src; - } - else if (srcc == Short.class) - { - return (long) (short) (Short) src; - } - else if (srcc == Integer.class) - { - return (long) (int) (Integer) src; - } - else if (srcc == Long.class) - { - return (long) (long) (Long) src; - } - else if (srcc == Float.class) - { - return (long) (float) (Float) src; - } - else if (srcc == Double.class) - { - return (long) (double) (Double) src; - } - else if (srcc == Boolean.class) - { - return (boolean) (Boolean) src ? (long) 1 : (long) 0; - } - else if (srcc == String.class) - { - return Long.valueOf((String) src); - } - else - { - return (long) 0; - } - } - else if (c == Float.class || c == Float.TYPE) - { - if (srcc == Byte.class) - { - return (float) (byte) (Byte) src; - } - else if (srcc == Short.class) - { - return (float) (short) (Short) src; - } - else if (srcc == Integer.class) - { - return (float) (int) (Integer) src; - } - else if (srcc == Long.class) - { - return (float) (long) (Long) src; - } - else if (srcc == Float.class) - { - return (float) (float) (Float) src; - } - else if (srcc == Double.class) - { - return (float) (double) (Double) src; - } - else if (srcc == Boolean.class) - { - return (boolean) (Boolean) src ? (float) 1 : (float) 0; - } - else if (srcc == String.class) - { - return Float.valueOf((String) src); - } - else - { - return (float) 0; - } - } - else if (c == Double.class || c == Double.TYPE) - { - if (srcc == Byte.class) - { - return (double) (byte) (Byte) src; - } - else if (srcc == Short.class) - { - return (double) (short) (Short) src; - } - else if (srcc == Integer.class) - { - return (double) (int) (Integer) src; - } - else if (srcc == Long.class) - { - return (double) (long) (Long) src; - } - else if (srcc == Float.class) - { - return (double) (float) (Float) src; - } - else if (srcc == Double.class) - { - return (double) (double) (Double) src; - } - else if (srcc == Boolean.class) - { - return (boolean) (Boolean) src ? (double) 1 : (double) 0; - } - else if (srcc == String.class) - { - return Double.valueOf((String) src); - } - else - { - return (double) 0; - } - } - else if (c == Boolean.class || c == Boolean.TYPE) - { - if (srcc == Byte.class) - { - return (byte) (Byte) src != 0; - } - else if (srcc == Short.class) - { - return (short) (Short) src != 0; - } - else if (srcc == Integer.class) - { - return (int) (Integer) src != 0; - } - else if (srcc == Long.class) - { - return (long) (Long) src != 0; - } - else if (srcc == Float.class) - { - return (float) (Float) src != 0; - } - else if (srcc == Double.class) - { - return (double) (Double) src != 0; - } - else if (srcc == Boolean.class) - { - return src; - } - else if (srcc == String.class) - { - return Integer.valueOf((String) src) != 0; - } - else - { - return false; - } - } - else if (c == String.class) - { - return String.valueOf(src); - } - // 这种一般是模板,直接转过去 - else if (c == Object.class) - { - return src; - } - // 这些就是特定的类型了 - else - { - if (srcc == Byte.class) - { - return null; - } - else if (srcc == Short.class) - { - return null; - } - else if (srcc == Integer.class) - { - return null; - } - else if (srcc == Long.class) - { - return null; - } - else if (srcc == Float.class) - { - return null; - } - else if (srcc == Double.class) - { - return null; - } - else if (srcc == Boolean.class) - { - return null; - } - else if (srcc == String.class) - { - return null; - } - else - { - return src; - } - } - } - - protected static boolean canTrans(Object src, Class c) - { - if (src == null) - { - return true; - } - - Class srcc = src.getClass(); - - if ((c == Byte.class || c == Byte.TYPE) || (c == Short.class || c == Short.TYPE) - || (c == Integer.class || c == Integer.TYPE) || (c == Long.class || c == Long.TYPE) - || (c == Float.class || c == Float.TYPE) || (c == Double.class || c == Double.TYPE) - || (c == Boolean.class || c == Boolean.TYPE)) - { - if (srcc == Byte.class) - { - return true; - } - else if (srcc == Short.class) - { - return true; - } - else if (srcc == Integer.class) - { - return true; - } - else if (srcc == Long.class) - { - return true; - } - else if (srcc == Float.class) - { - return true; - } - else if (srcc == Double.class) - { - return true; - } - else if (srcc == Boolean.class) - { - return true; - } - else if (srcc == String.class) - { - return false; - } - else - { - return false; - } - } - else if (c == String.class) - { - if (srcc == String.class) - { - return true; - } - else - { - return false; - } - } - // 这种一般是模板,直接转过去 - else if (c == Object.class) - { - return true; - } - // 这些就是特定的类型了 - else - { - return c.isInstance(src); - } - } - - private static void rundebugps(fake f, String func) - { - variant funcv = new variant(); - funcv.set_string(func); - - processor pro = new processor(f); - - try - { - routine r = pro.start_routine(funcv, new ArrayList()); - - f.rn.push_pro(pro); - - f.dbg.debug(); - } - catch (Exception e) - { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - types.seterror(f, getcurfile(f), getcurline(f), getcurfunc(f), e.toString() + "\n" + sw.toString()); - pw.close(); - f.ps.push_and_get(); - } - } - - private static void runps(fake f, String func) - { - variant funcv = new variant(); - funcv.set_string(func); - - f.clearerr(); - processor pro = new processor(f); - - try - { - routine r = pro.start_routine(funcv, new ArrayList()); - - f.rn.push_pro(pro); - pro.run(); - - variant ret = r.get_ret(); - - variant v = f.ps.push_and_get(); - v.copy_from(ret); - } - catch (Exception e) - { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - types.seterror(f, getcurfile(f), getcurline(f), getcurfunc(f), e.toString() + "\n" + sw.toString()); - pw.close(); - f.ps.push_and_get(); - } - finally - { - f.rn.pop_pro(); - } - } - - private static void reg_method(fake f, String name, Class c, Method m) - { - boolean isstatic = Modifier.isStatic(m.getModifiers()); - if (!isstatic) - { - name = c.getName() + name; - } - else - { - name = c.getSimpleName() + "." + name; - } - - synchronized (fk.class) - { - variant v = null; - fkfunctor fkf = null; - - if (regName.get(name) != null) - { - v = regName.get(name); - fkf = regFunctor.get(name); - - for (fkmethod fm : fkf.m_ms) - { - if (fm.m_param.length == m.getParameterTypes().length) - { - boolean equal = true; - for (int i = 0; i < fm.m_param.length; i++) - { - if (fm.m_param[i] != m.getParameterTypes()[i]) - { - equal = false; - break; - } - } - if (equal) - { - f.fm.add_func(v, fkf); - types.log(f, "fk reg %s %s from cache", name, fkf); - return; - } - } - } - } - else - { - v = new variant(); - v.set_string(name); - - fkf = new fkfunctor(); - fkf.m_c = c.getName(); - fkf.m_is_staic = isstatic; - - f.fm.add_func(v, fkf); - - regName.put(name, v); - regFunctor.put(name, fkf); - } - - fkmethod fm = new fkmethod(); - fm.m_m = m; - fm.m_param = m.getParameterTypes(); - fm.m_ret = m.getReturnType(); - - if (fkf.m_ms == null) - { - fkf.m_ms = new fkmethod[1]; - fkf.m_ms[0] = fm; - } - else - { - fkmethod[] newarray = new fkmethod[fkf.m_ms.length + 1]; - for (int i = 0; i < fkf.m_ms.length; i++) - { - newarray[i] = fkf.m_ms[i]; - } - newarray[fkf.m_ms.length] = fm; - fkf.m_ms = newarray; - } - - types.log(f, "fk reg %s %s", name, fkf); - } - } - - protected static boolean resumeps(fake f, boolean isend) throws Exception - { - isend = false; - - // 上次的processor - processor pro = f.rn.cur_pro(); - if (pro == null) - { - variant ret = f.ps.push_and_get(); - ret.set_nil(); - return isend; - } - - pro.run(); - if (pro.get_routine_num() != 0) - { - variant ret = f.ps.push_and_get(); - ret.set_nil(); - return isend; - } - - // 结束了 - variant ret = f.ps.push_and_get(); - ret.copy_from(pro.get_entrycurroutine().get_ret()); - - f.rn.pop_pro(); - - isend = true; - - return isend; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/esrrhs/fakescript/fkconfig.java b/src/main/java/com/github/esrrhs/fakescript/fkconfig.java deleted file mode 100644 index 724382b..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/fkconfig.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.esrrhs.fakescript; - -public class fkconfig -{ - public int per_frame_cmd_num = 10; // 每帧执行命令数目 - public int include_deps = 100; // 解析include最大深度 - public int stack_max = 10000; // stack最大尺寸 - public int open_debug_log = 0; // 打印内部调试信息 -} diff --git a/src/main/java/com/github/esrrhs/fakescript/fkfunctor.java b/src/main/java/com/github/esrrhs/fakescript/fkfunctor.java deleted file mode 100644 index f5c1b65..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/fkfunctor.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.github.esrrhs.fakescript; - -import java.lang.reflect.Method; - -class fkmethod -{ - Method m_m; - Class[] m_param; - Class m_ret; -} - -class fkfunctor -{ - String m_c; - boolean m_is_staic; - fkmethod[] m_ms; - - public void call(fake f) throws Exception - { - Object c = null; - if (!m_is_staic) - { - c = fk.pspop(f); - if (c == null) - { - throw new Exception("call bind class " + m_c.toString() + " " + m_ms[0].toString() + ", ref is null"); - } - // 检查类型 - if (!c.getClass().getName().equals(m_c)) - { - throw new Exception("call bind class " + m_c.toString() + " " + m_ms[0].toString() - + ", diff class type, give " + c.getClass().toString()); - } - } - - fkmethod dest = null; - for (fkmethod fm : m_ms) - { - if (f.ps.size() == fm.m_param.length) - { - boolean equal = true; - for (int i = 0; i < f.ps.size(); i++) - { - if (!fk.canTrans(fk.psget(f, i), fm.m_param[i])) - { - equal = false; - } - } - - if (equal) - { - dest = fm; - break; - } - } - } - - // 检查param数目对不对 - if (dest == null) - { - String tmp = ""; - for (int i = f.ps.size() - 1; i >= 0; i--) - { - tmp += fk.psget(f, i) != null ? fk.psget(f, i).getClass().getName() + " " : "null"; - } - throw new Exception( - "call bind class " + m_c.toString() + " " + m_ms[0].toString() + ", param not match, give " - + f.ps.size() + " need " + m_ms[0].m_param.length + ", give type " + tmp); - } - - // 参数 - Object[] param = new Object[dest.m_param.length]; - for (int i = dest.m_param.length - 1; i >= 0; i--) - { - param[i] = fk.trans(fk.pspop(f), dest.m_param[i]); - } - - Object ret = dest.m_m.invoke(c, param); - - // 检查类型 - if (ret != null && ret.getClass().isInstance(dest.m_ret)) - { - throw new Exception("call bind class " + m_c.toString() + " " + dest.m_m.toString() - + ", diff ret type, give " + ret.getClass().toString()); - } - - fk.pspush(f, ret); - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/func_binary.java b/src/main/java/com/github/esrrhs/fakescript/func_binary.java deleted file mode 100644 index ce86a80..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/func_binary.java +++ /dev/null @@ -1,207 +0,0 @@ -package com.github.esrrhs.fakescript; - -class func_binary -{ - // 最大栈空间 - public int m_maxstack; - // 参数个数 - public int m_paramnum; - // 名字 - public String m_name; - // 文件名 - public String m_filename; - // 包名 - public String m_packagename; - // 二进制缓冲区 - public long[] m_buff; - // 二进制行号缓冲区 - public int[] m_lineno_buff; - public int m_end_lineno; - // 常量 - public variant[] m_const_list; - // container地址 - public container_addr[] m_container_addr_list; - // 调试信息,栈变量 - public stack_variant_info[] m_debug_stack_variant_info; - // 占用标记 - public int m_use; - // 备份 - public func_binary m_backup; - // 新标记 - public int m_fresh; - - public func_binary clonef() - { - func_binary fb = new func_binary(); - - fb.m_maxstack = this.m_maxstack; - fb.m_paramnum = this.m_paramnum; - fb.m_name = this.m_name; - fb.m_filename = this.m_filename; - fb.m_packagename = this.m_packagename; - fb.m_buff = this.m_buff; - fb.m_lineno_buff = this.m_lineno_buff; - fb.m_end_lineno = this.m_end_lineno; - fb.m_const_list = this.m_const_list; - fb.m_container_addr_list = this.m_container_addr_list; - fb.m_debug_stack_variant_info = this.m_debug_stack_variant_info; - return fb; - } - - public String dump(int pos) - { - String ret = ""; - - // 名字 - ret += "\n["; - ret += m_name; - ret += "]\n"; - - // 最大栈 - ret += "\tmaxstack:\t"; - ret += m_maxstack; - ret += "\n\n"; - - // 常量表 - ret += "\t////// const define "; - ret += m_const_list.length; - ret += " //////\n"; - for (int i = 0; i < (int) m_const_list.length; i++) - { - ret += "\t["; - ret += i; - ret += "]\t"; - ret += m_const_list[i].m_type; - ret += "\t"; - ret += m_const_list[i].toString(); - ret += "\n"; - } - - // 容器地址表 - ret += "\n\t////// container addr "; - ret += m_container_addr_list.length; - ret += " //////\n"; - for (int i = 0; i < (int) m_container_addr_list.length; i++) - { - ret += "\t["; - ret += i; - ret += "]\t"; - long concmd = m_container_addr_list[i].m_con; - int concode = command.COMMAND_CODE(concmd); - ret += "[ CONTAINER ]\t"; - ret += types.dump_addr(concode); - long keycmd = m_container_addr_list[i].m_key; - int keycode = command.COMMAND_CODE(keycmd); - ret += "\t[ KEY ]\t"; - ret += types.dump_addr(keycode); - ret += "\n"; - } - - // 变量地址 - ret += "\n\t////// stack variant addr "; - ret += m_debug_stack_variant_info.length; - ret += " //////\n"; - for (int i = 0; i < (int) m_debug_stack_variant_info.length; i++) - { - stack_variant_info info = m_debug_stack_variant_info[i]; - ret += "\t["; - ret += info.m_pos; - ret += "]\t"; - ret += info.m_name; - ret += "\t\tLINE\t"; - ret += info.m_line; - ret += "\n"; - } - - ret += "\n\t////// byte code "; - ret += m_buff.length; - ret += " //////\n"; - // 字节码 - for (int i = 0; i < (int) m_buff.length; i++) - { - long cmd = m_buff[i]; - int type = command.COMMAND_TYPE(cmd); - int code = command.COMMAND_CODE(cmd); - if (i == pos) - { - ret += "->"; - } - ret += "\t["; - ret += i; - ret += "]"; - ret += "[LINE "; - ret += get_binary_lineno(i); - ret += "]\t"; - ret += String.format("0x%016x", cmd); - ret += "\t"; - switch (type) - { - case command.COMMAND_OPCODE: - { - ret += "["; - ret += types.OpCodeStr(code); - ret += "]\t"; - } - break; - case command.COMMAND_ADDR: - { - ret += "[ ADDR ]\t"; - ret += types.dump_addr(code); - } - break; - case command.COMMAND_POS: - { - ret += "[ POS ]\t"; - ret += code; - } - break; - default: - { - ret += "[unknow]\t"; - } - break; - } - ret += "\n"; - } - ret += "\n"; - return ret; - } - - public int get_binary_lineno(int pos) - { - return (pos >= 0 && pos < (int) m_lineno_buff.length) ? m_lineno_buff[pos] - : (m_lineno_buff.length > 0 ? m_end_lineno : 0); - } - - public void backup_move() - { - // 最大栈空间 - m_maxstack = m_backup.m_maxstack; - // 参数个数 - m_paramnum = m_backup.m_paramnum; - // 名字 - m_name = m_backup.m_name; - // 文件名 - m_filename = m_backup.m_filename; - // 包名 - m_packagename = m_backup.m_packagename; - // 二进制缓冲区 - m_buff = m_backup.m_buff; - // 二进制行号缓冲区 - m_lineno_buff = m_backup.m_lineno_buff; - m_end_lineno = m_backup.m_end_lineno; - // 常量 - m_const_list = m_backup.m_const_list; - // container地址 - m_container_addr_list = m_backup.m_container_addr_list; - // 调试信息,栈变量 - m_debug_stack_variant_info = m_backup.m_debug_stack_variant_info; - // 占用标记 - m_use = m_backup.m_use; - // 新标记 - m_fresh = m_backup.m_fresh; - - // 备份 - m_backup = null; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/funcmap.java b/src/main/java/com/github/esrrhs/fakescript/funcmap.java deleted file mode 100644 index 2c51ae5..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/funcmap.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.github.esrrhs.fakescript; - -import java.util.HashMap; -import java.util.Map; - -class funcmap -{ - private fake m_f; - private HashMap m_funcmap = new HashMap(); - - public funcmap(fake f) - { - m_f = f; - } - - public funcmap clonef(fake f) - { - funcmap ret = new funcmap(f); - ret.m_funcmap = new HashMap(); - for (Map.Entry e : this.m_funcmap.entrySet()) - { - funcunion fc = e.getValue(); - ret.m_funcmap.put(e.getKey(), fc.clonef()); - } - return ret; - } - - public String dump() - { - return ""; - } - - public int size() - { - return 0; - } - - public void add_func(variant name, func_binary fb) - { - funcunion f = add_func_union(name); - f.m_fb = fb; - f.m_havefb = true; - } - - public void add_func(variant name, fkfunctor ff) - { - funcunion f = add_func_union(name); - f.m_ff = ff; - f.m_haveff = true; - } - - public void add_func(variant name, bifunc bif) - { - funcunion f = add_func_union(name); - f.m_bif = bif; - f.m_havebif = true; - } - - public funcunion get_func(variant name) - { - return m_funcmap.get(name); - } - - public HashMap get_funcmap() - { - return m_funcmap; - } - - private funcunion add_func_union(variant name) - { - funcunion p = m_funcmap.get(name); - if (p != null) - { - return p; - } - - funcunion tmp = new funcunion(); - m_funcmap.put(name, tmp); - return tmp; - } - -} diff --git a/src/main/java/com/github/esrrhs/fakescript/funcunion.java b/src/main/java/com/github/esrrhs/fakescript/funcunion.java deleted file mode 100644 index 69f55fa..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/funcunion.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.github.esrrhs.fakescript; - -class funcunion -{ - public fkfunctor m_ff; - public bifunc m_bif; - public func_binary m_fb; - public boolean m_haveff; - public boolean m_havebif; - public boolean m_havefb; - - public funcunion clonef() - { - funcunion fc = new funcunion(); - fc.m_ff = this.m_ff; - fc.m_bif = this.m_bif; - fc.m_fb = this.m_fb != null ? this.m_fb.clonef() : null; - fc.m_haveff = this.m_haveff; - fc.m_havebif = this.m_havebif; - fc.m_havefb = this.m_havefb; - return fc; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/interpreter.java b/src/main/java/com/github/esrrhs/fakescript/interpreter.java deleted file mode 100644 index 882a730..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/interpreter.java +++ /dev/null @@ -1,1657 +0,0 @@ -package com.github.esrrhs.fakescript; - -import java.util.ArrayList; - -class warper -{ - public Object d; - - public warper(Object d) - { - this.d = d; - } -} - -class interpreter -{ - public static final String MAP_FUNC_NAME = "map"; - public static final String GMAP_FUNC_NAME = "_G"; - - private fake m_f; - private boolean m_isend; - private ArrayList m_ret = new ArrayList(); - private processor m_processor; - // [ret pos] .. [ret pos] [ret num] [old ip] [call time] [old fb] [old bp] - private static final int BP_SIZE = 5; - private int m_sp; - private int m_bp; - private int m_ip; - private func_binary m_fb; - private ArrayList m_stack = new ArrayList(); - private long m_wakeuptime; - private int m_yieldtime; - private boolean m_sleeping; - - public interpreter(fake f) - { - m_f = f; - } - - public boolean is_end() - { - return m_isend; - } - - public variant get_ret() - { - return m_ret.isEmpty() ? new variant() : m_ret.get(0); - } - - public void set_processor(processor pro) - { - m_processor = pro; - } - - public int get_running_bytecode_pos() - { - return m_fb != null ? m_ip : -1; - } - - public void call(variant func, ArrayList retpos) throws Exception - { - funcunion f = m_f.fm.get_func(func); - if (f == null) - { - throw new Exception("run no func " + func + " fail"); - } - - if (f.m_havefb) - { - func_binary fb = f.m_fb; - variant v; - - // 准备栈大小 - int needsize = m_sp + BP_SIZE + retpos.size() + fb.m_maxstack; - if (needsize > m_stack.size()) - { - int oldsize = m_stack.size(); - for (int i = 0; i < needsize - oldsize; i++) - { - m_stack.add(new variant()); - } - } - - // 老的bp - int oldbp = m_bp; - m_bp = m_sp; - - // 记录返回位置 - for (int i = 0; i < retpos.size(); i++) - { - v = m_stack.get(m_bp); - v.m_type = variant_type.NIL; - v.m_data = retpos.get(i); - m_bp++; - } - - // 记录返回值数目 - v = m_stack.get(m_bp); - v.m_type = variant_type.NIL; - v.m_data = retpos.size(); - m_bp++; - - // 记录老的ip - v = m_stack.get(m_bp); - v.m_type = variant_type.NIL; - v.m_data = m_ip; - m_bp++; - - // 记录profile - if (m_f.pf.isopen()) - { - v = m_stack.get(m_bp); - v.m_data = System.currentTimeMillis(); - } - v.m_type = variant_type.NIL; - m_bp++; - - // 记录老的fb - v = m_stack.get(m_bp); - v.m_type = variant_type.NIL; - v.m_data = m_fb; - m_bp++; - - // 记录老的bp - v = m_stack.get(m_bp); - v.m_type = variant_type.NIL; - v.m_data = oldbp; - m_bp++; - - // 设置sp - m_sp = m_bp + fb.m_maxstack; - - if (m_f.ps.size() != fb.m_paramnum) - { - m_isend = true; - throw new Exception( - "call func " + func + " param not match, need " + fb.m_paramnum + " give " + m_f.ps.size()); - } - - // 分配入参 - for (int i = 0; i < fb.m_paramnum; i++) - { - v = m_stack.get(m_bp + i); - v.copy_from(m_f.ps.get(i)); - } - m_f.ps.clear(); - - // 重置ret - if (m_ret.isEmpty()) - { - m_ret.add(new variant()); - } - else - { - m_ret.get(0).set_nil(); - } - - // 标记 - fb.m_use++; - - // 新函数 - m_fb = fb; - m_ip = 0; - - return; - } - - // 记录profile - long s = 0; - if (m_f.pf.isopen()) - { - s = System.currentTimeMillis(); - } - - // 绑定函数 - if (f.m_haveff) - { - f.m_ff.call(m_f); - } - // 内置函数 - else if (f.m_havebif) - { - f.m_bif.call(m_f, this); - } - else - { - m_isend = true; - throw new Exception("run no inter func " + func + " fail"); - } - - // 返回值 - // 这种情况是直接跳过脚本调用了java函数 - if (m_bp == 0) - { - variant cret = m_f.ps.pop_and_get(); - m_isend = true; - // 直接塞返回值 - if (m_ret.isEmpty()) - { - m_ret.add(new variant()); - } - m_ret.get(0).copy_from(cret); - } - // 否则塞到当前堆栈上 - else - { - // 检查返回值数目对不对 - if (m_f.ps.size() != retpos.size()) - { - m_isend = true; - throw new Exception("native func " + func + " return num not match, give " + m_f.ps.size() + " need " - + retpos.size()); - } - - // 塞返回值 - for (int i = 0; i < retpos.size(); i++) - { - variant ret = GET_VARIANT(m_fb, m_bp, retpos.get(i)); - - variant cret = m_f.ps.get(i); - - ret.copy_from(cret); - } - } - - if (m_f.pf.isopen()) - { - String name = func.get_string(); - m_f.pf.add_func_sample(name, System.currentTimeMillis() - s); - } - } - - public variant GET_VARIANT(func_binary fb, int bp, int pos) throws Exception - { - return GET_VARIANT_BY_CMD(fb, bp, fb.m_buff[pos]); - } - - public variant GET_VARIANT_BY_CMD(func_binary fb, int bp, long cmd) throws Exception - { - long v_cmd = cmd; - int v_addrtype = command.ADDR_TYPE(command.COMMAND_CODE(v_cmd)); - int v_addrpos = command.ADDR_POS(command.COMMAND_CODE(v_cmd)); - if (v_addrtype == command.ADDR_STACK) - { - return m_stack.get(bp + v_addrpos); - } - else if (v_addrtype == command.ADDR_CONST) - { - return fb.m_const_list[v_addrpos]; - } - else if (v_addrtype == command.ADDR_CONTAINER) - { - return get_container_variant(fb, v_addrpos); - } - else - { - throw new Exception("addrtype cannot be " + v_addrtype + " " + v_addrpos); - } - } - - public variant get_container_variant(func_binary fb, int conpos) throws Exception - { - container_addr ca = fb.m_container_addr_list[conpos]; - - variant conv = GET_VARIANT_BY_CMD(fb, m_bp, ca.m_con); - variant keyv = GET_VARIANT_BY_CMD(fb, m_bp, ca.m_key); - - if (m_isend) - { - return null; - } - - if (conv.m_type != variant_type.ARRAY && conv.m_type != variant_type.MAP) - { - m_isend = true; - throw new Exception("interpreter get container variant fail, container type error, type " + conv.m_type); - } - - if (conv.m_type == variant_type.MAP) - { - return conv.get_map().con_map_get(keyv); - } - else if (conv.m_type == variant_type.ARRAY) - { - return conv.get_array().con_array_get(keyv); - } - else - { - return null; - } - } - - public long BP_GET_CALLTIME(int bp) - { - variant v = m_stack.get(bp - 3); - return (long) (Long) v.m_data; - } - - public int BP_GET_RETNUM(int bp) - { - variant v = m_stack.get(bp - 5); - return (int) (Integer) v.m_data; - } - - public int BP_GET_BP(int bp) - { - variant v = m_stack.get(bp - 1); - return (int) (Integer) v.m_data; - } - - public func_binary BP_GET_FB(int bp) - { - variant v = m_stack.get(bp - 2); - return (func_binary) v.m_data; - } - - public int BP_GET_IP(int bp) - { - variant v = m_stack.get(bp - 4); - return (int) (Integer) v.m_data; - } - - public int BP_GET_RETPOS(int bp, int retnum, int i) - { - variant v = m_stack.get(bp - 5 - retnum + i); - return (int) (Integer) v.m_data; - } - - public boolean CHECK_DST_POS(func_binary fb, int ip) - { - return CHECK_STACK_POS(fb, ip) | CHECK_CONTAINER_POS(fb, ip); - } - - public boolean CHECK_STACK_POS(func_binary fb, int ip) - { - return command.ADDR_TYPE(command.COMMAND_CODE(fb.m_buff[ip])) == command.ADDR_STACK; - } - - public boolean CHECK_CONTAINER_POS(func_binary fb, int ip) - { - return command.ADDR_TYPE(command.COMMAND_CODE(fb.m_buff[ip])) == command.ADDR_CONTAINER; - } - - public boolean CHECK_CONST_MAP_POS(variant v) throws Exception - { - return (v.m_type == variant_type.MAP && v.get_map().m_isconst); - } - - public boolean CHECK_CONST_ARRAY_POS(variant v) throws Exception - { - return (v.m_type == variant_type.ARRAY && v.get_array().m_isconst); - } - - public String POS_TYPE_NAME(func_binary fb, int ip) - { - int index = (int) command.ADDR_TYPE(command.COMMAND_CODE(fb.m_buff[ip])); - return variant_type.values()[index].name(); - } - - public int run(int cmdnum) throws Exception - { - int runcmdnum = 0; - - // 栈溢出检查 - if (m_stack.size() > m_f.cfg.stack_max) - { - m_isend = true; - throw new Exception("stack too big " + m_stack.size()); - } - - // 切换检查 - if (m_sleeping) - { - if (m_yieldtime != 0) - { - m_yieldtime--; - return 0; - } - else if (System.currentTimeMillis() < m_wakeuptime) - { - return 0; - } - else - { - m_wakeuptime = 0; - } - } - - if (m_isend) - { - return 0; - } - - while (true) - { - try - { - // 当前函数走完 - if (m_ip >= m_fb.m_buff.length) - { - // 记录profile - if (m_f.pf.isopen()) - { - long calltime = BP_GET_CALLTIME(m_bp); - m_f.pf.add_func_sample(m_fb.m_name, System.currentTimeMillis() - calltime); - } - - // 标记 - m_fb.m_use--; - - // 更新 - if (m_fb.m_use == 0 && m_fb.m_backup != null) - { - m_fb.backup_move(); - } - - // 出栈 - int oldretnum = BP_GET_RETNUM(m_bp); - int callbp = BP_GET_BP(m_bp); - m_fb = BP_GET_FB(m_bp); - m_ip = BP_GET_IP(m_bp); - int oldbp = m_bp; - m_sp = m_bp - BP_SIZE - oldretnum; - m_bp = callbp; - - // 所有都完 - if (m_bp == 0) - { - m_isend = true; - break; - } - // 塞返回值 - else - { - for (int i = 0; i < oldretnum; i++) - { - int oldretpos = BP_GET_RETPOS(oldbp, oldretnum, i); - - variant ret = GET_VARIANT(m_fb, m_bp, oldretpos); - ret.copy_from(m_ret.get(i)); - } - } - continue; - } - - int code = command.COMMAND_CODE(m_fb.m_buff[m_ip]); - - m_ip++; - - if (m_f.pf.isopen()) - { - m_f.pf.add_code_sample(code); - } - - // 执行对应命令,放一起switch效率更高,cpu有缓存 - switch (code) - { - case command.OPCODE_ASSIGN: - { - // 赋值dest,必须为栈上或容器内 - if (!CHECK_DST_POS(m_fb, m_ip)) - { - throw new Exception("interpreter assign error, dest is not stack or container, type " - + POS_TYPE_NAME(m_fb, m_ip)); - } - - variant varv = GET_VARIANT(m_fb, m_bp, m_ip); - if ((CHECK_CONST_MAP_POS(varv) || CHECK_CONST_ARRAY_POS(varv))) - { - - throw new Exception("interpreter assign error, dest is const container"); - } - m_ip++; - - // 赋值来源 - variant valuev = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - // 赋值 - varv.copy_from(valuev); - } - break; - case command.OPCODE_PLUS: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant right = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - if (!(CHECK_DST_POS(m_fb, m_ip))) - { - throw new Exception("interpreter math oper error, dest is not stack, type " - + POS_TYPE_NAME(m_fb, m_ip)); - } - variant dest = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - dest.plus(left, right); - } - break; - case command.OPCODE_MINUS: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant right = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - if (!(CHECK_DST_POS(m_fb, m_ip))) - { - throw new Exception("interpreter math oper error, dest is not stack, type " - + POS_TYPE_NAME(m_fb, m_ip)); - } - variant dest = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - dest.minus(left, right); - } - break; - case command.OPCODE_MULTIPLY: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant right = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - if (!(CHECK_DST_POS(m_fb, m_ip))) - { - throw new Exception("interpreter math oper error, dest is not stack, type " - + POS_TYPE_NAME(m_fb, m_ip)); - } - variant dest = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - dest.multiply(left, right); - } - break; - case command.OPCODE_DIVIDE: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant right = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - if (!(CHECK_DST_POS(m_fb, m_ip))) - { - throw new Exception("interpreter math oper error, dest is not stack, type " - + POS_TYPE_NAME(m_fb, m_ip)); - } - variant dest = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - dest.divide(left, right); - } - break; - case command.OPCODE_DIVIDE_MOD: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant right = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - if (!(CHECK_DST_POS(m_fb, m_ip))) - { - throw new Exception("interpreter math oper error, dest is not stack, type " - + POS_TYPE_NAME(m_fb, m_ip)); - } - variant dest = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - dest.divide_mod(left, right); - } - break; - case command.OPCODE_STRING_CAT: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant right = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - if (!(CHECK_DST_POS(m_fb, m_ip))) - { - throw new Exception("interpreter math oper error, dest is not stack, type " - + POS_TYPE_NAME(m_fb, m_ip)); - } - variant dest = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - dest.string_cat(left, right); - } - break; - case command.OPCODE_AND: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant right = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - if (!(CHECK_DST_POS(m_fb, m_ip))) - { - throw new Exception("interpreter math oper error, dest is not stack, type " - + POS_TYPE_NAME(m_fb, m_ip)); - } - variant dest = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - dest.and(left, right); - } - break; - case command.OPCODE_OR: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant right = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - if (!(CHECK_DST_POS(m_fb, m_ip))) - { - throw new Exception("interpreter math oper error, dest is not stack, type " - + POS_TYPE_NAME(m_fb, m_ip)); - } - variant dest = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - dest.or(left, right); - } - break; - case command.OPCODE_LESS: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant right = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - if (!(CHECK_DST_POS(m_fb, m_ip))) - { - throw new Exception("interpreter math oper error, dest is not stack, type " - + POS_TYPE_NAME(m_fb, m_ip)); - } - variant dest = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - dest.less(left, right); - } - break; - case command.OPCODE_MORE: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant right = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - if (!(CHECK_DST_POS(m_fb, m_ip))) - { - throw new Exception("interpreter math oper error, dest is not stack, type " - + POS_TYPE_NAME(m_fb, m_ip)); - } - variant dest = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - dest.more(left, right); - } - break; - case command.OPCODE_EQUAL: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant right = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - if (!(CHECK_DST_POS(m_fb, m_ip))) - { - throw new Exception("interpreter math oper error, dest is not stack, type " - + POS_TYPE_NAME(m_fb, m_ip)); - } - variant dest = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - dest.equal(left, right); - } - break; - case command.OPCODE_MOREEQUAL: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant right = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - if (!(CHECK_DST_POS(m_fb, m_ip))) - { - throw new Exception("interpreter math oper error, dest is not stack, type " - + POS_TYPE_NAME(m_fb, m_ip)); - } - variant dest = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - dest.more_equal(left, right); - } - break; - case command.OPCODE_LESSEQUAL: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant right = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - if (!(CHECK_DST_POS(m_fb, m_ip))) - { - throw new Exception("interpreter math oper error, dest is not stack, type " - + POS_TYPE_NAME(m_fb, m_ip)); - } - variant dest = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - dest.less_equal(left, right); - } - break; - case command.OPCODE_NOTEQUAL: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant right = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - if (!(CHECK_DST_POS(m_fb, m_ip))) - { - throw new Exception("interpreter math oper error, dest is not stack, type " - + POS_TYPE_NAME(m_fb, m_ip)); - } - variant dest = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - dest.not_equal(left, right); - } - break; - case command.OPCODE_NOT: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - if (!(CHECK_DST_POS(m_fb, m_ip))) - { - throw new Exception("interpreter math oper error, dest is not stack, type " - + POS_TYPE_NAME(m_fb, m_ip)); - } - variant dest = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - dest.not(left); - } - break; - case command.OPCODE_AND_JNE: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant right = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - /* dest */ - m_ip++; - int destip = command.COMMAND_CODE(m_fb.m_buff[m_ip]); - m_ip++; - - boolean b = variant.and_jne(left, right); - - if (!b) - { - m_ip = destip; - } - } - break; - case command.OPCODE_OR_JNE: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant right = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - /* dest */ - m_ip++; - int destip = command.COMMAND_CODE(m_fb.m_buff[m_ip]); - m_ip++; - - boolean b = variant.or_jne(left, right); - - if (!b) - { - m_ip = destip; - } - } - break; - case command.OPCODE_LESS_JNE: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant right = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - /* dest */ - m_ip++; - int destip = command.COMMAND_CODE(m_fb.m_buff[m_ip]); - m_ip++; - - boolean b = variant.less_jne(left, right); - - if (!b) - { - m_ip = destip; - } - } - break; - case command.OPCODE_MORE_JNE: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant right = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - /* dest */ - m_ip++; - int destip = command.COMMAND_CODE(m_fb.m_buff[m_ip]); - m_ip++; - - boolean b = variant.more_jne(left, right); - - if (!b) - { - m_ip = destip; - } - } - break; - case command.OPCODE_EQUAL_JNE: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant right = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - /* dest */ - m_ip++; - int destip = command.COMMAND_CODE(m_fb.m_buff[m_ip]); - m_ip++; - - boolean b = variant.equal_jne(left, right); - - if (!b) - { - m_ip = destip; - } - } - break; - case command.OPCODE_MOREEQUAL_JNE: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant right = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - /* dest */ - m_ip++; - int destip = command.COMMAND_CODE(m_fb.m_buff[m_ip]); - m_ip++; - - boolean b = variant.more_equal_jne(left, right); - - if (!b) - { - m_ip = destip; - } - } - break; - case command.OPCODE_LESSEQUAL_JNE: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant right = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - /* dest */ - m_ip++; - int destip = command.COMMAND_CODE(m_fb.m_buff[m_ip]); - m_ip++; - - boolean b = variant.less_equal_jne(left, right); - - if (!b) - { - m_ip = destip; - } - } - break; - case command.OPCODE_NOTEQUAL_JNE: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant right = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - /* dest */ - m_ip++; - int destip = command.COMMAND_CODE(m_fb.m_buff[m_ip]); - m_ip++; - - boolean b = variant.not_equal_jne(left, right); - - if (!b) - { - m_ip = destip; - } - } - break; - case command.OPCODE_NOT_JNE: - { - variant left = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - /* dest */ - m_ip++; - int destip = command.COMMAND_CODE(m_fb.m_buff[m_ip]); - m_ip++; - - boolean b = variant.not_jne(left); - - if (!b) - { - m_ip = destip; - } - } - break; - case command.OPCODE_JNE: - { - variant cmp = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - int ip = command.COMMAND_CODE(m_fb.m_buff[m_ip]); - m_ip++; - - if (!cmp.bool()) - { - m_ip = ip; - } - } - break; - case command.OPCODE_JMP: - { - int ip = command.COMMAND_CODE(m_fb.m_buff[m_ip]); - m_ip++; - - m_ip = ip; - } - break; - case command.OPCODE_PLUS_ASSIGN: - { - if (!CHECK_DST_POS(m_fb, m_ip)) - { - throw new Exception( - "interpreter math assign oper error, dest is not stack or container, type " - + POS_TYPE_NAME(m_fb, m_ip)); - } - variant var = GET_VARIANT(m_fb, m_bp, m_ip); - if ((CHECK_CONST_MAP_POS(var) || CHECK_CONST_ARRAY_POS(var))) - { - throw new Exception("interpreter assign error, dest is const container"); - } - m_ip++; - - variant value = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - var.plus(var, value); - } - break; - case command.OPCODE_MINUS_ASSIGN: - { - if (!CHECK_DST_POS(m_fb, m_ip)) - { - throw new Exception( - "interpreter math assign oper error, dest is not stack or container, type " - + POS_TYPE_NAME(m_fb, m_ip)); - } - variant var = GET_VARIANT(m_fb, m_bp, m_ip); - if ((CHECK_CONST_MAP_POS(var) || CHECK_CONST_ARRAY_POS(var))) - { - throw new Exception("interpreter assign error, dest is const container"); - } - m_ip++; - - variant value = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - var.minus(var, value); - } - break; - case command.OPCODE_MULTIPLY_ASSIGN: - { - if (!CHECK_DST_POS(m_fb, m_ip)) - { - throw new Exception( - "interpreter math assign oper error, dest is not stack or container, type " - + POS_TYPE_NAME(m_fb, m_ip)); - } - variant var = GET_VARIANT(m_fb, m_bp, m_ip); - if ((CHECK_CONST_MAP_POS(var) || CHECK_CONST_ARRAY_POS(var))) - { - throw new Exception("interpreter assign error, dest is const container"); - } - m_ip++; - - variant value = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - var.multiply(var, value); - } - break; - case command.OPCODE_DIVIDE_ASSIGN: - { - if (!CHECK_DST_POS(m_fb, m_ip)) - { - throw new Exception( - "interpreter math assign oper error, dest is not stack or container, type " - + POS_TYPE_NAME(m_fb, m_ip)); - } - variant var = GET_VARIANT(m_fb, m_bp, m_ip); - if ((CHECK_CONST_MAP_POS(var) || CHECK_CONST_ARRAY_POS(var))) - { - throw new Exception("interpreter assign error, dest is const container"); - } - m_ip++; - - variant value = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - var.divide(var, value); - } - break; - case command.OPCODE_DIVIDE_MOD_ASSIGN: - { - if (!CHECK_DST_POS(m_fb, m_ip)) - { - throw new Exception( - "interpreter math assign oper error, dest is not stack or container, type " - + POS_TYPE_NAME(m_fb, m_ip)); - } - variant var = GET_VARIANT(m_fb, m_bp, m_ip); - if ((CHECK_CONST_MAP_POS(var) || CHECK_CONST_ARRAY_POS(var))) - { - throw new Exception("interpreter assign error, dest is const container"); - } - m_ip++; - - variant value = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - var.divide_mod(var, value); - } - break; - case command.OPCODE_CALL: - { - int calltype = command.COMMAND_CODE(m_fb.m_buff[m_ip]); - m_ip++; - - variant callpos = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - int retnum = command.COMMAND_CODE(m_fb.m_buff[m_ip]); - m_ip++; - - ArrayList retpos = new ArrayList(); - - for (int i = 0; i < retnum; i++) - { - retpos.add(m_ip); - m_ip++; - } - - int argnum = command.COMMAND_CODE(m_fb.m_buff[m_ip]); - m_ip++; - - paramstack ps = m_f.ps; - ps.clear(); - for (int i = 0; i < argnum; i++) - { - variant arg = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant argdest = ps.push_and_get(); - - argdest.copy_from(arg); - } - - if (calltype == command.CALL_NORMAL) - { - call(callpos, retpos); - } - else if (calltype == command.CALL_CLASSMEM) - { - // prefix - variant classvar = ps.get(ps.size() - 1); - if (classvar == null) - { - throw new Exception("interpreter class mem call error, the class ref is null"); - } - - Object classptr = classvar.get_pointer(); - if (classptr == null) - { - throw new Exception("interpreter class mem call error, the class ref is null"); - } - - // mem func name - String funcname = callpos.get_string(); - - // whole name - String wholename = classptr.getClass().getName() + funcname; - - // call it - variant tmp = new variant(); - tmp.set_string(wholename); - - call(tmp, retpos); - } - else - { - m_processor.start_routine(callpos, retpos); - } - } - break; - case command.OPCODE_RETURN: - { - int returnnum = command.COMMAND_CODE(m_fb.m_buff[m_ip]); - if (returnnum == 0) - { - m_ip = (m_fb).m_buff.length; - break; - } - m_ip++; - - int oldsize = m_ret.size(); - for (int i = 0; i < returnnum - oldsize; i++) - { - m_ret.add(new variant()); - } - - // 塞给ret - for (int i = 0; i < returnnum; i++) - { - variant ret = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - variant retv = new variant(); - retv.copy_from(ret); - m_ret.set(i, retv); - } - - m_ip = (m_fb).m_buff.length; - } - break; - case command.OPCODE_FORBEGIN: - { - // 赋值dest,必须为栈上或容器内 - if (!CHECK_DST_POS(m_fb, m_ip)) - { - throw new Exception("interpreter assign error, dest is not stack or container, type " - + POS_TYPE_NAME(m_fb, m_ip)); - } - - // var - variant varv = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - // begin - variant beginv = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - // end - variant endv = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - // add - variant addv = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - int jneip = command.COMMAND_CODE(m_fb.m_buff[m_ip]); - m_ip++; - - // 赋值 - varv.copy_from(beginv); - - // 增长 - if (addv.get_real() > 0) - { - // 判断是否超出 - if (varv.get_real() >= endv.get_real()) - { - m_ip = jneip; - } - } - else - { - // 判断是否小 - if (varv.get_real() <= endv.get_real()) - { - m_ip = jneip; - } - } - } - break; - case command.OPCODE_FORLOOP: - { - // var - variant varv = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - // end - variant endv = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - // add - variant addv = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - int continueip = command.COMMAND_CODE(m_fb.m_buff[m_ip]); - m_ip++; - - // 赋值 - varv.plus(varv, addv); - - // 增长 - if (addv.get_real() > 0) - { - // 判断是否超出 - if (varv.get_real() < endv.get_real()) - { - m_ip = continueip; - } - } - else - { - // 判断是否小 - if (varv.get_real() > endv.get_real()) - { - m_ip = continueip; - } - } - } - break; - case command.OPCODE_SLEEP: - { - variant time = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - long sleeptime = (long) (double) time.get_real(); - - m_wakeuptime = System.currentTimeMillis() + sleeptime; - m_sleeping = true; - return runcmdnum + 1; - } - case command.OPCODE_YIELD: - { - variant time = GET_VARIANT(m_fb, m_bp, m_ip); - m_ip++; - - m_yieldtime = (int) time.get_real(); - m_sleeping = true; - return runcmdnum + 1; - } - default: - throw new Exception("next err code " + code + " " + types.OpCodeStr(code)); - } - } - catch (Exception e) - { - // 发生错误 - m_isend = true; - - // 清除当前栈上函数的使用标记 - { - int ip = m_ip; - int bp = m_bp; - func_binary fb = m_fb; - - while (bp != 0) - { - // 标记 - fb.m_use--; - - // 更新 - if (fb.m_use == 0 && fb.m_backup != null) - { - fb.backup_move(); - } - - fb = BP_GET_FB(bp); - ip = BP_GET_IP(bp); - int callbp = BP_GET_BP(bp); - bp = callbp; - if (bp == 0) - { - break; - } - } - } - - throw e; - } - - if ((m_isend)) - { - break; - } - - runcmdnum++; - - if ((runcmdnum >= cmdnum)) - { - break; - } - } - - return runcmdnum; - } - - public String get_running_file_name() - { - return m_fb != null ? m_fb.m_filename : ""; - } - - public String get_running_func_name() - { - return m_fb != null ? m_fb.m_name : ""; - } - - public int get_running_file_line() - { - return m_fb != null ? m_fb.get_binary_lineno(m_ip) : 0; - } - - public String get_running_call_stack() - { - String cur_runinginfo = ""; - - if (m_fb == null) - { - return ""; - } - - int deps = 0; - - int ip = m_ip; - int bp = m_bp; - func_binary fb = m_fb; - - while (bp != 0) - { - cur_runinginfo += "#"; - cur_runinginfo += deps; - cur_runinginfo += " "; - cur_runinginfo += fb != null ? fb.m_name : ""; - cur_runinginfo += " at "; - cur_runinginfo += fb != null ? fb.m_filename : ""; - cur_runinginfo += ":"; - cur_runinginfo += fb != null ? fb.get_binary_lineno(ip) : 0; - cur_runinginfo += "\n"; - for (int j = 0; fb != null && j < fb.m_maxstack; j++) - { - cur_runinginfo += " "; - - String variant_name = ""; - for (int i = 0; i < fb.m_debug_stack_variant_info.length; i++) - { - stack_variant_info info = fb.m_debug_stack_variant_info[i]; - if (info.m_pos == j) - { - variant_name += info.m_name; - variant_name += "(line:"; - variant_name += info.m_line; - variant_name += ") "; - } - } - if (variant_name.isEmpty()) - { - variant_name = "(anonymous)"; - } - - cur_runinginfo += variant_name; - cur_runinginfo += "\t["; - cur_runinginfo += j; - cur_runinginfo += "]\t"; - variant v = bp + j < m_stack.size() ? m_stack.get(bp + j) : new variant(); - cur_runinginfo += v; - cur_runinginfo += "\n"; - } - - fb = BP_GET_FB(bp); - ip = BP_GET_IP(bp); - int callbp = BP_GET_BP(bp); - bp = callbp; - if (bp == 0) - { - break; - } - - deps++; - } - - return cur_runinginfo; - } - - public void get_running_vaiant(int frame, String name, int line, warper value, warper outline) - { - String valueret = ""; - - if (m_fb == null) - { - return; - } - - // const define - variant gcv = m_f.pa.get_const_define(name); - if (gcv != null) - { - if (gcv.m_type == variant_type.STRING) - { - valueret += "\""; - valueret += gcv.toString(); - valueret += "\""; - } - else - { - valueret += gcv.toString(); - } - value.d = valueret; - outline.d = m_f.pa.get_const_define_lineno(name); - return; - } - - int deps = 0; - - int bp = m_bp; - func_binary fb = m_fb; - - while (bp != 0) - { - if (deps >= frame) - { - for (int i = 0; i < fb.m_debug_stack_variant_info.length; i++) - { - stack_variant_info info = fb.m_debug_stack_variant_info[i]; - if ((line != -1 && info.m_name.equals(name) && info.m_line == line) - || (line == -1 && info.m_name.equals(name))) - { - variant v = m_stack.get(bp + info.m_pos); - if (v.m_type == variant_type.STRING) - { - valueret += "\""; - valueret += v.toString(); - valueret += "\""; - } - else - { - valueret += v.toString(); - } - - value.d = valueret; - outline.d = info.m_line; - return; - } - } - break; - } - - fb = BP_GET_FB(bp); - int callbp = 0; - callbp = BP_GET_BP(bp); - bp = callbp; - if (bp == 0) - { - break; - } - - deps++; - } - - return; - } - - public void get_running_call_stack_frame_info(int frame, warper stackinfo, warper func, warper file, warper line) - { - String valueret = ""; - - if (m_fb == null) - { - return; - } - - int deps = 0; - - int ip = m_ip; - int bp = m_bp; - func_binary fb = m_fb; - - while (bp != 0) - { - if (deps >= frame) - { - func.d = fb != null ? fb.m_name : ""; - file.d = fb != null ? fb.m_filename : ""; - line.d = fb != null ? fb.get_binary_lineno(ip) : 0; - - valueret += "#"; - valueret += deps; - valueret += " "; - valueret += func; - valueret += " at "; - valueret += file; - valueret += ":"; - valueret += line; - valueret += "\n"; - - stackinfo.d = valueret; - - return; - } - - fb = BP_GET_FB(bp); - ip = BP_GET_IP(bp); - int callbp = 0; - callbp = BP_GET_BP(bp); - bp = callbp; - if (bp == 0) - { - break; - } - - deps++; - } - - } - - public int get_running_call_stack_length() - { - if (m_fb == null) - { - return 0; - } - - int deps = 0; - - int bp = m_bp; - - while (bp != 0) - { - deps++; - int callbp = 0; - callbp = BP_GET_BP(bp); - bp = callbp; - if (bp == 0) - { - break; - } - } - - return deps; - } - - public void set_running_vaiant(int frame, String name, int line, String value) - { - fake f = m_f; - - if (m_fb == null) - { - return; - } - - // const define - variant gcv = m_f.pa.get_const_define(name); - if (gcv != null) - { - // can not change - return; - } - - int deps = 0; - - int bp = m_bp; - func_binary fb = m_fb; - - while (bp != 0) - { - if (deps >= frame) - { - for (int i = 0; i < fb.m_debug_stack_variant_info.length; i++) - { - stack_variant_info info = fb.m_debug_stack_variant_info[i]; - if ((line != -1 && info.m_name.equals(name) && info.m_line == line) - || (line == -1 && info.m_name.equals(name))) - { - variant v = m_stack.get(bp + info.m_pos); - - String valuestr = value; - if (valuestr.isEmpty()) - { - return; - } - - if (valuestr.startsWith("\"")) - { - valuestr = valuestr.substring(1, valuestr.length() - 1); - v.set_string(valuestr); - } - else - { - v.set_real(Integer.parseInt(value)); - } - - return; - } - } - break; - } - - fb = BP_GET_FB(bp); - int callbp = 0; - callbp = BP_GET_BP(bp); - bp = callbp; - if (bp == 0) - { - break; - } - - deps++; - } - - } - -} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/Arith.java b/src/main/java/com/github/esrrhs/fakescript/lua/Arith.java new file mode 100644 index 0000000..942c6eb --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/Arith.java @@ -0,0 +1,285 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * 常量折叠需要的那部分算术 —— 对应 {@code lobject.c} 的 {@code luaO_rawarith} / + * {@code intarith} / {@code numarith},以及 {@code lvm.c} 的 {@code luaV_idiv} / + * {@code luaV_mod} / {@code luaV_modf} / {@code luaV_shiftl}。 + *

+ * 为什么现在就要它:编译期的常量折叠跑的就是这套运算,折叠结果会被直接写进 + * 常量表。若折叠用的算术与运行期的不是同一套,就会出现 + * 「同一个表达式,常量写法与变量写法结果不同」——那是最难查的一类 bug。 + *

+ * 几处刻意的「不像 Java」: + *

    + *
  • 整数除法 {@code //} 是向下取整,C/Java 的 {@code /} 是向零截断, + * 所以要在结果需要时减一;
  • + *
  • 取模的符号跟随除数,而 Java 的 {@code %} 跟随被除数;
  • + *
  • 右移是逻辑右移({@code intop} 先转无符号),空位补 0, + * 不是 Java 的算术右移;
  • + *
  • 移位量超出字长时结果是 0,而 Java 的移位会把位数按 64 取模;
  • + *
  • {@code math.mininteger // -1} 不溢出(返回 {@code -mininteger})。
  • + *
+ */ +final class Arith { + + private Arith() { + } + + /* ORDER TM / ORDER OP —— 与 lua.h 的 LUA_OP* 一致 */ + static final int OP_ADD = 0; + static final int OP_SUB = 1; + static final int OP_MUL = 2; + static final int OP_MOD = 3; + static final int OP_POW = 4; + static final int OP_DIV = 5; + static final int OP_IDIV = 6; + static final int OP_BAND = 7; + static final int OP_BOR = 8; + static final int OP_BXOR = 9; + static final int OP_SHL = 10; + static final int OP_SHR = 11; + static final int OP_UNM = 12; + static final int OP_BNOT = 13; + + /** 整数的位数 */ + private static final int NBITS = 64; + + /** + * {@code luaO_rawarith}:不经元方法的原始算术。 + *

+ * {@code L} 只为一件事服务:{@code luaV_idiv} / {@code luaV_mod} 在除数为零时 + * 要抛 {@code luaG_runerror},而后者当场就把 {@code file:line:} 前缀 + * 拼进消息里({@code luaG_addinfo})。没有 {@code L} 就抛裸消息, + * 位置得等到保护边界才补——而补的时候出错的那个帧已经不在了: + * {@code coroutine.resume} 把错误当值返回时,参考实现给的是 + * {@code file:line: attempt to divide by zero},本实现只给后半句。 + *

+ * {@code L == null} 是编译期常量折叠那一路:{@code validOp} 已经把除零 + * 排除掉了,那里不会走到这两个分支。 + * + * @return 结果值;操作数类型不合适时返回 {@code null} + */ + static LuaValue rawArith(LuaState L, int op, LuaValue p1, LuaValue p2) { + switch (op) { + case OP_BAND: + case OP_BOR: + case OP_BXOR: + case OP_SHL: + case OP_SHR: + case OP_BNOT: { // 位运算只在整数上做 + Long i1 = toIntegerNs(p1); + Long i2 = toIntegerNs(p2); + if (i1 == null || i2 == null) { + return null; + } + return LuaValue.ofInteger(intArith(L, op, i1, i2)); + } + case OP_DIV: + case OP_POW: { // 只在浮点上做 + Double n1 = toNumberNs(p1); + Double n2 = toNumberNs(p2); + if (n1 == null || n2 == null) { + return null; + } + return LuaValue.ofFloat(numArith(op, n1, n2)); + } + default: { + if (p1.isInteger() && p2.isInteger()) { + return LuaValue.ofInteger(intArith(L, op, p1.i, p2.i)); + } + Double n1 = toNumberNs(p1); + Double n2 = toNumberNs(p2); + if (n1 == null || n2 == null) { + return null; + } + return LuaValue.ofFloat(numArith(op, n1, n2)); + } + } + } + + private static Double toNumberNs(LuaValue v) { + if (v.isInteger()) { + return (double) v.i; + } + if (v.isFloat()) { + return v.n; + } + return null; + } + + /** {@code luaV_tointegerns}:不做「字符串转数字」,只认真正的数字 */ + static Long toIntegerNs(LuaValue v) { + if (v.isFloat()) { + return fltToInteger(v.n, F2I_EQ); + } + if (v.isInteger()) { + return v.i; + } + return null; + } + + /* ---------- 浮点 → 整数 ---------- */ + + static final int F2I_EQ = 0; + static final int F2I_FLOOR = 1; + static final int F2I_CEIL = 2; + + /** {@code luaV_flttointeger} */ + static Long fltToInteger(double n, int mode) { + double f = Math.floor(n); + if (n != f) { // 不是整值? + if (mode == F2I_EQ) { + return null; // 要求必须是整值,失败 + } else if (mode == F2I_CEIL) { + f += 1; + } + } + return numberToInteger(f); + } + + /** {@code lua_numbertointeger}:超出 int64 可表示范围时失败 */ + private static Long numberToInteger(double n) { + if (n >= (double) Long.MIN_VALUE && n < -((double) Long.MIN_VALUE)) { + return (long) n; + } + return null; + } + + /* ---------- 整数运算 ---------- */ + + /** {@code intarith} */ + private static long intArith(LuaState L, int op, long v1, long v2) { + switch (op) { + case OP_ADD: + return v1 + v2; + case OP_SUB: + return v1 - v2; + case OP_MUL: + return v1 * v2; + case OP_MOD: + return mod(L, v1, v2); + case OP_IDIV: + return idiv(L, v1, v2); + case OP_BAND: + return v1 & v2; + case OP_BOR: + return v1 | v2; + case OP_BXOR: + return v1 ^ v2; + case OP_SHL: + return shiftl(v1, v2); + case OP_SHR: + return shiftl(v1, -v2); + case OP_UNM: + return -v1; + case OP_BNOT: + return ~v1; + default: + throw new IllegalArgumentException("bad int op " + op); + } + } + + /** + * {@code luaV_idiv}:向下取整的整数除法。 + *

+ * 除零要 {@code luaG_runerror},位置前缀是当场加的(见 + * {@link #rawArith})。留着那条裸 {@code LuaError} 只在 + * {@code L == null}(常量折叠)时用得上,而那条路进不来这里。 + */ + static long idiv(LuaState L, long m, long n) { + if (n == 0) { + if (L != null) { + throw L.runError("attempt to divide by zero"); + } + throw new LuaError(LuaValue.ofString(LuaString.latin1("attempt to divide by zero"))); + } + if (n == -1) { + return -m; // 避开 mininteger // -1 的溢出 + } + long q = m / n; + if ((m ^ n) < 0 && m % n != 0) { + q -= 1; + } + return q; + } + + /** {@code luaV_mod}:结果的符号跟随除数 */ + static long mod(LuaState L, long m, long n) { + if (n == 0) { + /* + * 注意是 'n%0' 而不是 'n%%0':C 版这里是 printf 的格式串, + * {@code %%} 只是转义,真正打出来的是 {@code n%0}。Java 不做 + * 格式串转义,照抄 {@code %%} 会多出一个百分号。 + */ + if (L != null) { + throw L.runError("attempt to perform 'n%0'"); + } + throw new LuaError(LuaValue.ofString(LuaString.latin1("attempt to perform 'n%0'"))); + } + if (n == -1) { + return 0; // 避开溢出 + } + long r = m % n; + if (r != 0 && (r ^ n) < 0) { + r += n; + } + return r; + } + + /** + * {@code luaV_shiftl}:移位数超出字长时结果是 0(Java 会按 64 取模,不一样)。 + *

+ * 注意右移是逻辑右移:C 里的 {@code intop(>>, x, -y)} 先把两个操作数 + * 转成 {@code lua_Unsigned} 再移,空位补 0。所以 {@code -8 >> 2} 是 + * {@code 0x3FFFFFFFFFFFFFFE}(4611686018427387902)而不是 {@code -2}, + * 与 Lua 手册「两个移位运算符都用 0 填充空位」一致。 + * 参考实现实测过,别照 Java 直觉写成 {@code >>}。 + */ + static long shiftl(long x, long y) { + if (y < 0) { + if (y <= -NBITS) { + return 0; + } + return x >>> (-y); + } + if (y >= NBITS) { + return 0; + } + return x << y; + } + + /* ---------- 浮点运算 ---------- */ + + /** {@code numarith} */ + private static double numArith(int op, double v1, double v2) { + switch (op) { + case OP_ADD: + return v1 + v2; + case OP_SUB: + return v1 - v2; + case OP_MUL: + return v1 * v2; + case OP_DIV: + return v1 / v2; + case OP_POW: + return (v2 == 2) ? v1 * v1 : Math.pow(v1, v2); + case OP_IDIV: + return Math.floor(v1 / v2); + case OP_UNM: + return -v1; + case OP_MOD: + return modf(v1, v2); + default: + throw new IllegalArgumentException("bad num op " + op); + } + } + + /** {@code luaV_modf}:{@code fmod} 之后把结果调到与除数同号 */ + static double modf(double m, double n) { + double r = m % n; + if ((r > 0) ? (n < 0) : ((r < 0 && n > 0))) { + r += n; + } + return r; + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/BaseLib.java b/src/main/java/com/github/esrrhs/fakescript/lua/BaseLib.java new file mode 100644 index 0000000..d490d7c --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/BaseLib.java @@ -0,0 +1,1237 @@ +package com.github.esrrhs.fakescript.lua; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; + +/** + * 基础库 —— 对应 {@code lbaselib.c},加上 {@code lstrlib.c} 里那组字符串算术元方法。 + *

+ * 阶段 3 只求「脚本能跑起来并对 stdout 做差分」,所以这里挑的是差分比对真正 + * 用得上的函数:输出、类型、转换、元表、迭代、错误处理。字符串 / 表 / os / + * io 等库随阶段 7 各自成文件。 + *

+ * 为什么这里要挂字符串的算术元方法。{@code "10" + 1 == 11} 这件事 + * 不是 VM 做的——VM 的算术宏只认真正的数字。它来自 {@code lstrlib.c} + * 给字符串元表装的 {@code __add}:先把两侧 {@code lua_stringtonumber}, + * 再 {@code lua_arith}。少了这一组,字符串参与算术就会走到「attempt to perform + * arithmetic on a string value」——一个看起来合理但与参考实现不同的错误。 + */ +public final class BaseLib { + + private BaseLib() { + } + + /** 把基础库装进全局表 */ + public static void open(LuaState L) { + LuaTable g = L.globals; + + setFn(g, "print", BaseLib::print); + setFn(g, "type", BaseLib::type); + setFn(g, "tostring", BaseLib::tostring); + setFn(g, "tonumber", BaseLib::tonumber); + setFn(g, "rawget", BaseLib::rawget); + setFn(g, "rawset", BaseLib::rawset); + setFn(g, "rawequal", BaseLib::rawequal); + setFn(g, "rawlen", BaseLib::rawlen); + setFn(g, "next", BaseLib::next); + setFn(g, "pairs", BaseLib::pairs); + setFn(g, "ipairs", BaseLib::ipairs); + setFn(g, "select", BaseLib::select); + setFn(g, "error", BaseLib::error); + setFn(g, "assert", BaseLib::assertFn); + setFn(g, "pcall", BaseLib::pcall); + setFn(g, "xpcall", BaseLib::xpcall); + setFn(g, "setmetatable", BaseLib::setmetatable); + setFn(g, "getmetatable", BaseLib::getmetatable); + setFn(g, "collectgarbage", BaseLib::collectgarbage); + setFn(g, "load", BaseLib::loadFn); + setFn(g, "loadfile", BaseLib::loadfileFn); + setFn(g, "dofile", BaseLib::dofile); + setFn(g, "warn", BaseLib::warn); + + /* + * {@code lbaselib.c: luaopen_base} 最后两件事:设 {@code _G} 与 + * {@code _VERSION}。{@code _G} 在 {@link LuaState} 的构造里就已经挂上了 + * (注册表也要用到它),这里只补 {@code _VERSION}。 + *

+ * 别小看这一个字符串:{@code _VERSION} 是最常见的版本探测手段, + * 真实项目里到处是 {@code if _VERSION == "Lua 5.1" then ... end} 这种 + * 分支,缺了它走的是 else —— 不报错,只是行为悄悄不同,正好是最难查的那类。 + */ + g.set("_VERSION", LuaValue.ofString(LuaString.latin1(Lua.LUA_VERSION))); + + openStringMetatable(L); + } + + /* ==================== 字符串元表 ==================== */ + + /** + * {@code lstrlib.c: createmetatable} + 算术元方法。 + *

+ * 上游还给这个元表设了 {@code __index = string} 库(所以 {@code ("x"):upper()} + * 能用),那部分随字符串库一起到阶段 7 再补。 + */ + private static void openStringMetatable(LuaState L) { + LuaTable mt = new LuaTable(); + setFn(mt, "__add", l -> strArith(l, Arith.OP_ADD, Tm.TM_ADD, "add")); + setFn(mt, "__sub", l -> strArith(l, Arith.OP_SUB, Tm.TM_SUB, "sub")); + setFn(mt, "__mul", l -> strArith(l, Arith.OP_MUL, Tm.TM_MUL, "mul")); + setFn(mt, "__mod", l -> strArith(l, Arith.OP_MOD, Tm.TM_MOD, "mod")); + setFn(mt, "__pow", l -> strArith(l, Arith.OP_POW, Tm.TM_POW, "pow")); + setFn(mt, "__div", l -> strArith(l, Arith.OP_DIV, Tm.TM_DIV, "div")); + setFn(mt, "__idiv", l -> strArith(l, Arith.OP_IDIV, Tm.TM_IDIV, "idiv")); + setFn(mt, "__unm", l -> strArith(l, Arith.OP_UNM, Tm.TM_UNM, "unm")); + L.setTypeMetatable(Lua.TSTRING, mt); + } + + /** + * {@code lstrlib.c: arith}。 + *

+ * 关键在那句 {@code else trymt(L, mtname)}:转换不成数字不是立刻报错, + * 而是再去第二个操作数身上找同名元方法。少了这一步, + * {@code '5' + b}({@code b} 是带 {@code __add} 的表)会报 + * 「attempt to add a 'string' with a 'table'」,而参考实现是正常调用 + * {@code b} 的 {@code __add}——上游 {@code events.lua} 那一整串 + * {@code checkcap{"add", '5', b}} 全靠它。 + */ + private static int strArith(LuaState L, int op, int event, String name) { + LuaValue p1 = L.arg(0); + LuaValue p2 = L.arg(1); + LuaValue n1 = numOf(p1); + LuaValue n2 = numOf(p2); + if (n1 != null && n2 != null) { + LuaValue res = Arith.rawArith(L, op, n1, n2); + if (res != null) { + L.push(res); + return 1; + } + } + /* lstrlib.c: trymt —— 回到原来的两个参数,去 p2 身上找元方法 */ + L.setTop(2); + /* + * 「第二个操作数是字符串」时不找:否则 {@code '5' + 'x'} 会找到 + * 字符串元表的 {@code __add}——也就是现在正在跑的这个函数——无限递归。 + * C 版用 {@code lua_type(L, 2) == LUA_TSTRING} 挡住,正是这个原因。 + */ + if (!p2.isString()) { + LuaValue tm = LuaTm.getTmByObj(L, p2, event); + if (tm != null && !tm.isNil()) { + int f1 = L.ci.func + 1; // 第一个参数的位置 + L.push(tm); + // lua_insert(L, -3):元方法挪到两个参数下面 + LuaValue moved = new LuaValue(L.stack[L.top - 1]); + for (int i = L.top - 1; i > f1; i--) { + L.stack[i].set(L.stack[i - 1]); + } + L.stack[f1].set(moved); + L.call(f1, 1); // C 版是 lua_call,允许 yield + return 1; + } + } + throw L.libError("attempt to " + name + " a '" + p1.typeName() + "' with a '" + + p2.typeName() + "'"); + } + + /** {@code lstrlib.c: tonum}:已是数字就原样返回,否则试着把整个字符串解释成数字 */ + private static LuaValue numOf(LuaValue v) { + if (v.isNumber()) { + return v; + } + return Vm.strToNum(v); + } + + /* ==================== 各函数 ==================== */ + + /** {@code luaB_print}:制表符分隔,末尾换行 */ + private static int print(LuaState L) { + int n = L.argCount(); + ByteArrayOutputStream tmp = new ByteArrayOutputStream(); + for (int i = 0; i < n; i++) { + if (i > 0) { + tmp.write('\t'); + } + LuaString s = toLString(L, L.arg(i)); + tmp.write(s.bytes, 0, s.bytes.length); + } + tmp.write('\n'); + byte[] all = tmp.toByteArray(); + L.out.write(all, 0, all.length); + L.out.flush(); + return 0; + } + + /** {@code luaB_type}:{@code luaL_argcheck(t != LUA_TNONE, 1, "value expected")} */ + private static int type(LuaState L) { + LuaAux.argCheck(L, !L.isNoneArg(1), 1, "value expected"); + L.push(LuaValue.ofString(LuaString.latin1(L.arg(0).typeName()))); + return 1; + } + + /** {@code luaB_tostring} */ + private static int tostring(LuaState L) { + LuaAux.checkAny(L, 1); + L.push(LuaValue.ofString(toLString(L, L.arg(0)))); + return 1; + } + + /** {@code luaL_tolstring}:先看 {@code __tostring},再按类型格式化 */ + static LuaString toLString(LuaState L, LuaValue v) { + LuaValue ts = metamethodByName(L, v, "__tostring"); + if (ts != null) { + // 调 __tostring(v) + int func = L.top; + L.ensureSlots(func + 2); + L.stack[func].set(ts); + L.stack[func + 1].set(v); + L.top = func + 2; + /* + * luaL_callmeta 走的是 lua_call,也就是 luaD_callnoyield: + * __tostring 里 yield 会报「跨 C 调用边界」。实测参考实现 + * print(setmetatable({}, {__tostring = f})) 里 f 一 yield 就炸, + * 换成 L.call 会让它「成功」,反而与参考实现不一致。 + */ + L.callNoYield(func, 1); + LuaValue res = L.stack[--L.top]; + /* + * 判据是 {@code lua_isstring} 而不是「是不是字符串」: + * 数字也算,会被就地转换成字符串。实测 + * {@code print(setmetatable({}, {__tostring = function() return 42 end}))} + * 打印 {@code 42},只有返回表这类真不行的才报 + * 「'__tostring' must return a string」。 + */ + if (res.isString()) { + return res.asString(); + } + if (res.isNumber()) { + return LuaString.latin1(Vm.numberToString(res)); + } + throw L.libError("'__tostring' must return a string"); + } + switch (v.baseType()) { + case Lua.TNUMBER: + return LuaString.latin1(Vm.numberToString(v)); + case Lua.TSTRING: + return v.asString(); + case Lua.TBOOLEAN: + return LuaString.latin1(v.toBoolean() ? "true" : "false"); + case Lua.TNIL: + return LuaString.latin1("nil"); + default: { + LuaString kind = LuaString.latin1(v.typeName()); + LuaValue name = metamethodByName(L, v, "__name"); + if (name != null && name.isString()) { + /* + * __name 走的是 C 字符串语义:lauxlib.c: luaL_tolstring 取到 __name + * 后用 lua_tostring 转成 const char*,再拿 "%s" 拼进 "名字: 0x地址", + * 于是名字在第一个 \0 处被砍断。__name = "A\0B" 只打印出 "A"。 + */ + kind = name.asString().toCString(); + } + /* + * 地址用 C 的 "%p" 格式:0x 加十六进制、不补前导零 + * (macOS 打出 0x7ab30002c0,glibc 打出 0x55d3...)。补成 16 位 + * 只是看着整齐,跟参考实现对不上。 + */ + String ptr = "0x" + Integer.toHexString(System.identityHashCode(v.o)); + return kind.concat(LuaString.latin1(": " + ptr)); + } + } + } + + private static LuaValue metamethodByName(LuaState L, LuaValue v, String name) { + LuaTable mt = LuaTm.metatableOf(L, v); + if (mt == null) { + return null; + } + LuaValue r = mt.get(LuaString.latin1(name)); + return (r == null || r.isNil()) ? null : r; + } + + /** {@code luaB_tonumber} */ + private static int tonumber(LuaState L) { + int n = L.argCount(); + if (n == 0) { + throw L.libError("bad argument #1 to 'tonumber' (value expected)"); + } + LuaValue v = L.arg(0); + if (n == 1 || L.arg(1).isNil()) { + if (v.isNumber()) { + L.push(v); + return 1; + } + LuaValue num = Vm.strToNum(v); + if (num != null) { + L.push(num); + return 1; + } + L.pushNil(); + return 1; + } + // 带进制 + /* + * 三道校验的顺序照抄 C:{@code luaL_checkinteger(2)} → + * {@code luaL_checktype(1, LUA_TSTRING)} → {@code luaL_argcheck(2 <= base <= 36)}。 + * 范围检查在最后,所以 {@code tonumber(1, 1)} 报的是 + * {@code bad argument #1 (string expected, got number)} 而不是 + * {@code bad argument #2 (base out of range)} —— 曾经把范围检查放在 + * 中间,那两条全反了。 + */ + long base = checkInteger(L, 1); + LuaAux.checkType(L, 1, Lua.TSTRING); + LuaAux.argCheck(L, base >= 2 && base <= 36, 2, "base out of range"); + Long parsed = parseBase(v.asString().bytes, (int) base); + if (parsed == null) { + L.pushNil(); + } else { + L.push(LuaValue.ofInteger(parsed)); + } + return 1; + } + + /** + * {@code lbaselib.c: b_str2int}。 + *

+ * 必须在字节上做,而且必须回到长度上判「吃完了没有」——这两点 + * 缺一个就会把 {@code tonumber('1\0', 2)} 算成 1。 + *

+ * C 版靠的是「{@code s} 走到哪里」与 {@code s + l} 比:数字循环遇到 + * {@code \0}(不是 {@code isalnum})就停,后面的空格跳过一步也停,返回的 + * 指针还站在 {@code \0} 上,与 {@code s + l} 不等,于是 + * {@code luaB_tonumber} 判失败。早先这里用 {@code String.trim()} 去掉首尾 + * 空白——而 Java 的 {@code trim} 连 {@code \0} 一起去掉——这个差别直接让 + * 上游 {@code math.lua} 的 {@code assert(not f(tonumber('1\0', 2)))} + * 挂掉。 + *

+ * 另外数字只看 ASCII:C 版用的是 C locale 的 {@code isalnum},不是 + * {@code lislalnum}(后者把 {@code _} 也算字母),也不是 + * {@code Character.digit}(后者认全角数字)。 + * + * @return 解析出的整数;不是合法的整数字面量返回 {@code null} + */ + private static Long parseBase(byte[] b, int base) { + int len = b.length; + int p = 0; + while (p < len && LCType.isSpace(b[p] & 0xFF)) { + p++; // SPACECHARS + } + boolean neg = false; + if (p < len && b[p] == '-') { + p++; + neg = true; + } else if (p < len && b[p] == '+') { + p++; + } + if (p >= len || !isAsciiAlnum(b[p])) { + return null; // 没有数字 + } + long n = 0; + do { + int d = (b[p] >= '0' && b[p] <= '9') + ? b[p] - '0' + : ((b[p] >= 'a' && b[p] <= 'z') ? b[p] - 'a' : b[p] - 'A') + 10; + if (d >= base) { + return null; // 不是这个进制里的数字 + } + n = n * base + d; // 溢出按 Lua 的 int64 环绕处理,与 C 版一致 + p++; + } while (p < len && isAsciiAlnum(b[p])); + while (p < len && LCType.isSpace(b[p] & 0xFF)) { + p++; // 尾部空白 + } + if (p != len) { + return null; // 还有没吃掉的字节,包括内嵌的 \0 + } + return neg ? -n : n; + } + + /** C locale 的 {@code isalnum}:只认 ASCII 字母与数字,不含 {@code '_'} */ + private static boolean isAsciiAlnum(byte c) { + return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); + } + + /** + * {@code luaB_rawget}。 + *

+ * C 只做 {@code luaL_checktype(1, TABLE)} 与 {@code luaL_checkany(2)} + * 两条校验,没有「键不能是 nil」那条 —— {@code rawget({}, nil)} + * 是 {@code true nil}(nil 键走 {@code luaH_get} 落到 dummynode)。报 + * 「table index is nil」的是 {@code rawset},那条来自 + * {@code ltable.c: luaH_newkey}。 + *

+ * 曾经这里既漏了 {@code checkAny(2)},又自作主张加了 nil 键校验。 + */ + private static int rawget(LuaState L) { + LuaAux.checkType(L, 1, Lua.TTABLE); + LuaAux.checkAny(L, 2); + LuaValue t = L.arg(0); + LuaValue k = L.arg(1); + LuaValue v = t.asTable().get(k); + L.push((v == null) ? LuaValue.ofNil() : v); + return 1; + } + + /** {@code luaB_rawset} */ + private static int rawset(LuaState L) { + LuaAux.checkType(L, 1, Lua.TTABLE); + LuaAux.checkAny(L, 2); + LuaAux.checkAny(L, 3); + LuaValue t = L.arg(0); + t.asTable().set(L.arg(1), L.arg(2)); + L.push(t); + return 1; + } + + /** + * {@code luaB_rawequal}。 + *

+ * {@code luaL_checkany} 要做两遍(第 1、第 2 参数各一遍): + * {@code rawequal(1)} 报 {@code bad argument #2 ... (value expected)}。 + * 曾经一遍也没做,于是 {@code pcall(rawequal, 1)} 直接返回 {@code true true}。 + */ + private static int rawequal(LuaState L) { + LuaAux.checkAny(L, 1); + LuaAux.checkAny(L, 2); + L.push(LuaValue.ofBoolean(LuaValue.rawEqual(L.arg(0), L.arg(1)))); + return 1; + } + + /** + * {@code luaB_rawlen}:走 {@code luaL_argexpected},条件是「类型是表或字符串」。 + *

+ * 缺参时 {@code lua_type} 是 {@code LUA_TNONE},既不是表也不是字符串, + * 所以报 {@code got no value} —— 用 {@link LuaState#argTypeName} 取类型名, + * 它把越界翻译成 {@code no value}。 + */ + private static int rawlen(LuaState L) { + int t = L.argBaseType(1); + LuaAux.argExpected(L, t == Lua.TTABLE || t == Lua.TSTRING, 1, "table or string"); + LuaValue v = L.arg(0); + if (v.isTable()) { + L.push(LuaValue.ofInteger(v.asTable().length())); + return 1; + } + L.push(LuaValue.ofInteger(v.asString().length())); + return 1; + } + + /** {@code luaB_next} */ + private static int next(LuaState L) { + /* + * C 是 {@code luaL_checktype(1, TABLE)} 然后 {@code lua_settop(L, 2)} + * —— 第二个参数不给就补一个(补出来的是 nil),不是报错。 + */ + LuaAux.checkType(L, 1, Lua.TTABLE); + LuaValue t = L.arg(0); + LuaValue k = L.arg(1); + LuaValue[] kv; + try { + kv = t.asTable().next(k); + } catch (IllegalArgumentException e) { + /* + * 必须是 {@link LuaState#runError} 而不是 {@link LuaState#libError}。 + *

+ * C 里这句话在 {@code ltable.c: findindex},是 + * {@code luaG_runerror} 而不是 {@code luaL_error} —— 前者只在 + * {@code isLua(ci)} 时才加位置,而跑到这儿 {@code ci} 是 + * {@code luaB_next} 这个 C 帧,于是永远没有位置。 + * {@code libError} 走的是 {@code luaL_where(L, 1)},会往回跳一层 + * 拿到调用者的行号,于是凭空多出 {@code case.lua:N:}(约定 #8 那句 + * 「库函数一律 libError」在这里不成立——这条消息的源头不在库代码里)。 + *

+ * 实测:{@code next({}, 1)} 参考给 {@code invalid key to 'next'}, + * 本实现给 {@code case.lua:12: invalid key to 'next'}。 + */ + throw L.runError("invalid key to 'next'"); + } + if (kv == null) { + L.pushNil(); + return 1; + } + L.push(kv[0]); + L.push(kv[1]); + return 2; + } + + /** + * {@code lbaselib.c: pairscont}。 + *

+ * yield 恢复之后被调用,三个返回值(迭代器、状态、初值)那时候已经在栈上, + * 这里只要报出「有 3 个结果」。 + */ + private static final LuaKFunction PAIRS_CONT = (L, status, ctx) -> 3; + + /** + * {@code luaB_pairs}(含 {@code __pairs} 元方法)。 + *

+ * C 只做 {@code luaL_checkany(L, 1)}——不要求是表。没有 + * {@code __pairs} 时照样返回 {@code (next, 值, nil)},于是 + * {@code pairs(5)} 是三个返回值而不是报错;真正炸是在迭代时 + * {@code next(5)} 报 {@code bad argument #1 to 'next' (table expected, got number)}。 + * 曾经这里多加了「必须是表」的校验,把 {@code pairs(5)} 变成报错。 + */ + private static int pairs(LuaState L) { + LuaAux.checkAny(L, 1); + LuaValue t = L.arg(0); + LuaValue pf = metamethodByName(L, t, "__pairs"); + if (pf != null) { + int func = L.top; + L.ensureSlots(func + 2); + L.stack[func].set(pf); + L.stack[func + 1].set(t); + L.top = func + 2; + /* + * {@code lua_callk(L, 1, 3, 0, pairscont)}:带续体,所以 + * {@code __pairs} 里 yield 是允许的(上游 nextvar.lua 有这一条)。 + * 用不带续体的 {@code call} 会在恢复时炸,因为那时 {@code ci.k} 是 + * {@code null}。 + */ + L.callK(func, 3, 0, PAIRS_CONT); + return 3; + } + L.pushFunction(BaseLib::next); + L.push(t); + L.pushNil(); + return 3; + } + + /** {@code luaB_ipairs}:与 {@code pairs} 一样只 {@code luaL_checkany} */ + private static int ipairs(LuaState L) { + LuaAux.checkAny(L, 1); + L.pushFunction(BaseLib::ipairsAux); + L.push(L.arg(0)); + L.push(LuaValue.ofInteger(0)); + return 3; + } + + /** + * {@code ipairsaux}。 + *

+ * 这里有一个容易写反的地方:C 函数的 n 个返回值是「栈顶最后 n 个」, + * 不是「参数之后的前 n 个」。所以先压 {@code i}、再压 {@code t[i]},然后 + * {@code return 1}——取到的是刚压进去的那个值(结束时是 {@code nil}), + * 而不是 {@code i}。写成「先压 i 就返回 i」的直觉版本,泛型 for 会看到 + * 首值非 nil 而永不终止:参考实现里 {@code ipairs} 能停住,靠的 + * 就是这个「取最后 n 个」的约定,不是靠判 nil 提前返回。 + */ + private static int ipairsAux(LuaState L) { + long i = checkInteger(L, 1) + 1; + LuaValue t = L.arg(0); + L.push(LuaValue.ofInteger(i)); + int vIdx = L.top; + L.pushNil(); + Vm.getTable(L, t, LuaValue.ofInteger(i), vIdx); + return L.stack[vIdx].isNil() ? 1 : 2; + } + + /** + * {@code luaB_select}。 + *

+ * 两处容易写错,都是实测出来的: + *

    + *
  • {@code #} 只比第一个字节:C 是 + * {@code *lua_tostring(L, 1) == '#'},所以 {@code select("#abc", 1)} + * 也是「数个数」,返回 1 —— 判断整串等于 {@code "#"} 就漏了。
  • + *
  • 缺参报的是 {@code number expected, got no value}:没有 + * 「至少一个参数」那条检查,缺参直接落进 + * {@code luaL_checkinteger(L, 1)}。曾经先判 {@code n == 0} 报 + * {@code value expected},跟参考实现对不上。
  • + *
+ */ + private static int select(LuaState L) { + int n = L.argCount(); + LuaValue first = L.arg(0); + if (first.isString() && first.asString().length() > 0 + && first.asString().byteAt(0) == '#') { + L.push(LuaValue.ofInteger(n - 1)); + return 1; + } + long i = checkInteger(L, 0); + if (i < 0) { + i = n + i; + } else if (i > n) { + i = n; + } + LuaAux.argCheck(L, i >= 1, 1, "index out of range"); + int start = L.ci.func + 1 + (int) i; // 绝对下标:跳过前 i 个参数 + int count = n - (int) i; + for (int j = 0; j < count; j++) { + L.push(L.stack[start + j]); + } + return count; + } + + /** + * {@code luaB_error}。 + *

+ * 必须照 C 版的形状写,尤其那句 {@code lua_settop(L, 1)}——它不是顺手的 + * 清理,而是决定「栈顶之下那一格是什么」。 + *

+ * 实测({@code coroutine.wrap} 出来的函数崩掉时): + * {@code luaB_auxwrap} 在错误分支里会先 {@code lua_xmove(co, L, 1)} 把错误 + * 对象搬走,再 {@code lua_closethread} → {@code luaE_resetthread},后者 + * 走 {@code luaD_seterrorobj(L, status, L->stack + 1)},而 + * {@code seterrorobj} 的 {@code default} 分支是 + * {@code setobjs2s(L, oldtop, L->top - 1)}——搬的是「搬走一格之后」的栈顶。 + * 只有 {@code settop(1)} + {@code where} + {@code pushvalue} + {@code concat} + * 这套写法能让那一格正好是带位置的错误对象({@code concat} 的结果把参数格 + * 覆盖掉,于是「错误对象」在栈上是连着两格的)。少了 {@code settop},那一格 + * 还是原始的 {@code "boom"},报出来就成了没有 {@code file:line:} 前缀的裸串。 + */ + private static int error(LuaState L) { + int n = L.argCount(); + // 注意没有「至少一个参数」的检查:error() 等价于 error(nil), + // 参考实现实测 print(pcall(error)) 得到 false nil。 + long level = (n > 1 && !L.arg(1).isNil()) ? checkInteger(L, 1) : 1; + L.setTop(1); // lua_settop(L, 1):多出来的参数全部摘掉 + LuaValue msg = L.arg(0); + if (msg.isString() && level > 0) { + L.push(LuaValue.ofString(L.whereAt((int) level))); // luaL_where(字节串,别转 Java String) + L.push(msg); // lua_pushvalue(L, 1) + Vm.concat(L, 2); // lua_concat(L, 2) + } + // lua_error(L):抛栈顶那个值。位置已经拼进去了,errorAt 的 level 传 0 + // 表示「别再加一次」——但 where 字段仍非 null,pcall 也不会补。 + throw L.errorAt(L.stack[L.top - 1], 0); + } + + /** + * {@code luaB_assert}。 + *

+ * 校验的位置照抄 C:{@code luaL_checkany} 在条件为假那一支里, + * 不在函数开头。{@code assert()} 走的是「条件假 → 报 value expected」。 + * 曾经写成「一个参数也没有就报」,措辞对、位置不对 —— 而真正咬人的是 + * {@code L.arg(0)} 会读到残值:残值为真时 {@code assert()} 直接返回 0 个值。 + */ + private static int assertFn(LuaState L) { + int n = L.argCount(); + if (L.arg(0).toBoolean()) { + for (int i = 0; i < n; i++) { + L.push(L.arg(i)); + } + return n; + } + LuaAux.checkAny(L, 1); + LuaValue msg = (n > 1) ? L.arg(1) + : LuaValue.ofString(LuaString.latin1("assertion failed!")); + throw L.errorAt(msg, 1); + } + + /** {@code luaB_pcall} */ + private static int pcall(LuaState L) { + LuaAux.checkAny(L, 1); + return doPcall(L, false); + } + + /** + * {@code luaB_xpcall}。 + *

+ * 第一件事是 {@code luaL_checktype(L, 2, LUA_TFUNCTION)} —— 消息处理函数 + * 必须是函数,而且这条检查在调用被调函数之前就发生。曾经写成 + * 「只判参数够不够两个」,于是 {@code xpcall(f, 5)} 不报错,而是照常调用 + * {@code f}:{@code f} 抛错后拿 5 当处理函数,得到 + * {@code true, false, "error in error handling"};参考实现给的是 + * {@code false bad argument #2 to 'xpcall' (function expected, got number)}。 + * 参数不足时同理:报 {@code function expected, got no value}, + * 而不是含糊的 {@code value expected}。 + */ + private static int xpcall(LuaState L) { + LuaAux.checkType(L, 2, Lua.TFUNCTION); + return doPcall(L, true); + } + + /** + * pcall / xpcall 的公共部分。 + *

+ * 布局按 {@code lbaselib.c} 来:先在结果区起点塞 {@code true},再把被调函数 + * 与它的参数整体上移一格,于是调用结果自然接在 {@code true} 后面,返回时数 + * {@code L.top - start} 就得到「true + 全部结果」。 + *

+ * xpcall 的坑在消息处理函数夹在被调函数和参数中间 + * (原始布局 {@code [f, msgh, args...]})。必须先把它摘出来、把参数左移一格, + * 否则它会变成传给 {@code f} 的第一个实参——表现是处理函数收到的 + * 错误对象是 {@code nil}。参考实现用 {@code lua_rotate(L, 3, 2)} 把 + * {@code true} 和 {@code f} 转到参数前面,这里直接删掉中间那格,等价且更好读。 + */ + private static int doPcall(LuaState L, boolean hasHandler) { + int base = L.ci.func; + CallInfo myCi = L.ci; // 本函数自己的帧;yield 时要往上面挂续体 + LuaValue handler = null; + if (hasHandler) { + handler = new LuaValue(L.stack[base + 2]); + // 抹掉 msgh:参数整体左移一格 + for (int p = base + 2; p < L.top - 1; p++) { + L.stack[p].set(L.stack[p + 1]); + } + L.top--; + } + int start = base + 1; // 结果区起点,当前放着被调函数 + L.ensureSlots(L.top + 1); + for (int p = L.top; p > start; p--) { + L.stack[p].set(L.stack[p - 1]); + } + L.top++; + L.stack[start].setBoolean(true); + int f = start + 1; + try { + LuaValue err = L.pcall(f, LuaState.MULTRET, handler); + if (err == null) { + return L.top - start; + } + L.top = start; + L.push(LuaValue.ofBoolean(false)); + L.push(err); + return 2; + } catch (LuaYield y) { + /* + * pcall 可以 yield —— 实测参考实现就是这样。但 yield 是异常抛出去的, + * Java 栈帧到这里就没了,「收尾」只能交给续体。 + * 这里做的事等价于 C 版 lua_pcallk 里那段 + * 「ci->u.c.k = k; ci->callstatus |= CIST_YPCALL; ...」: + * 记下恢复点,让 resume 之后的 unroll 能找到它。 + */ + myCi.yieldPcall = true; + myCi.k = PCALL_CONT; + myCi.ctx = start; + myCi.funcIdx = f; + myCi.errHandler = handler; + /* + * 两种「被打断」必须分开记,因为 resume 之后 {@code finishPcallK} + * 只看 {@code recst}: + *

    + *
  • 被保护的函数正常跑着就 yield 了 → {@code LUA_OK},续体原样 + * 把结果交给调用方。
  • + *
  • 是出错后关 tbc 变量时 yield 的 → 错误还没交出去, + * 续体要接着把剩下的变量关完,再返回 {@code false, err}。 + * 这就是 {@code finishpcallk} 里 {@code recst} 记着 + * {@code LUA_ERRRUN} 的含义。
  • + *
+ * {@link LuaState#closeErrPending} 非空就说明是后者(错误对象和 + * 「还有变量没关完」是同一个条件,见那里的注释)。 + */ + myCi.recst = (L.closeErrPending != null) ? LuaState.LUA_ERRRUN : LuaState.LUA_OK; + throw y; + } + } + + /** + * {@code lbaselib.c: finishpcall} —— pcall 的续体。 + *

+ * {@code ctx} 是结果区起点(就是 {@code true} 那一格),错误对象由 + * {@code finishPcallK} 放在 {@code ctx + 1}。 + */ + private static final LuaKFunction PCALL_CONT = (L, status, ctx) -> { + if (status != LuaState.LUA_OK && status != LuaState.LUA_YIELD) { + LuaValue err = new LuaValue(L.stack[ctx + 1]); + L.top = ctx; + L.push(LuaValue.ofBoolean(false)); + L.push(err); + return 2; + } + return L.top - ctx; + }; + + /** {@code luaB_setmetatable} */ + private static int setmetatable(LuaState L) { + LuaAux.checkType(L, 1, Lua.TTABLE); + /* + * 第二个参数走 {@code luaL_argexpected},措辞是 + * 「nil or table expected, got 实际类型」——那个 {@code got} 不能省, + * 省了就成了裸的「nil or table expected」。 + * 条件是 {@code lua_type(2)} 属于 {nil, table}:缺参是 TNONE, + * 不属于,所以 {@code setmetatable({})} 报错,报的是 {@code got no value}。 + */ + int t2 = L.argBaseType(2); + LuaAux.argExpected(L, t2 == Lua.TNIL || t2 == Lua.TTABLE, 2, "nil or table"); + LuaValue t = L.arg(0); + LuaValue mt = L.arg(1); + if (metamethodByName(L, t, "__metatable") != null) { + throw L.libError("cannot change a protected metatable"); + } + t.asTable().setMetatable(mt.isNil() ? null : mt.asTable()); + // __gc 只在设元表的当下登记一次,之后往元表里塞 __gc 不生效(实测参考实现如此) + t.asTable().markFinalizable(L); + L.push(t); + return 1; + } + + /** {@code luaB_getmetatable} */ + private static int getmetatable(LuaState L) { + LuaAux.checkAny(L, 1); + LuaValue v = L.arg(0); + LuaValue protect = metamethodByName(L, v, "__metatable"); + if (protect != null) { + L.push(protect); + return 1; + } + LuaTable mt = LuaTm.metatableOf(L, v); + if (mt == null) { + L.pushNil(); + } else { + L.push(LuaValue.ofTable(mt)); + } + return 1; + } + + /** + * {@code luaB_collectgarbage} 的 {@code opts[]},顺序与 {@code luaB_collectgarbage} + * 里那个 {@code switch} 的下标严格对应。 + */ + private static final String[] GC_OPTS = { + "stop", "restart", "collect", "count", "step", "setpause", + "setstepmul", "isrunning", "generational", "incremental" + }; + + /** + * {@code luaB_collectgarbage}。 + *

+ * {@code "collect"} 分支做两件事:请求 JVM 做一次完整收集,然后递增 + * 「收集轮次」——弱表靠轮次决定什么时候清扫失效条目,这一点与 C 版一致 + * (Lua 是在 GC 周期末尾清弱表,不是键一不可达就立刻消失)。 + *

+ * 不假装统计内存:{@code collectgarbage("count")} 一律报 {@code 0.0}。 + * 报任何具体数字都是谎报(我们连自己的堆都摸不到,更没有「收集之后降了多少」 + * 这种量),宁可让依赖它的脚本拿到 0。曾试过报 JVM 堆的已用量,看起来 + * 「更真」,但上游 {@code gc.lua} 里 {@code assert(gcinfo() < x)} 仍然 + * 恒假(一百张小表对堆的影响测不出来),而 {@code repeat until gcinfo() > 3*x} + * 那种循环还可能一直涨不到——不如恒 0 那样确定:到那儿就停,不会 hang。 + * 其余选项只保证彼此自洽({@code stop} 之后 {@code isrunning} + * 为假、{@code generational} 返回切换前的模式名)。 + */ + private static int collectgarbage(LuaState L) { + /* + * {@code luaL_checkoption}:必须先校验选项。 + *

+ * 这一句不是为了挑刺——上游 {@code errors.lua} 的 + * {@code (collectgarbage or print){}} 断言的就是 + * 「bad argument #1 to 'collectgarbage' (string expected, got table)」。 + * 少校验,那句里的 {@code pcall} 会成功返回, + * {@code checkmessage} 拿到 {@code false} 去 {@code string.find}, + * 整个文件就死在一条跟它无关的消息上。 + */ + int o = LuaAux.checkOption(L, 1, "collect", GC_OPTS); + switch (o) { + case 3: // "count" + L.push(LuaValue.ofFloat(0.0)); + return 1; + case 4: // "step" + /* + * C 版的 {@code LUA_GCSTEP} 只推进一步,本实现没有步子可迈, + * 就照一次完整收集来做。这不是偷懒:弱表与 {@code __gc} + * 的清理时机全挂在「收集轮次」上,{@code "step"} 不做任何事的话, + * 上游 {@code gengc.lua} 那串「弱表该被清掉」的断言就全红—— + * 而它们红在一个跟 {@code collectgarbage} 无关的地方。 + */ + doCollect(L); + L.push(LuaValue.ofBoolean(true)); + return 1; + case 5: // "setpause" + case 6: { // "setstepmul" + LuaState m = L.mainThread; + int old = (o == 5) ? m.gcPause : m.gcStepMul; + long v = LuaAux.optInteger(L, 2, 0); + if (v != 0) { + if (o == 5) { + m.gcPause = (int) v; + } else { + m.gcStepMul = (int) v; + } + } + L.push(LuaValue.ofInteger(old)); + return 1; + } + case 7: // "isrunning" + L.push(LuaValue.ofBoolean(L.mainThread.gcRunning)); + return 1; + case 8: // "generational" + case 9: { // "incremental" + /* + * {@code lbaselib.c: pushmode}:这两个选项返回的是切换前的 + * 模式,而且是字符串({@code "incremental"} / + * {@code "generational"}),不是 {@code LUA_GCINC} 那个整数。 + * 上游 {@code gc.lua} 开头四行就是照着字符串断言的,返回整数的话 + * 它连「testing incremental garbage collection」都打不完。 + */ + LuaState m = L.mainThread; + /* + * 判据是 {@code isdecGCmodegen(g)}({@code gckind == KGC_GEN || + * lastatomic != 0})而不是 {@code gckind} 本身 —— 见 + * {@link LuaState#gcLastAtomic}。两种切换都会把 {@code lastatomic} + * 清 0,所以切过一次之后就只看 {@code gckind} 了。 + */ + boolean prevGen = (m.gcKind == LuaState.LUA_GCGEN) || m.gcLastAtomic; + m.gcKind = (o == 8) ? LuaState.LUA_GCGEN : LuaState.LUA_GCINC; + m.gcLastAtomic = false; + L.push(LuaValue.ofString(LuaString.latin1( + prevGen ? "generational" : "incremental"))); + return 1; + } + case 0: // "stop" + L.mainThread.gcRunning = false; + L.push(LuaValue.ofInteger(0)); + return 1; + case 1: // "restart" + L.mainThread.gcRunning = true; + L.push(LuaValue.ofInteger(0)); + return 1; + default: + break; // "collect" + } + doCollect(L); + L.push(LuaValue.ofInteger(0)); + return 1; + } + + /** + * 一次「完整收集」在本实现里的等价物:清栈死区 → {@code System.gc()} → + * 催终结 → 排空待终结队列 → 递增收集轮次。 + *

+ * 先把所有栈的「死区」清掉,再请求收集。顺序不能反: + * {@code lgc.c: traversethread} 的 atomic 分支做的正是这件事—— + * mark 只扫 {@code [stack, top)},扫完把 {@code [top, stack_last)} + * 整段置 nil。也就是说 {@code top} 之上的槽在 Lua 里对 GC 不可见。 + *

+ * 这里必须显式做,因为 {@code stack} 是一条 Java 数组:数组里每个槽 + * 对 JVM 都是活引用。不清的话,凡是「寄存器落在 {@code top} 之上」的 + * 对象都收不掉——循环里最后一次迭代建的表就挂在这样的寄存器上 + * ({@code for i = 1, 5 do t[i] = {i} end} 之后 {@code {5}} 还留在 + * 寄存器里,而 {@code top} 只到调用参数处),于是弱表清不掉它。 + */ + private static void doCollect(LuaState L) { + L.clearDeadStackSlices(); + System.gc(); + /* + * 光 gc 不够:不可达对象的 {@code finalize} 是由 JVM 的终结线程跑的, + * 不主动催一下,哨兵还没入队我们就往下走了。{@code runFinalization} 会 + * 尽量把队列排干,之后才能排空待终结队列。 + */ + System.runFinalization(); + L.mainThread.drainFinalizers(L); + LuaTable.gcGeneration++; + } + + /* ==================== 加载 ==================== */ + + /** {@code LUA_SIGNATURE} 的第一个字节;以它开头的就是二进制 chunk */ + private static final int SIGNATURE0 = Lua.LUA_SIGNATURE0; + + /** + * {@code luaB_load}。 + *

+ * 二进制 chunk 目前只能挡下来——{@code luaU_undump} 还没搬,硬吃掉会静默 + * 编出一个错误的 chunk。挡的时候照 C 的 {@code checkmode} 先报模式错, + * 模式没错才报「不支持」,这样 mode 的语义先对上。 + */ + private static int loadFn(LuaState L) { + LuaValue a = L.arg1(1); + // checkmode 比的是 const char*:load(src, name, "t\0b") 的模式是 "t" + // mode 会原样拼进错误消息(mode is 'x'),Latin-1 往返才不丢字节 + String mode = LuaAux.optString(L, 3, LuaString.latin1("bt")).toCString().toLatin1String(); + int env = L.isNoneArg(4) ? 0 : 4; + // lua_tolstring:数字也算字符串,load(42) 编的是 "42" + LuaString src = (a.isString() || a.isNumber()) ? LuaAux.asString(a) : null; + if (src != null) { + LuaString chunkname = LuaAux.optString(L, 2, src); + return loadAux(L, loadBuffer(L, src.bytes, chunkname.bytes, mode), env); + } + // 否则第一个参数必须是读函数 + LuaString chunkname = LuaAux.optString(L, 2, LuaString.latin1("=(load)")); + LuaAux.checkType(L, 1, Lua.TFUNCTION); + Zio.Source readerSrc = readerSource(L, a); + return loadAux(L, loadZio(L, new Zio(readerSrc), chunkname.bytes, mode), env); + } + + /** + * {@code generic_reader}:把 Lua 侧的读函数包成一条惰性字节流。 + *

+ * 必须是惰性的。先一股脑读完再解析,遇到永不返回 nil 的读函数就 + * 会一直累积;而 C 那边是边解析边要,语法错误一冒出来就停了。 + *

+ * 返回 nil 表示结束;返回非字符串非数字要报 + * {@code reader function must return a string}(这里用 + * {@code lua_isstring} 判,所以数字是合法的)。 + */ + private static Zio.Source readerSource(LuaState L, LuaValue reader) { + return () -> { + int f = L.top; + L.ensureSlots(f + 2); + L.stack[f].set(reader); + L.top = f + 1; + L.callNoYield(f, 1); /* lbaselib.c: generic_reader 用 lua_call —— 不可 yield */ + LuaValue r = new LuaValue(L.stack[f]); + L.top = f; + if (r.isNil()) { + return null; // 读函数返回 nil:流结束 + } + if (!(r.isString() || r.isNumber())) { + throw L.libError("reader function must return a string"); + } + return LuaAux.asString(r).bytes; + }; + } + + /** + * 编译一段源码。 + * + * @return {@code null} 表示成功(闭包已压栈);否则是那条加载期错误 + */ + private static LuaError loadBuffer(LuaState L, byte[] src, byte[] chunkname, String mode) { + return loadZio(L, new Zio(src), chunkname, mode); + } + + /** {@code lbaselib.c: checkmode} —— 结尾是 {@code luaD_throw(LUA_ERRSYNTAX)},直跳 */ + private static LuaError modeError(String what, String mode) { + LuaError e = new LuaError(LuaValue.ofString(LuaString.latin1( + "attempt to load a " + what + " chunk (mode is '" + mode + "')"))); + e.directThrow = true; + return e; + } + + private static LuaError loadZio(LuaState L, Zio z, byte[] chunkname, String mode) { + /* + * 整段都必须在 try 里,包括最前面那次窥探。读函数是惰性的, + * 第一个字节就要调它一次;而读函数里的错误(返回了非字符串、或自己 + * error 了)在 C 那边是落在 luaD_protectedparser 的保护区里, + * 于是 load 返回 nil + 消息,不抛。窥探写在外面就会让 + * "reader function must return a string" 变成抛出去的错误。 + */ + try { + int first = z.getc(); + if (first == SIGNATURE0) { + if (mode.indexOf('b') < 0) { + throw modeError("binary", mode); + } + /* + * 二进制分支不回退首字节:C 的 luaU_undump 的注释写得很直白, + * "skip 1st char (already read and checked)"——头部的签名首字节 + * 已经归 f_parser 了,checkHeader 从第二个字节开始比。 + * 文本分支则要回退(或把首字符交给 parser),否则会吃掉源码第一个字节。 + */ + L.push(LuaValue.ofClosure(L.undump(z, chunkname))); + return null; + } + if (mode.indexOf('t') < 0) { + throw modeError("text", mode); + } + /* + * 首字节传进去而不是回退。回退看着等价,但对惰性读函数不等价: + * 词法器起步时会再 {@code getc()} 一次,而那一次会真的再调一遍读 + * 函数。首字节是 EOZ 时差别最大——参考实现就此认定流已结束, + * 这里却会重新去要下一批。 + */ + L.push(LuaValue.ofClosure(L.load(z, chunkname, first))); + return null; + } catch (LuaError e) { + /* + * 整段 try 对应 C 的 {@code luaD_protectedparser},而它把 + * {@code L->errfunc} 原样传进内层的 {@code luaD_pcall}。 + * 于是「不是 {@code luaD_throw} 直跳的错」在抛出点就被外层 + * {@code xpcall} 的处理器接住了 —— 实测 + * {@code xpcall(function() return load(function() error("boom") end) end, H)} + * 参考实现给 {@code nil, "H:case.lua:13: boom"};语法错误因为走 + * {@code luaD_throw} 直跳,给的是裸消息,两种都得对上。 + */ + if (!e.directThrow && L.errFunc != null) { + return new LuaError(L.applyErrFunc(e.value), e.where); + } + return e; + } + } + + /** {@code load_aux}:成功就(可能)设上 {@code _ENV} 返回 1 个值,失败给 nil + 消息 */ + private static int loadAux(LuaState L, LuaError err, int envidx) { + if (err == null) { + if (envidx != 0) { + L.pushValue(envidx); + LuaValue f = L.stack[L.top - 2]; + if (f.tt == Lua.VLIF) { + LClosure cl = (LClosure) f.o; + if (cl.upvals.length >= 1 && cl.upvals[0] != null) { + cl.upvals[0].value().set(L.stack[L.top - 1]); + } + } + L.top--; // 无论设没设上,env 都要弹掉 + } + return 1; + } + LuaAux.pushFail(L); + L.push(err.value == null ? LuaValue.ofNil() : err.value); + return 2; + } + + /** {@code luaB_loadfile} */ + private static int loadfileFn(LuaState L) { + LuaString fname = LuaAux.optString(L, 1, null); + LuaString modeArg = LuaAux.optString(L, 2, null); + // luaL_loadfilex 的 mode 是 NULL 时内部按 "bt" 处理 + String mode = (modeArg == null) ? "bt" : modeArg.toCString().toLatin1String(); + int env = L.isNoneArg(3) ? 0 : 3; + if (fname == null) { + // 无参:从 stdin 读。chunk 名是 "=stdin" + byte[] src; + try { + src = L.in.readAllBytes(); + } catch (java.io.IOException e) { + LuaAux.pushFail(L); + L.push(LuaValue.ofString(LuaString.latin1("cannot read stdin"))); + return 2; + } + return loadAux(L, loadBuffer(L, src, LuaString.latin1("=stdin").bytes, mode), env); + } + // luaL_loadfilex 的 filename 是 const char*:第一个 \0 之后的内容既进不了 + // fopen,也进不了 "cannot open ..." 那条消息 + String path = fname.toCString().toJavaString(); + byte[] src; + try { + // luaL_loadfilex 会先吃掉 BOM 与首行的 #!...;这一步属于加载层, + // 用 load 加载字符串时首行写 '#' 照样报 unexpected symbol + src = Parser.skipFilePreamble( + java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(path))); + } catch (Exception e) { + /* + * 空路径是 C 与 Java 唯一判不一致的一处:{@code fopen("")} 给 + * {@code ENOENT}(实测 {@code loadfile("")} 报 + * {@code cannot open : No such file or directory}),而 Java 把空路径 + * 解析成当前目录、读的时候给 {@code Is a directory}。 + *

+ * 目录走的是另一条分支,之前这里是错的:{@code loadfile("/tmp")} + * 在 C 里 fopen 成功、失败在读,{@code luaL_loadfilex} 的 + * {@code ferror} 于是为真,报的是 + * {@code cannot read /tmp: Is a directory} —— 不是 + * {@code cannot open ...: Input/output error}。实测(参考二进制): + *

+             * loadfile(dir) -> nil, "cannot read /tmp/fkdir/adir.lua: Is a directory"
+             * 
+ */ + boolean isDir = !path.isEmpty() && new java.io.File(path).isDirectory(); + int en = path.isEmpty() ? 2 + : isDir ? 21 + : LuaAux.errnoOf((e instanceof java.io.IOException) + ? (java.io.IOException) e : null); + LuaAux.pushFail(L); + L.push(LuaValue.ofString(LuaString.latin1( + (isDir ? "cannot read " : "cannot open ") + path + ": " + + LuaAux.strError(en)))); + return 2; + } + /* + * luaL_loadfilex 的 chunk 名是 "@" + 文件名。 + * 加载期错误打印时 luaO_chunkid 会把它剥掉。 + */ + return loadAux(L, loadBuffer(L, src, LuaString.utf8("@" + path).bytes, mode), env); + } + + /** + * {@code lbaselib.c: dofilecont}:yield 恢复后,结果已经在栈上,只要报出个数。 + *

+ * C 版算的是 {@code lua_gettop(L) - 1}(栈顶减去「帧底 + 1」再减去 chunk + * 本身那一格)。这里直接把 chunk 的下标当 {@code ctx} 传进来,省得再推一遍 + * ——{@code ctx} 在 {@code lua_callk} 里本来就是给续体用的。 + */ + private static final LuaKFunction DOFILE_CONT = (L, status, ctx) -> L.top - ctx; + + /** + * {@code luaB_dofile}。 + *

+ * 注意它不吞加载错误:{@code dofile("nope.lua")} 直接把错误抛出去, + * 而不是像 {@code loadfile} 那样给 {@code nil, msg}。 + */ + private static int dofile(LuaState L) { + LuaString fname = LuaAux.optString(L, 1, null); + L.setTop(1); + int n = loadfileFn(L); + if (n == 2) { + // 加载失败:栈顶那条消息要当错误抛出去,不是当返回值交出去 + LuaValue msg = new LuaValue(L.stack[L.top - 1]); + L.top -= 2; + throw new LuaError(msg); + } + int f = L.top - 1; // 闭包在栈顶,0 个参数 + /* + * {@code lua_callk(L, 0, LUA_MULTRET, 0, dofilecont)}:在 dofile 出来的 + * chunk 里 yield 是允许的,所以这里要带续体。续体什么也不做,直接沿用 + * 恢复时栈上已有的结果个数。 + */ + L.callK(f, LuaState.MULTRET, f, DOFILE_CONT); + return L.top - f; + } + + /** + * {@code lbaselib.c: luaB_warn}。 + *

+ * 两条不显眼的约束,都是逐句照 C 抄出来的,凭记忆推会推反: + *

    + *
  • 先把参数全查完,再开始拼消息。C 的注释就写在函数头上: + * 「Check first for errors; otherwise an error may interrupt the + * composition of a warning, leaving it unfinished」。所以 + * {@code warn("PARTIAL", {})} 一条警告都不吐,而不是先吐半句再炸—— + * 实测参考实现的 stderr 上确实什么都没有。
  • + *
  • 参数是 C 字符串。循环里用的是 {@code lua_tostring},拿到的是 + * {@code const char*},所以 {@code warn("a\0b")} 只写 {@code a}。 + * 这条还连带影响控制消息:{@code warn("@on\0x")} 截断后是 {@code "@on"}, + * 能把警告打开;不截断就变成未知控制消息被吃掉。
  • + *
+ *

+ * 数字参数是允许的({@code luaL_checkstring} 底下是 + * {@code lua_tolstring},数字会被转成字符串):{@code warn(1, 2.5, "x")} + * 参考实现输出 {@code Lua warning: 12.5x}。 + */ + private static int warn(LuaState L) { + int n = L.getTop(); + if (n < 1) { + /* + * C 版循环之前单独有一句 {@code luaL_checkstring(L, 1)}:一个参数 + * 都没给时这个检查照样发生,报的是 + * 「bad argument #1 to 'warn' (string expected, got no value)」。 + * 只写 {@code for i = 1..n} 的话 n==0 时循环一次都不进,这条报错 + * 就没了,而 {@code warn()} 会静默成功。 + */ + LuaAux.checkString(L, 1); + } + /* + * 存 {@link LuaString} 而不是 {@code String}:{@code lua_tostring} 拿到的是 + * {@code const char*},截断发生在字节上,而 {@code toJavaString} 会先按 + * UTF-8 解码——{@code warn("\xff")} 必须原样写出 {@code 0xFF}。 + */ + LuaString[] parts = new LuaString[n]; + for (int i = 1; i <= n; i++) { + parts[i - 1] = LuaAux.checkString(L, i); + } + for (int i = 0; i < n - 1; i++) { + L.warning(parts[i], true); + } + L.warning(parts[n - 1], false); + return 0; + } + + /* ==================== 小工具 ==================== */ + + private static void setFn(LuaTable t, String name, LuaFunction f) { + t.set(name, LuaValue.ofFunction(f)); + } + + /** + * {@code luaL_checkinteger} 的 0 下标形态,直接转调 {@link LuaAux#checkInteger}。 + *

+ * 这里不再自己实现一份。曾经有两份,而这一份少了两样东西,四个调用点 + * ({@code tonumber} 的进制、{@code select} 的下标、{@code error} 的层号、 + * {@code ipairs} 的迭代器下标)全都跟着错: + *

    + *
  • 函数名前缀:{@code bad argument #2 (...)} 而参考实现是 + * {@code bad argument #2 to 'tonumber' (...)}——前缀来自 + * {@code lauxlib.c: luaL_argerror} 里的 {@code pushglobalfuncname}。
  • + *
  • 「无整数表示」这一档:{@code 1.5} 报成了 + * {@code number expected, got number}(数字说自己是数字,读着就不对), + * 参考实现报 {@code number has no integer representation}。
  • + *
+ * 这是「同一个 C 函数两份实现」的老毛病,和 {@code Vm.formatG} / + * {@code StringLib.generalDigits} 那次是同一类。 + */ + static long checkInteger(LuaState L, int i) { + return LuaAux.checkInteger(L, i + 1); + } + + /** 把 {@link LuaValue} 变成函数值(供外部注册额外库函数) */ + public static LuaValue functionValue(LuaFunction f) { + return LuaValue.ofFunction(f); + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/CClosure.java b/src/main/java/com/github/esrrhs/fakescript/lua/CClosure.java new file mode 100644 index 0000000..dbac218 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/CClosure.java @@ -0,0 +1,32 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * C 闭包 —— 对应 {@code lobject.h} 的 {@code CClosure}。 + *

+ * 与 Lua 闭包({@link LClosure})的区别只有一个:绑定的是值而不是 + * upvalue 槽。C 版 {@code CClosure} 里直接放着 {@code TValue upvalue[]}, + * 因为 C 函数没有栈上的局部变量可供开放 upvalue 指向,绑成值就够了。 + *

+ * 现存代码里第一个真正需要它的是 {@code coroutine.wrap}: + * {@code lcorolib.c} 用 {@code lua_pushcclosure(L, luaB_auxwrap, 1)} 把协程 + * 对象绑进闭包,之后每次调用 wrap 出来的函数都从 upvalue 里取回那个协程。 + * 用全局变量或表来存都不行——那样两个 wrap 会互相串。 + *

+ * 放入 {@link LuaValue} 时用标签 {@link Lua#VCCL}。 + */ +public final class CClosure { + + public final LuaFunction f; + + /** 绑定上来的值,长度在构造时固定 */ + public final LuaValue[] upvalue; + + public CClosure(LuaFunction f, int nupvals) { + LuaState.chargeGc(); + this.f = f; + this.upvalue = new LuaValue[nupvals]; + for (int i = 0; i < nupvals; i++) { + this.upvalue[i] = LuaValue.ofNil(); + } + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/CallInfo.java b/src/main/java/com/github/esrrhs/fakescript/lua/CallInfo.java new file mode 100644 index 0000000..e5f9d80 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/CallInfo.java @@ -0,0 +1,147 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * 调用帧 —— 对应 {@code lstate.h} 的 {@code CallInfo}。 + *

+ * C 版用指针({@code func.p} / {@code top.p})在栈上定位,这里换成栈下标。 + * 等价性依赖于 {@link LuaState} 的不变式:栈扩容时只复制数组、不替换槽对象, + * 所以下标与「槽对象引用」两种定位方式在语义上一致。 + *

+ * C 版把 {@code u.l.savedpc} 与 {@code u.c.*} 放在一个 union 里区分 Lua / C 帧, + * 这里摊平成字段,用 {@link #isLua} 区分含义。 + */ +public final class CallInfo { + + /** 被调用函数在栈上的下标;参数紧跟其后 */ + int func; + + /** 本帧可用的栈顶(下标),对应 {@code ci->top.p} */ + int top; + + /** 调用方期望的结果数;{@link LuaState#MULTRET} 表示「全部」 */ + int nresults; + + /* ---- Lua 帧 ---- */ + + /** 下一条待执行指令的 pc(对应 {@code u.l.savedpc}) */ + int savedpc; + + /** 可变参数里「额外参数」的个数({@code u.l.nextraargs}) */ + int nextraargs; + + /** + * {@code u.l.trap}:本帧是否要在每条指令前停下来查钩子。 + *

+ * 这是「钩子开销为零」的关键:钩子没开时 {@code trap} 是 0,主循环连 + * {@code luaG_traceexec} 都不进。它由三处写: + * {@code luaG_tracecall}(进函数时置 1)、{@code luaG_traceexec} + * (没开行/计数钩子时置 0)、以及 {@code lua_sethook} 的 + * {@code settraps}——最后这一处才是「执行到一半打开钩子」也能立刻生效的 + * 原因,因为主循环的 {@code trap} 局部变量只在特定点才从帧里重读。 + */ + boolean trap; + + /** + * {@code CIST_HOOKED}:本帧正在被钩子跟踪。 + *

+ * 作用有二:一是 {@code luaD_hook} 期间置位,让钩子内发起的调用不再触发 + * 新钩子(配合 {@code allowhook});二是让 {@code funcnamefromcall} 把 + * 回溯里的这一层打印成 {@code in hook '?'}。 + */ + boolean hooked; + + /** + * {@code CIST_FIN}:本帧正在跑一个 {@code __gc} 终结器。 + *

+ * 注意这个标记打在发起终结的那一帧上,不是终结器自己的帧—— + * {@code lgc.c: GCTM} 是在调用之前给 {@code L->ci} 置位、之后再清掉。 + * 于是终结器里 {@code debug.getinfo(1)} 走 + * {@code funcnamefromcall(ci->previous)} 时能认出来,给出 + * {@code namewhat == "metamethod"}、{@code name == "__gc"}。 + * 上游 {@code db.lua}「testing debug info for finalizers」就断言这两个值, + * 而且它用 {@code repeat local a = {} until name} 等终结器跑——认不出来 + * 就是死循环。 + */ + boolean fin; + + /** + * {@code CIST_TRAN}:本帧的 {@code ftransfer / ntransfer} 有效。 + * 只在返回钩子的调用期间为真({@code debug.getinfo(2, 'r')} 读它)。 + */ + boolean tran; + + /** {@code u2.transferinfo.ftransfer}:第一个返回值相对 {@link #func} 的偏移 */ + int ftransfer; + + /** {@code u2.transferinfo.ntransfer}:返回值的个数 */ + int ntransfer; + + /* ---- 通用 ---- */ + + /** + * {@code u2.nres}:{@code OP_RETURN} 关闭包时暂存的结果数, + * 供 {@code luaV_finishOp} 在 yield 后恢复。本实现不做 yield, + * 但字段留着,保持与上游一一对应。 + */ + int nres; + + /** 本帧是 Lua 函数(false 表示 C 函数) */ + boolean isLua = true; + + /* ---- yield 相关(C 版在 {@code u.c.*} 与 {@code u2.*} 里) ---- */ + + /** + * {@code u2.nyield}:本帧 yield 出去的值的个数。 + * {@code lua_resume} 靠它知道该从协程栈上搬几个值回调用方。 + */ + int nyield; + + /** + * {@code u.c.k}:yield 之后的续体。只有「可能被 yield 打断」的 C 帧才设, + * 见 {@link LuaKFunction}。 + */ + LuaKFunction k; + + /** {@code u.c.ctx}:续体的上下文 */ + int ctx; + + /** + * {@code CIST_YPCALL}:本帧是一个被打断的 {@code pcall} / {@code xpcall}, + * 可以作为错误恢复点。{@code precover} 全靠这个标记找到「该在哪儿继续」。 + */ + boolean yieldPcall; + + /** + * {@code CIST_RECST}:{@code pcall} 被 yield 打断后、恢复时拿到的错误状态。 + * {@code LUA_OK} 表示不是出错而是被 yield 打断的。 + */ + int recst; + + /** {@code u2.funcidx}:被 {@code pcall} 保护着调用的那个函数的栈下标 */ + int funcIdx; + + /** {@code xpcall} 的消息处理函数;没有就是 {@code null} */ + LuaValue errHandler; + + /** {@code CIST_CLSRET}:C 函数是在关 tbc 变量时被 yield 打断的 */ + boolean clsret; + + /** + * {@code CIST_FRESH}:这一帧是 {@code luaV_execute} 的入口帧。 + * 返回时若是 fresh 帧就退出解释器,否则回到调用者的帧继续跑—— + * 这就是 {@code OP_CALL} 不递归而直接 {@code goto startfunc} 的代价: + * 得自己记住「该在哪一层停下来」。 + */ + boolean fresh; + + /** {@code CIST_TAIL}:是一次尾调用 */ + boolean tail; + + /** {@code CIST_LEQ}:正在用 {@code __lt} 顶替缺失的 {@code __le} */ + boolean leq; + + CallInfo previous; + + /** 本帧的名字,仅供错误信息与调试 */ + String what = "?"; +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/Codegen.java b/src/main/java/com/github/esrrhs/fakescript/lua/Codegen.java new file mode 100644 index 0000000..6e80bb7 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/Codegen.java @@ -0,0 +1,1555 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * 代码生成 —— 逐行翻译 {@code lcode.c}(外加 {@code lcode.h} 的 BinOpr / UnOpr)。 + *

+ * 这里几乎没有「设计」,只有搬运。真正值得记的是几条为什么不能改写的约束: + *

    + *
  1. {@code and} / {@code or} 不生成布尔值,而是生成跳转链表 + * ({@link ExpDesc#t} / {@link ExpDesc#f})。只有最后确定「这里要一个值」 + * 时才用 {@code LFALSESKIP + LOADTRUE} 补上。所以 + * {@code local x = a and b} 里 {@code b} 的值可能直接来自 {@code a} 的 + * 寄存器,一条 MOVE 都不发;
  2. + *
  3. 常量折叠在编译期就做,而且折叠用的算术必须与运行期是同一套 + * (见 {@link Arith})。否则同一个表达式写成常量与写成变量会得到不同结果;
  4. + *
  5. 整数与浮点常量不合并:{@code 100000} 与 {@code 100000.0} 数值相等, + * 但必须是两个常量槽(实测 {@code luac -l} 如此);
  6. + *
  7. 跳转的目标只能回填:Lua 的跳转是相对偏移,且只能往后填, + * 所以所有「跳到哪儿」都要存成链表最后统一 {@code patchlist}。
  8. + *
+ */ +public final class Codegen { + + private Codegen() { + } + + /** {@code NO_JUMP}:跳转链表的结束标记(既是非法地址,也是自环) */ + public static final int NO_JUMP = -1; + + /** {@code MAXREGS}:一个函数最多 255 个寄存器 */ + public static final int MAXREGS = 255; + + /** {@code LIMLINEDIFF}:相对行号差值的上限,超了就得存绝对行号 */ + private static final int LIMLINEDIFF = 0x80; + + /** {@code LUA_MULTRET} */ + public static final int MULTRET = -1; + + /* ---------- BinOpr / UnOpr(ORDER OPR,与上游严格一致) ---------- */ + + public static final int OPR_ADD = 0; + public static final int OPR_SUB = 1; + public static final int OPR_MUL = 2; + public static final int OPR_MOD = 3; + public static final int OPR_POW = 4; + public static final int OPR_DIV = 5; + public static final int OPR_IDIV = 6; + public static final int OPR_BAND = 7; + public static final int OPR_BOR = 8; + public static final int OPR_BXOR = 9; + public static final int OPR_SHL = 10; + public static final int OPR_SHR = 11; + public static final int OPR_CONCAT = 12; + public static final int OPR_EQ = 13; + public static final int OPR_LT = 14; + public static final int OPR_LE = 15; + public static final int OPR_NE = 16; + public static final int OPR_GT = 17; + public static final int OPR_GE = 18; + public static final int OPR_AND = 19; + public static final int OPR_OR = 20; + public static final int OPR_NOBINOPR = 21; + + public static final int UNOPR_MINUS = 0; + public static final int UNOPR_BNOT = 1; + public static final int UNOPR_NOT = 2; + public static final int UNOPR_LEN = 3; + public static final int UNOPR_NOUNOPR = 4; + + /** {@code foldbinop}:只有算术与位运算可折叠 */ + public static boolean foldBinOp(int op) { + return op <= OPR_SHR; + } + + /* ---------- 指令发射 ---------- */ + + /** {@code luaK_code} */ + public static int code(FuncState fs, int instr) { + Proto f = fs.f; + f.setCode(fs.pc, instr); + fs.pc++; + saveLineInfo(fs, f, fs.ls.lastline); + return fs.pc - 1; + } + + /** {@code luaK_codeABCk} */ + public static int codeABCk(FuncState fs, int o, int a, int b, int c, int k) { + return code(fs, OpCode.createABCk(o, a, b, c, k)); + } + + /** {@code luaK_codeABC} */ + public static int codeABC(FuncState fs, int o, int a, int b, int c) { + return codeABCk(fs, o, a, b, c, 0); + } + + /** {@code luaK_codeABx} */ + public static int codeABx(FuncState fs, int o, int a, int bx) { + return code(fs, OpCode.createABx(o, a, bx)); + } + + /** {@code codeAsBx} */ + private static int codeAsBx(FuncState fs, int o, int a, int bx) { + return codeABx(fs, o, a, bx + OpCode.OFFSET_SBX); + } + + /** {@code codesJ} */ + private static int codesJ(FuncState fs, int o, int sj, int k) { + return code(fs, OpCode.createsJ(o, sj + OpCode.OFFSET_SJ, k)); + } + + /** {@code codeextraarg} */ + private static int codeExtraArg(FuncState fs, int a) { + return code(fs, OpCode.createAx(OpCode.OP_EXTRAARG, a)); + } + + /** {@code luaK_codek}:常量下标太大时用 LOADKX + EXTRAARG */ + private static int codeK(FuncState fs, int reg, int k) { + if (k <= OpCode.MAXARG_BX) { + return codeABx(fs, OpCode.OP_LOADK, reg, k); + } + int p = codeABx(fs, OpCode.OP_LOADKX, reg, 0); + codeExtraArg(fs, k); + return p; + } + + /* ---------- 行号 ---------- */ + + /** {@code savelineinfo} */ + private static void saveLineInfo(FuncState fs, Proto f, int line) { + int linedif = line - fs.previousline; + int pc = fs.pc - 1; // 刚写进去的那条指令 + if (Math.abs(linedif) >= LIMLINEDIFF || fs.iwthabs++ >= Proto.MAXIWTHABS) { + f.addAbsLineInfo(pc, line); + linedif = Proto.ABSLINEINFO; + fs.iwthabs = 1; + } + f.setLineInfo(pc, (byte) linedif); + fs.previousline = line; + } + + /** {@code removelastlineinfo} */ + private static void removeLastLineInfo(FuncState fs) { + Proto f = fs.f; + int pc = fs.pc - 1; + if (f.lineinfo[pc] != Proto.ABSLINEINFO) { + fs.previousline -= f.lineinfo[pc]; + fs.iwthabs--; + } else { + f.sizeabslineinfo--; + fs.iwthabs = Proto.MAXIWTHABS + 1; // 强制下一条也用绝对行号 + } + } + + /** {@code removelastinstruction} */ + private static void removeLastInstruction(FuncState fs) { + removeLastLineInfo(fs); + fs.pc--; + } + + /** {@code luaK_fixline}:把刚发的那条指令的行号改写掉 */ + public static void fixLine(FuncState fs, int line) { + removeLastLineInfo(fs); + saveLineInfo(fs, fs.f, line); + } + + /* ---------- 寄存器栈 ---------- */ + + /** {@code luaK_checkstack} */ + public static void checkStack(FuncState fs, int n) { + int newstack = fs.freereg + n; + if (newstack > fs.f.maxstacksize) { + if (newstack >= MAXREGS) { + fs.ls.syntaxError("function or expression needs too many registers"); + } + fs.f.maxstacksize = newstack; + } + } + + /** {@code luaK_reserveregs} */ + public static void reserveRegs(FuncState fs, int n) { + checkStack(fs, n); + fs.freereg += n; + } + + /** {@code freereg}:局部变量占住的寄存器不能释放 */ + private static void freeReg(FuncState fs, int reg) { + if (reg >= fs.nvarstack()) { + fs.freereg--; + } + } + + /** {@code freeregs}:按从高到低的顺序释放 */ + private static void freeRegs(FuncState fs, int r1, int r2) { + if (r1 > r2) { + freeReg(fs, r1); + freeReg(fs, r2); + } else { + freeReg(fs, r2); + freeReg(fs, r1); + } + } + + private static void freeExp(FuncState fs, ExpDesc e) { + if (e.k == ExpDesc.VNONRELOC) { + freeReg(fs, e.info); + } + } + + private static void freeExps(FuncState fs, ExpDesc e1, ExpDesc e2) { + int r1 = (e1.k == ExpDesc.VNONRELOC) ? e1.info : -1; + int r2 = (e2.k == ExpDesc.VNONRELOC) ? e2.info : -1; + freeRegs(fs, r1, r2); + } + + /* ---------- 常量 ---------- */ + + /** {@code stringK} */ + private static int stringK(FuncState fs, LuaString s) { + return addK(fs, LuaValue.ofString(s)); + } + + /** {@code luaK_intK} */ + public static int intK(FuncState fs, long n) { + return addK(fs, LuaValue.ofInteger(n)); + } + + /** + * {@code luaK_numberK}。 + *

+ * 上游是 static,这里给包内可见是为了让「整数常量与浮点常量不合并」这条 + * 规则能被测试直接锁住——它靠肉眼很难发现。 + */ + static int numberK(FuncState fs, double r) { + return addK(fs, LuaValue.ofFloat(r)); + } + + /** {@code boolK} */ + private static int boolK(FuncState fs, boolean b) { + return addK(fs, LuaValue.ofBoolean(b)); + } + + /** {@code nilK} */ + private static int nilK(FuncState fs) { + return addK(fs, LuaValue.ofNil()); + } + + /** + * {@code addk}:命中缓存就复用下标。 + *

+ * 缓存是整份源码共享的({@code ls->h}),命中后还要按「下标没越界 + 变体标签 + * 一致 + 值相等」三道校验——详见 {@link Lexer#lookupConstant}。 + */ + private static int addK(FuncState fs, LuaValue v) { + Integer idx = fs.ls.lookupConstant(fs, v); + if (idx != null) { + return idx; + } + if (fs.f.sizek >= OpCode.MAXARG_AX) { + throw fs.ls.syntaxError("too many constants"); + } + return fs.ls.addConstant(fs, v); + } + + /* ---------- 立即数 ---------- */ + + /** {@code fitsC}:能否塞进 sC 操作数(范围 -127..128) */ + public static boolean fitsC(long i) { + return i >= -OpCode.OFFSET_SC && i <= OpCode.MAXARG_C - OpCode.OFFSET_SC; + } + + /** {@code fitsBx}:能否塞进 sBx 操作数(范围 -65535..65536) */ + public static boolean fitsBx(long i) { + return -OpCode.OFFSET_SBX <= i && i <= OpCode.MAXARG_BX - OpCode.OFFSET_SBX; + } + + /** {@code luaK_int} */ + public static void codeInt(FuncState fs, int reg, long i) { + if (fitsBx(i)) { + codeAsBx(fs, OpCode.OP_LOADI, reg, (int) i); + } else { + codeK(fs, reg, intK(fs, i)); + } + } + + /** {@code luaK_float} */ + private static void codeFloat(FuncState fs, int reg, double f) { + Long fi = Arith.fltToInteger(f, Arith.F2I_EQ); + if (fi != null && fitsBx(fi)) { + codeAsBx(fs, OpCode.OP_LOADF, reg, fi.intValue()); + } else { + codeK(fs, reg, numberK(fs, f)); + } + } + + /** {@code luaK_nil}:相邻的 LOADNIL 会合并成一条 */ + public static void codeNil(FuncState fs, int from, int n) { + int l = from + n - 1; // 最后一个要置 nil 的寄存器 + int previous = previousInstruction(fs); + if (previous != -1 && OpCode.getOpCode(previous) == OpCode.OP_LOADNIL) { + int pfrom = OpCode.getArgA(previous); + int pl = pfrom + OpCode.getArgB(previous); + if ((pfrom <= from && from <= pl + 1) || (from <= pfrom && pfrom <= l + 1)) { + if (pfrom < from) { + from = pfrom; + } + if (pl > l) { + l = pl; + } + int patched = OpCode.setArgA(previous, from); + patched = OpCode.setArgB(patched, l - from); + fs.f.setCode(fs.pc - 1, patched); + return; + } + } + codeABC(fs, OpCode.OP_LOADNIL, from, n - 1, 0); + } + + /** + * {@code previousinstruction}:返回上一条指令;若两者之间可能存在跳转目标, + * 返回 -1(全 1 的非法指令),以避免跨基本块做优化。 + */ + private static int previousInstruction(FuncState fs) { + if (fs.pc > fs.lasttarget) { + return fs.f.code[fs.pc - 1]; + } + return -1; + } + + /* ---------- 跳转 ---------- */ + + /** {@code getjump} */ + private static int getJump(FuncState fs, int pc) { + int offset = OpCode.getArgSJ(fs.f.code[pc]); + if (offset == NO_JUMP) { + return NO_JUMP; // 链表结束 + } + return (pc + 1) + offset; // 相对偏移转绝对位置 + } + + /** {@code fixjump} */ + private static void fixJump(FuncState fs, int pc, int dest) { + int offset = dest - (pc + 1); + if (!(-OpCode.OFFSET_SJ <= offset && offset <= OpCode.MAXARG_SJ - OpCode.OFFSET_SJ)) { + fs.ls.syntaxError("control structure too long"); + } + fs.f.setCode(pc, OpCode.setArgSJ(fs.f.code[pc], offset)); + } + + /** {@code luaK_concat}:把链表 l2 接到 l1 尾部 */ + public static void concat(FuncState fs, int[] l1, int l2) { + if (l2 == NO_JUMP) { + return; + } + if (l1[0] == NO_JUMP) { + l1[0] = l2; + } else { + int list = l1[0]; + int next; + while ((next = getJump(fs, list)) != NO_JUMP) { + list = next; + } + fixJump(fs, list, l2); + } + } + + /** 把 {@code list} 接到 {@code e} 的 t 链表上(C 里是 {@code &e->t}) */ + public static void concatT(FuncState fs, ExpDesc e, int list) { + int[] box = new int[] { e.t }; + concat(fs, box, list); + e.t = box[0]; + } + + /** 同上,接到 f 链表 */ + public static void concatF(FuncState fs, ExpDesc e, int list) { + int[] box = new int[] { e.f }; + concat(fs, box, list); + e.f = box[0]; + } + + /** {@code luaK_jump} */ + public static int jump(FuncState fs) { + return codesJ(fs, OpCode.OP_JMP, NO_JUMP, 0); + } + + /** {@code luaK_ret} */ + public static void ret(FuncState fs, int first, int nret) { + int op; + switch (nret) { + case 0: + op = OpCode.OP_RETURN0; + break; + case 1: + op = OpCode.OP_RETURN1; + break; + default: + op = OpCode.OP_RETURN; + break; + } + codeABC(fs, op, first, nret + 1, 0); + } + + /** {@code condjump} */ + private static int condJump(FuncState fs, int op, int a, int b, int c, int k) { + codeABCk(fs, op, a, b, c, k); + return jump(fs); + } + + /** {@code luaK_getlabel} */ + public static int getLabel(FuncState fs) { + fs.lasttarget = fs.pc; + return fs.pc; + } + + /** {@code getjumpcontrol}:找到真正控制这个跳转的那条(测试指令在前一条) */ + private static int getJumpControl(FuncState fs, int pc) { + if (pc >= 1 && OpCode.testTMode(OpCode.getOpCode(fs.f.code[pc - 1]))) { + return pc - 1; + } + return pc; + } + + /** + * {@code patchtestreg}:把 TESTSET 的目标寄存器改掉; + * 若不需要值就把它降级成 TEST。 + * + * @return 是否成功(那条指令确实是 TESTSET) + */ + private static boolean patchTestReg(FuncState fs, int node, int reg) { + int i = getJumpControl(fs, node); + int inst = fs.f.code[i]; + if (OpCode.getOpCode(inst) != OpCode.OP_TESTSET) { + return false; + } + if (reg != OpCode.NO_REG && reg != OpCode.getArgB(inst)) { + fs.f.setCode(i, OpCode.setArgA(inst, reg)); + } else { + // 不需要值,或寄存器里已经有值了 —— 降级成纯测试 + fs.f.setCode(i, OpCode.createABCk(OpCode.OP_TEST, OpCode.getArgB(inst), 0, 0, + OpCode.getArgK(inst))); + } + return true; + } + + /** {@code removevalues} */ + private static void removeValues(FuncState fs, int list) { + for (; list != NO_JUMP; list = getJump(fs, list)) { + patchTestReg(fs, list, OpCode.NO_REG); + } + } + + /** {@code patchlistaux} */ + private static void patchListAux(FuncState fs, int list, int vtarget, int reg, int dtarget) { + while (list != NO_JUMP) { + int next = getJump(fs, list); + if (patchTestReg(fs, list, reg)) { + fixJump(fs, list, vtarget); + } else { + fixJump(fs, list, dtarget); + } + list = next; + } + } + + /** {@code luaK_patchlist} */ + public static void patchList(FuncState fs, int list, int target) { + patchListAux(fs, list, target, OpCode.NO_REG, target); + } + + /** {@code luaK_patchtohere} */ + public static void patchToHere(FuncState fs, int list) { + int hr = getLabel(fs); + patchList(fs, list, hr); + } + + /** {@code luaK_jumpto} */ + public static void jumpTo(FuncState fs, int target) { + patchList(fs, jump(fs), target); + } + + /* ---------- 表达式落地 ---------- */ + + /** {@code luaK_dischargevars}:把「变量」变成「值」(可能仍在寄存器里) */ + public static void dischargeVars(FuncState fs, ExpDesc e) { + switch (e.k) { + case ExpDesc.VCONST: { + LuaValue v = constVal(fs, e); + constToExp(v, e); + break; + } + case ExpDesc.VLOCAL: { + int temp = e.varRidx; + e.info = temp; + e.k = ExpDesc.VNONRELOC; + break; + } + case ExpDesc.VUPVAL: { + e.info = codeABC(fs, OpCode.OP_GETUPVAL, 0, e.info, 0); + e.k = ExpDesc.VRELOC; + break; + } + case ExpDesc.VINDEXUP: { + e.info = codeABC(fs, OpCode.OP_GETTABUP, 0, e.indT, e.indIdx); + e.k = ExpDesc.VRELOC; + break; + } + case ExpDesc.VINDEXI: { + freeReg(fs, e.indT); + e.info = codeABC(fs, OpCode.OP_GETI, 0, e.indT, e.indIdx); + e.k = ExpDesc.VRELOC; + break; + } + case ExpDesc.VINDEXSTR: { + freeReg(fs, e.indT); + e.info = codeABC(fs, OpCode.OP_GETFIELD, 0, e.indT, e.indIdx); + e.k = ExpDesc.VRELOC; + break; + } + case ExpDesc.VINDEXED: { + freeRegs(fs, e.indT, e.indIdx); + e.info = codeABC(fs, OpCode.OP_GETTABLE, 0, e.indT, e.indIdx); + e.k = ExpDesc.VRELOC; + break; + } + case ExpDesc.VVARARG: + case ExpDesc.VCALL: { + setOneRet(fs, e); + break; + } + default: + break; // 已经有值了 + } + } + + /** {@code const2val} */ + private static LuaValue constVal(FuncState fs, ExpDesc e) { + return fs.dyd.actvar.get(e.info).k; + } + + /** {@code const2exp} */ + private static void constToExp(LuaValue v, ExpDesc e) { + if (v.isInteger()) { + e.k = ExpDesc.VKINT; + e.ival = v.i; + } else if (v.isFloat()) { + e.k = ExpDesc.VKFLT; + e.nval = v.n; + } else if (v.isBoolean()) { + e.k = v.asBoolean() ? ExpDesc.VTRUE : ExpDesc.VFALSE; + } else if (v.isNil()) { + e.k = ExpDesc.VNIL; + } else if (v.isString()) { + e.k = ExpDesc.VKSTR; + e.strval = v.asString(); + } else { + throw new IllegalStateException("bad constant " + v); + } + } + + /** {@code luaK_exp2const} */ + public static boolean exp2Const(FuncState fs, ExpDesc e, LuaValue out) { + if (e.hasJumps()) { + return false; + } + switch (e.k) { + case ExpDesc.VFALSE: + out.setBoolean(false); + return true; + case ExpDesc.VTRUE: + out.setBoolean(true); + return true; + case ExpDesc.VNIL: + out.setNil(); + return true; + case ExpDesc.VKSTR: + out.setString(e.strval); + return true; + case ExpDesc.VCONST: + out.set(constVal(fs, e)); + return true; + default: + if (e.k == ExpDesc.VKINT) { + out.setInteger(e.ival); + return true; + } + if (e.k == ExpDesc.VKFLT) { + out.setFloat(e.nval); + return true; + } + return false; + } + } + + /** {@code discharge2reg} */ + private static void discharge2Reg(FuncState fs, ExpDesc e, int reg) { + dischargeVars(fs, e); + switch (e.k) { + case ExpDesc.VNIL: + codeNil(fs, reg, 1); + break; + case ExpDesc.VFALSE: + codeABC(fs, OpCode.OP_LOADFALSE, reg, 0, 0); + break; + case ExpDesc.VTRUE: + codeABC(fs, OpCode.OP_LOADTRUE, reg, 0, 0); + break; + case ExpDesc.VKSTR: + e.info = stringK(fs, e.strval); + e.k = ExpDesc.VK; + // FALLTHROUGH + case ExpDesc.VK: + codeK(fs, reg, e.info); + break; + case ExpDesc.VKFLT: + codeFloat(fs, reg, e.nval); + break; + case ExpDesc.VKINT: + codeInt(fs, reg, e.ival); + break; + case ExpDesc.VRELOC: { + int pc = e.info; + fs.f.setCode(pc, OpCode.setArgA(fs.f.code[pc], reg)); + break; + } + case ExpDesc.VNONRELOC: + if (reg != e.info) { + codeABC(fs, OpCode.OP_MOVE, reg, e.info, 0); + } + break; + default: + return; // VJMP:值由跳转链表决定,这里什么都不做 + } + e.info = reg; + e.k = ExpDesc.VNONRELOC; + } + + private static void discharge2AnyReg(FuncState fs, ExpDesc e) { + if (e.k != ExpDesc.VNONRELOC) { + reserveRegs(fs, 1); + discharge2Reg(fs, e, fs.freereg - 1); + } + } + + /** {@code code_loadbool} */ + private static int codeLoadBool(FuncState fs, int a, int op) { + getLabel(fs); // 这两条指令可能是跳转目标 + return codeABC(fs, op, a, 0, 0); + } + + /** {@code need_value} */ + private static boolean needValue(FuncState fs, int list) { + for (; list != NO_JUMP; list = getJump(fs, list)) { + int i = fs.f.code[getJumpControl(fs, list)]; + if (OpCode.getOpCode(i) != OpCode.OP_TESTSET) { + return true; + } + } + return false; + } + + /** {@code exp2reg} */ + private static void exp2Reg(FuncState fs, ExpDesc e, int reg) { + discharge2Reg(fs, e, reg); + if (e.k == ExpDesc.VJMP) { + concatT(fs, e, e.info); + } + if (e.hasJumps()) { + int pF = NO_JUMP; // 可能的 LOADFALSE + int pT = NO_JUMP; // 可能的 LOADTRUE + if (needValue(fs, e.t) || needValue(fs, e.f)) { + int fj = (e.k == ExpDesc.VJMP) ? NO_JUMP : jump(fs); + pF = codeLoadBool(fs, reg, OpCode.OP_LFALSESKIP); + pT = codeLoadBool(fs, reg, OpCode.OP_LOADTRUE); + patchToHere(fs, fj); + } + int fin = getLabel(fs); + patchListAux(fs, e.f, fin, reg, pF); + patchListAux(fs, e.t, fin, reg, pT); + } + e.f = e.t = NO_JUMP; + e.info = reg; + e.k = ExpDesc.VNONRELOC; + } + + /** {@code luaK_exp2nextreg} */ + public static void exp2NextReg(FuncState fs, ExpDesc e) { + dischargeVars(fs, e); + freeExp(fs, e); + reserveRegs(fs, 1); + exp2Reg(fs, e, fs.freereg - 1); + } + + /** {@code luaK_exp2anyreg} */ + public static int exp2AnyReg(FuncState fs, ExpDesc e) { + dischargeVars(fs, e); + if (e.k == ExpDesc.VNONRELOC) { + if (!e.hasJumps()) { + return e.info; + } + if (e.info >= fs.nvarstack()) { + exp2Reg(fs, e, e.info); + return e.info; + } + // 否则它是局部变量:有跳转时不能拿它当结果的落点,走默认分支 + } + exp2NextReg(fs, e); + return e.info; + } + + /** {@code luaK_exp2anyregup} */ + public static void exp2AnyRegUp(FuncState fs, ExpDesc e) { + if (e.k != ExpDesc.VUPVAL || e.hasJumps()) { + exp2AnyReg(fs, e); + } + } + + /** {@code luaK_exp2val} */ + public static void exp2Val(FuncState fs, ExpDesc e) { + if (e.hasJumps()) { + exp2AnyReg(fs, e); + } else { + dischargeVars(fs, e); + } + } + + /** {@code luaK_exp2K} */ + private static boolean exp2K(FuncState fs, ExpDesc e) { + if (!e.hasJumps()) { + int info; + switch (e.k) { + case ExpDesc.VTRUE: + info = boolK(fs, true); + break; + case ExpDesc.VFALSE: + info = boolK(fs, false); + break; + case ExpDesc.VNIL: + info = nilK(fs); + break; + case ExpDesc.VKINT: + info = intK(fs, e.ival); + break; + case ExpDesc.VKFLT: + info = numberK(fs, e.nval); + break; + case ExpDesc.VKSTR: + info = stringK(fs, e.strval); + break; + case ExpDesc.VK: + info = e.info; + break; + default: + return false; // 不是常量 + } + if (info <= OpCode.MAXARG_B) { // 能塞进 RK 下标? + e.k = ExpDesc.VK; + e.info = info; + return true; + } + } + return false; + } + + /** {@code exp2RK} */ + private static boolean exp2RK(FuncState fs, ExpDesc e) { + if (exp2K(fs, e)) { + return true; + } + exp2AnyReg(fs, e); + return false; + } + + /** {@code codeABRK} */ + private static void codeABRK(FuncState fs, int o, int a, int b, ExpDesc ec) { + int k = exp2RK(fs, ec) ? 1 : 0; + codeABCk(fs, o, a, b, ec.info, k); + } + + /* ---------- 多返回值 ---------- */ + + /** {@code luaK_setreturns} */ + public static void setReturns(FuncState fs, ExpDesc e, int nresults) { + int pc = e.info; + if (e.k == ExpDesc.VCALL) { + fs.f.setCode(pc, OpCode.setArgC(fs.f.code[pc], nresults + 1)); + } else { + int inst = fs.f.code[pc]; + inst = OpCode.setArgC(inst, nresults + 1); + inst = OpCode.setArgA(inst, fs.freereg); + fs.f.setCode(pc, inst); + reserveRegs(fs, 1); + } + } + + /** {@code luaK_setmultret} */ + public static void setMultiRet(FuncState fs, ExpDesc e) { + setReturns(fs, e, MULTRET); + } + + /** {@code luaK_setoneret} */ + public static void setOneRet(FuncState fs, ExpDesc e) { + if (e.k == ExpDesc.VCALL) { + e.k = ExpDesc.VNONRELOC; + e.info = OpCode.getArgA(fs.f.code[e.info]); + } else if (e.k == ExpDesc.VVARARG) { + fs.f.setCode(e.info, OpCode.setArgC(fs.f.code[e.info], 2)); + e.k = ExpDesc.VRELOC; + } + } + + /* ---------- 变量读写 ---------- */ + + /** {@code luaK_storevar} */ + public static void storeVar(FuncState fs, ExpDesc var, ExpDesc ex) { + switch (var.k) { + case ExpDesc.VLOCAL: + freeExp(fs, ex); + exp2Reg(fs, ex, var.varRidx); + return; + case ExpDesc.VUPVAL: { + int e = exp2AnyReg(fs, ex); + codeABC(fs, OpCode.OP_SETUPVAL, e, var.info, 0); + break; + } + case ExpDesc.VINDEXUP: + codeABRK(fs, OpCode.OP_SETTABUP, var.indT, var.indIdx, ex); + break; + case ExpDesc.VINDEXI: + codeABRK(fs, OpCode.OP_SETI, var.indT, var.indIdx, ex); + break; + case ExpDesc.VINDEXSTR: + codeABRK(fs, OpCode.OP_SETFIELD, var.indT, var.indIdx, ex); + break; + case ExpDesc.VINDEXED: + codeABRK(fs, OpCode.OP_SETTABLE, var.indT, var.indIdx, ex); + break; + default: + throw new IllegalStateException("invalid var kind to store: " + var); + } + freeExp(fs, ex); + } + + /** {@code luaK_self} */ + public static void self(FuncState fs, ExpDesc e, ExpDesc key) { + exp2AnyReg(fs, e); + int ereg = e.info; + freeExp(fs, e); + e.info = fs.freereg; + e.k = ExpDesc.VNONRELOC; + reserveRegs(fs, 2); // 方法本身与 self + codeABRK(fs, OpCode.OP_SELF, e.info, ereg, key); + freeExp(fs, key); + } + + /** {@code luaK_indexed} */ + public static void indexed(FuncState fs, ExpDesc t, ExpDesc k) { + if (k.k == ExpDesc.VKSTR) { + k.info = stringK(fs, k.strval); + k.k = ExpDesc.VK; + } + if (t.k == ExpDesc.VUPVAL && !isKstr(fs, k)) { + exp2AnyReg(fs, t); // upvalue 只能被字面短串索引 + } + if (t.k == ExpDesc.VUPVAL) { + int temp = t.info; + t.indT = temp; + t.indIdx = k.info; + t.k = ExpDesc.VINDEXUP; + } else { + t.indT = (t.k == ExpDesc.VLOCAL) ? t.varRidx : t.info; + if (isKstr(fs, k)) { + t.indIdx = k.info; + t.k = ExpDesc.VINDEXSTR; + } else if (isCint(k)) { + t.indIdx = (int) k.ival; + t.k = ExpDesc.VINDEXI; + } else { + t.indIdx = exp2AnyReg(fs, k); + t.k = ExpDesc.VINDEXED; + } + } + } + + /** {@code isKstr}:常量短串(GETFIELD/SETFIELD 要求短串) */ + private static boolean isKstr(FuncState fs, ExpDesc e) { + return e.k == ExpDesc.VK && !e.hasJumps() && e.info <= OpCode.MAXARG_B + && fs.f.k[e.info].asString().isShort(); + } + + /** {@code isKint} */ + private static boolean isKint(ExpDesc e) { + return e.k == ExpDesc.VKINT && !e.hasJumps(); + } + + /** {@code isCint}:能塞进 C 字段的整数字面量 */ + private static boolean isCint(ExpDesc e) { + return isKint(e) && e.ival >= 0 && e.ival <= OpCode.MAXARG_C; + } + + /** {@code isSCint}:能塞进 sC 字段的整数字面量 */ + private static boolean isSCint(ExpDesc e) { + return isKint(e) && fitsC(e.ival); + } + + /** {@code isSCnumber} */ + private static boolean isSCnumber(ExpDesc e, int[] im, boolean[] isfloat) { + long i; + if (e.k == ExpDesc.VKINT) { + i = e.ival; + } else if (e.k == ExpDesc.VKFLT) { + Long fi = Arith.fltToInteger(e.nval, Arith.F2I_EQ); + if (fi == null) { + return false; + } + i = fi; + isfloat[0] = true; + } else { + return false; + } + if (!e.hasJumps() && fitsC(i)) { + im[0] = OpCode.int2sC((int) i); + return true; + } + return false; + } + + /* ---------- 条件 ---------- */ + + /** {@code negatecondition} */ + private static void negateCondition(FuncState fs, ExpDesc e) { + int pc = getJumpControl(fs, e.info); + int inst = fs.f.code[pc]; + fs.f.setCode(pc, OpCode.setArgK(inst, OpCode.getArgK(inst) ^ 1)); + } + + /** {@code jumponcond} */ + private static int jumpOnCond(FuncState fs, ExpDesc e, int cond) { + if (e.k == ExpDesc.VRELOC) { + int ie = fs.f.code[e.info]; + if (OpCode.getOpCode(ie) == OpCode.OP_NOT) { + removeLastInstruction(fs); // 消掉这条 NOT + return condJump(fs, OpCode.OP_TEST, OpCode.getArgB(ie), 0, 0, cond == 0 ? 1 : 0); + } + } + discharge2AnyReg(fs, e); + freeExp(fs, e); + return condJump(fs, OpCode.OP_TESTSET, OpCode.NO_REG, e.info, 0, cond); + } + + /** {@code luaK_goiftrue} */ + public static void goIfTrue(FuncState fs, ExpDesc e) { + int pc; + dischargeVars(fs, e); + switch (e.k) { + case ExpDesc.VJMP: + negateCondition(fs, e); + pc = e.info; + break; + case ExpDesc.VK: + case ExpDesc.VKFLT: + case ExpDesc.VKINT: + case ExpDesc.VKSTR: + case ExpDesc.VTRUE: + pc = NO_JUMP; // 恒为真 + break; + default: + pc = jumpOnCond(fs, e, 0); // 为假则跳 + break; + } + concatF(fs, e, pc); + patchToHere(fs, e.t); + e.t = NO_JUMP; + } + + /** {@code luaK_goiffalse} */ + public static void goIfFalse(FuncState fs, ExpDesc e) { + int pc; + dischargeVars(fs, e); + switch (e.k) { + case ExpDesc.VJMP: + pc = e.info; + break; + case ExpDesc.VNIL: + case ExpDesc.VFALSE: + pc = NO_JUMP; // 恒为假 + break; + default: + pc = jumpOnCond(fs, e, 1); // 为真则跳 + break; + } + concatT(fs, e, pc); + patchToHere(fs, e.f); + e.f = NO_JUMP; + } + + /** {@code codenot} */ + private static void codeNot(FuncState fs, ExpDesc e) { + switch (e.k) { + case ExpDesc.VNIL: + case ExpDesc.VFALSE: + e.k = ExpDesc.VTRUE; + break; + case ExpDesc.VK: + case ExpDesc.VKFLT: + case ExpDesc.VKINT: + case ExpDesc.VKSTR: + case ExpDesc.VTRUE: + e.k = ExpDesc.VFALSE; + break; + case ExpDesc.VJMP: + negateCondition(fs, e); + break; + case ExpDesc.VRELOC: + case ExpDesc.VNONRELOC: { + discharge2AnyReg(fs, e); + freeExp(fs, e); + e.info = codeABC(fs, OpCode.OP_NOT, 0, e.info, 0); + e.k = ExpDesc.VRELOC; + break; + } + default: + throw new IllegalStateException("cannot happen: " + e); + } + int temp = e.f; + e.f = e.t; + e.t = temp; + removeValues(fs, e.f); // 取反之后值就没用了 + removeValues(fs, e.t); + } + + /* ---------- 运算符 ---------- */ + + /** {@code binopr2op}(ORDER OPR - ORDER OP) */ + private static int binOpr2Op(int opr, int baser, int base) { + return (opr - baser) + base; + } + + /** {@code unopr2op} */ + private static int unOpr2Op(int opr) { + return (opr - UNOPR_MINUS) + OpCode.OP_UNM; + } + + /** {@code binopr2TM}(ORDER OPR - ORDER TM) */ + private static int binOpr2TM(int opr) { + return (opr - OPR_ADD) + Tm.TM_ADD; + } + + /** {@code codeunexpval} */ + private static void codeUnExpVal(FuncState fs, int op, ExpDesc e, int line) { + int r = exp2AnyReg(fs, e); + freeExp(fs, e); + e.info = codeABC(fs, op, 0, r, 0); + e.k = ExpDesc.VRELOC; + fixLine(fs, line); + } + + /** {@code finishbinexpval} */ + private static void finishBinExpVal(FuncState fs, ExpDesc e1, ExpDesc e2, int op, int v2, + int flip, int line, int mmop, int event) { + int v1 = exp2AnyReg(fs, e1); + int pc = codeABCk(fs, op, 0, v1, v2, 0); + freeExps(fs, e1, e2); + e1.info = pc; + e1.k = ExpDesc.VRELOC; + fixLine(fs, line); + codeABCk(fs, mmop, v1, v2, event, flip); // 元方法兜底 + fixLine(fs, line); + } + + /** {@code codebinexpval} */ + private static void codeBinExpVal(FuncState fs, int opr, ExpDesc e1, ExpDesc e2, int line) { + int op = binOpr2Op(opr, OPR_ADD, OpCode.OP_ADD); + int v2 = exp2AnyReg(fs, e2); + finishBinExpVal(fs, e1, e2, op, v2, 0, line, OpCode.OP_MMBIN, binOpr2TM(opr)); + } + + /** {@code codebini} */ + private static void codeBinI(FuncState fs, int op, ExpDesc e1, ExpDesc e2, int flip, int line, + int event) { + int v2 = OpCode.int2sC((int) e2.ival); + finishBinExpVal(fs, e1, e2, op, v2, flip, line, OpCode.OP_MMBINI, event); + } + + /** {@code codebinK} */ + private static void codeBinK(FuncState fs, int opr, ExpDesc e1, ExpDesc e2, int flip, + int line) { + int event = binOpr2TM(opr); + int v2 = e2.info; + int op = binOpr2Op(opr, OPR_ADD, OpCode.OP_ADDK); + finishBinExpVal(fs, e1, e2, op, v2, flip, line, OpCode.OP_MMBINK, event); + } + + /** {@code finishbinexpneg}:{@code a - 5} 编成 {@code a + (-5)} */ + private static boolean finishBinExpNeg(FuncState fs, ExpDesc e1, ExpDesc e2, int op, int line, + int event) { + if (!isKint(e2)) { + return false; + } + long i2 = e2.ival; + if (!(fitsC(i2) && fitsC(-i2))) { + return false; + } + int v2 = (int) i2; + finishBinExpVal(fs, e1, e2, op, OpCode.int2sC(-v2), 0, line, OpCode.OP_MMBINI, event); + // 元方法必须拿到原本的第二个操作数 + int last = fs.pc - 1; + fs.f.setCode(last, OpCode.setArgB(fs.f.code[last], OpCode.int2sC(v2))); + return true; + } + + private static void swapExps(ExpDesc e1, ExpDesc e2) { + ExpDesc temp = new ExpDesc(e1); + e1.set(e2); + e2.set(temp); + } + + /** {@code codebinNoK} */ + private static void codeBinNoK(FuncState fs, int opr, ExpDesc e1, ExpDesc e2, int flip, + int line) { + if (flip != 0) { + swapExps(e1, e2); + } + codeBinExpVal(fs, opr, e1, e2, line); + } + + /** {@code codearith} */ + private static void codeArith(FuncState fs, int opr, ExpDesc e1, ExpDesc e2, int flip, + int line) { + if (isNumeral(e2) && exp2K(fs, e2)) { + codeBinK(fs, opr, e1, e2, flip, line); + } else { + codeBinNoK(fs, opr, e1, e2, flip, line); + } + } + + /** {@code codecommutative} */ + private static void codeCommutative(FuncState fs, int op, ExpDesc e1, ExpDesc e2, int line) { + int flip = 0; + if (isNumeral(e1)) { + swapExps(e1, e2); + flip = 1; + } + if (op == OPR_ADD && isSCint(e2)) { + codeBinI(fs, OpCode.OP_ADDI, e1, e2, flip, line, Tm.TM_ADD); + } else { + codeArith(fs, op, e1, e2, flip, line); + } + } + + /** {@code codebitwise} */ + private static void codeBitwise(FuncState fs, int opr, ExpDesc e1, ExpDesc e2, int line) { + int flip = 0; + if (e1.k == ExpDesc.VKINT) { + swapExps(e1, e2); + flip = 1; + } + if (e2.k == ExpDesc.VKINT && exp2K(fs, e2)) { + codeBinK(fs, opr, e1, e2, flip, line); + } else { + codeBinNoK(fs, opr, e1, e2, flip, line); + } + } + + /** {@code codeorder} */ + private static void codeOrder(FuncState fs, int opr, ExpDesc e1, ExpDesc e2) { + int r1; + int r2; + int[] im = new int[1]; + boolean[] isfloat = new boolean[1]; + int op; + if (isSCnumber(e2, im, isfloat)) { + r1 = exp2AnyReg(fs, e1); + r2 = im[0]; + op = binOpr2Op(opr, OPR_LT, OpCode.OP_LTI); + } else if (isSCnumber(e1, im, isfloat)) { + // (A < B) 变成 (B > A),(A <= B) 变成 (B >= A) + r1 = exp2AnyReg(fs, e2); + r2 = im[0]; + op = binOpr2Op(opr, OPR_LT, OpCode.OP_GTI); + } else { + r1 = exp2AnyReg(fs, e1); + r2 = exp2AnyReg(fs, e2); + op = binOpr2Op(opr, OPR_LT, OpCode.OP_LT); + } + freeExps(fs, e1, e2); + e1.info = condJump(fs, op, r1, r2, isfloat[0] ? 1 : 0, 1); + e1.k = ExpDesc.VJMP; + } + + /** {@code codeeq} */ + private static void codeEq(FuncState fs, int opr, ExpDesc e1, ExpDesc e2) { + int r1; + int r2; + int[] im = new int[1]; + boolean[] isfloat = new boolean[1]; + int op; + if (e1.k != ExpDesc.VNONRELOC) { + swapExps(e1, e2); + } + r1 = exp2AnyReg(fs, e1); // 第一个操作数必须在寄存器里 + if (isSCnumber(e2, im, isfloat)) { + op = OpCode.OP_EQI; + r2 = im[0]; + } else if (exp2RK(fs, e2)) { + op = OpCode.OP_EQK; + r2 = e2.info; + } else { + op = OpCode.OP_EQ; + r2 = exp2AnyReg(fs, e2); + } + freeExps(fs, e1, e2); + e1.info = condJump(fs, op, r1, r2, isfloat[0] ? 1 : 0, opr == OPR_EQ ? 1 : 0); + e1.k = ExpDesc.VJMP; + } + + /** {@code codeconcat} */ + private static void codeConcat(FuncState fs, ExpDesc e1, ExpDesc e2, int line) { + int ie2 = previousInstruction(fs); + if (ie2 != -1 && OpCode.getOpCode(ie2) == OpCode.OP_CONCAT) { + int n = OpCode.getArgB(ie2); // e2 里已拼接的元素数 + freeExp(fs, e2); + int patched = OpCode.setArgA(ie2, e1.info); + patched = OpCode.setArgB(patched, n + 1); + fs.f.setCode(fs.pc - 1, patched); + } else { + codeABC(fs, OpCode.OP_CONCAT, e1.info, 2, 0); + freeExp(fs, e2); + fixLine(fs, line); + } + } + + /* ---------- 常量折叠 ---------- */ + + /** {@code tonumeral} */ + private static boolean isNumeral(ExpDesc e) { + return !e.hasJumps() && (e.k == ExpDesc.VKINT || e.k == ExpDesc.VKFLT); + } + + /** {@code validop}:折叠会不会引发错误(除零、浮点转整数失败) */ + private static boolean validOp(int op, LuaValue v1, LuaValue v2) { + switch (op) { + case Arith.OP_BAND: + case Arith.OP_BOR: + case Arith.OP_BXOR: + case Arith.OP_SHL: + case Arith.OP_SHR: + case Arith.OP_BNOT: + return Arith.toIntegerNs(v1) != null && Arith.toIntegerNs(v2) != null; + case Arith.OP_DIV: + case Arith.OP_IDIV: + case Arith.OP_MOD: { + Double n = v2.isInteger() ? (double) v2.i : (v2.isFloat() ? v2.n : null); + return n != null && n != 0; + } + default: + return true; + } + } + + /** {@code constfolding} */ + private static boolean constFolding(FuncState fs, int op, ExpDesc e1, ExpDesc e2) { + LuaValue v1 = toNumeralValue(e1); + LuaValue v2 = toNumeralValue(e2); + if (v1 == null || v2 == null || !validOp(op, v1, v2)) { + return false; + } + /* + * 常量折叠没有 LuaState,除零那两个分支也到不了 + * (validOp 已经把它们挡在外面),所以这里传 null 是安全的。 + */ + LuaValue res = Arith.rawArith(null, op, v1, v2); + if (res == null) { + return false; + } + if (res.isInteger()) { + e1.k = ExpDesc.VKINT; + e1.ival = res.i; + } else { + // NaN 与 0.0 不折叠(避免 -0.0 的问题) + double n = res.n; + if (Double.isNaN(n) || n == 0) { + return false; + } + e1.k = ExpDesc.VKFLT; + e1.nval = n; + } + return true; + } + + private static LuaValue toNumeralValue(ExpDesc e) { + if (e.hasJumps()) { + return null; + } + if (e.k == ExpDesc.VKINT) { + return LuaValue.ofInteger(e.ival); + } + if (e.k == ExpDesc.VKFLT) { + return LuaValue.ofFloat(e.nval); + } + return null; + } + + /* ---------- prefix / infix / posfix ---------- */ + + /** {@code luaK_prefix} */ + public static void prefix(FuncState fs, int opr, ExpDesc e, int line) { + dischargeVars(fs, e); + switch (opr) { + case UNOPR_MINUS: + case UNOPR_BNOT: { + ExpDesc ef = ExpDesc.ofInt(0); // 假的第二个操作数 + if (constFolding(fs, opr + Arith.OP_UNM, e, ef)) { + break; + } + // FALLTHROUGH + codeUnExpVal(fs, unOpr2Op(opr), e, line); + break; + } + case UNOPR_LEN: + codeUnExpVal(fs, unOpr2Op(opr), e, line); + break; + case UNOPR_NOT: + codeNot(fs, e); + break; + default: + throw new IllegalStateException("bad unopr " + opr); + } + } + + /** {@code luaK_infix} */ + public static void infix(FuncState fs, int op, ExpDesc v) { + dischargeVars(fs, v); + switch (op) { + case OPR_AND: + goIfTrue(fs, v); + break; + case OPR_OR: + goIfFalse(fs, v); + break; + case OPR_CONCAT: + exp2NextReg(fs, v); // 拼接的操作数必须在栈上相邻 + break; + case OPR_ADD: + case OPR_SUB: + case OPR_MUL: + case OPR_DIV: + case OPR_IDIV: + case OPR_MOD: + case OPR_POW: + case OPR_BAND: + case OPR_BOR: + case OPR_BXOR: + case OPR_SHL: + case OPR_SHR: + if (!isNumeral(v)) { + exp2AnyReg(fs, v); + } + break; + case OPR_EQ: + case OPR_NE: + if (!isNumeral(v)) { + exp2RK(fs, v); + } + break; + case OPR_LT: + case OPR_LE: + case OPR_GT: + case OPR_GE: { + int[] dummy = new int[1]; + boolean[] dummy2 = new boolean[1]; + if (!isSCnumber(v, dummy, dummy2)) { + exp2AnyReg(fs, v); + } + break; + } + default: + throw new IllegalStateException("bad binopr " + op); + } + } + + /** {@code luaK_posfix} */ + public static void posfix(FuncState fs, int opr, ExpDesc e1, ExpDesc e2, int line) { + dischargeVars(fs, e2); + if (foldBinOp(opr) && constFolding(fs, opr + Arith.OP_ADD, e1, e2)) { + return; // 折叠掉了 + } + switch (opr) { + case OPR_AND: + concatF(fs, e2, e1.f); + e1.set(e2); + break; + case OPR_OR: + concatT(fs, e2, e1.t); + e1.set(e2); + break; + case OPR_CONCAT: + exp2NextReg(fs, e2); + codeConcat(fs, e1, e2, line); + break; + case OPR_ADD: + case OPR_MUL: + codeCommutative(fs, opr, e1, e2, line); + break; + case OPR_SUB: + if (finishBinExpNeg(fs, e1, e2, OpCode.OP_ADDI, line, Tm.TM_SUB)) { + break; // 编成了 (r1 + -I) + } + // FALLTHROUGH + case OPR_DIV: + case OPR_IDIV: + case OPR_MOD: + case OPR_POW: + codeArith(fs, opr, e1, e2, 0, line); + break; + case OPR_BAND: + case OPR_BOR: + case OPR_BXOR: + codeBitwise(fs, opr, e1, e2, line); + break; + case OPR_SHL: + if (isSCint(e1)) { + swapExps(e1, e2); + codeBinI(fs, OpCode.OP_SHLI, e1, e2, 1, line, Tm.TM_SHL); // I << r2 + } else if (finishBinExpNeg(fs, e1, e2, OpCode.OP_SHRI, line, Tm.TM_SHL)) { + // 编成了 (r1 >> -I) + } else { + codeBinExpVal(fs, opr, e1, e2, line); + } + break; + case OPR_SHR: + if (isSCint(e2)) { + codeBinI(fs, OpCode.OP_SHRI, e1, e2, 0, line, Tm.TM_SHR); // r1 >> I + } else { + codeBinExpVal(fs, opr, e1, e2, line); + } + break; + case OPR_EQ: + case OPR_NE: + codeEq(fs, opr, e1, e2); + break; + case OPR_GT: + case OPR_GE: + // (a > b) == (b < a);(a >= b) == (b <= a) + swapExps(e1, e2); + opr = (opr - OPR_GT) + OPR_LT; + // FALLTHROUGH + case OPR_LT: + case OPR_LE: + codeOrder(fs, opr, e1, e2); + break; + default: + throw new IllegalStateException("bad binopr " + opr); + } + } + + /* ---------- 表构造 ---------- */ + + /** {@code luaK_settablesize}:回填 NEWTABLE 的数组/哈希大小 */ + public static void setTableSize(FuncState fs, int pc, int ra, int asize, int hsize) { + int rb = (hsize != 0) ? ceilLog2(hsize) + 1 : 0; + int extra = asize / (OpCode.MAXARG_C + 1); // 数组大小的高位 + int rc = asize % (OpCode.MAXARG_C + 1); // 低位 + int k = (extra > 0) ? 1 : 0; + fs.f.setCode(pc, OpCode.createABCk(OpCode.OP_NEWTABLE, ra, rb, rc, k)); + fs.f.setCode(pc + 1, OpCode.createAx(OpCode.OP_EXTRAARG, extra)); + } + + /** + * {@code luaO_ceillog2}:不小于 {@code log2(x)} 的最小整数。 + *

+ * 上游用「先 {@code x--} 再查一张 256 项表」实现,那张表记的其实是 + * 「表示 x 需要几位」,所以整件事等价于 {@code bitlength(x - 1)}。 + * 直接写等价形式而不是照抄移位:照抄的版本我写错过一次 + * ({@code x = 2} 算出 0 而不是 1,于是 {@code {a = 1, b = 2}} 的 + * NEWTABLE 少算一格哈希)。 + */ + static int ceilLog2(int x) { + if (x <= 1) { + return 0; + } + return 32 - Integer.numberOfLeadingZeros(x - 1); + } + + /** {@code luaK_setlist} */ + public static void setList(FuncState fs, int base, int nelems, int tostore) { + if (tostore == MULTRET) { + tostore = 0; + } + if (nelems <= OpCode.MAXARG_C) { + codeABC(fs, OpCode.OP_SETLIST, base, tostore, nelems); + } else { + int extra = nelems / (OpCode.MAXARG_C + 1); + nelems %= (OpCode.MAXARG_C + 1); + codeABCk(fs, OpCode.OP_SETLIST, base, tostore, nelems, 1); + codeExtraArg(fs, extra); + } + fs.freereg = base + 1; // 列表里的值占用的寄存器释放掉 + } + + /* ---------- 收尾 ---------- */ + + /** {@code finaltarget}:跳过「跳到跳转」的链 */ + private static int finalTarget(int[] code, int i) { + for (int count = 0; count < 100; count++) { // 防止死循环 + int pc = code[i]; + if (OpCode.getOpCode(pc) != OpCode.OP_JMP) { + break; + } + i += OpCode.getArgSJ(pc) + 1; + } + return i; + } + + /** {@code luaK_finish}:最后一遍扫描,做小优化与回填 */ + public static void finish(FuncState fs) { + Proto p = fs.f; + for (int i = 0; i < fs.pc; i++) { + int inst = p.code[i]; + int op = OpCode.getOpCode(inst); + switch (op) { + case OpCode.OP_RETURN0: + case OpCode.OP_RETURN1: { + if (!(fs.needclose || p.isVararg)) { + break; // 没什么要补的 + } + inst = OpCode.setOpCode(inst, OpCode.OP_RETURN); + op = OpCode.OP_RETURN; + } + // FALLTHROUGH + case OpCode.OP_RETURN: + case OpCode.OP_TAILCALL: { + if (fs.needclose) { + inst = OpCode.setArgK(inst, 1); + } + if (p.isVararg) { + inst = OpCode.setArgC(inst, p.numparams + 1); + } + p.setCode(i, inst); + break; + } + case OpCode.OP_JMP: { + int target = finalTarget(p.code, i); + fixJump(fs, i, target); + break; + } + default: + break; + } + } + } + + /** {@code luaK_semerror}:语义错误不带 "near " */ + public static LuaError semError(FuncState fs, String msg) { + fs.ls.t.token = 0; + return fs.ls.syntaxError(msg); + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/CoroutineLib.java b/src/main/java/com/github/esrrhs/fakescript/lua/CoroutineLib.java new file mode 100644 index 0000000..dd95cce --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/CoroutineLib.java @@ -0,0 +1,286 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * 协程库 —— {@code lcorolib.c} 直译。 + *

+ * 没有用线程实现协程。Lua 协程是协作式的:同一时刻只有一个在跑,挂起 + * 点由 {@code coroutine.yield} 显式给出。C 版靠「每个 {@code lua_State} 一条 + * 独立栈 + {@code longjmp} 跳出」实现,这里同样靠「每个 {@link LuaState} 一条 + * 独立栈 + 异常跳出」实现,两者结构一一对应。 + *

+ * 不用虚拟线程还有一层理由:虚拟线程会让 {@code table.sort} 的比较函数里的 + * yield 变成合法的,而参考实现在那里报 + * {@code attempt to yield across a C-call boundary}。目标是「一行不改地跑现有 + * Lua 项目」,所以宁可和 C 一样保守。 + */ +public final class CoroutineLib { + + /* lcorolib.c 里的 auxstatus 返回值 */ + private static final int COS_RUN = 0; + private static final int COS_DEAD = 1; + private static final int COS_YIELD = 2; + private static final int COS_NORM = 3; + + private static final String[] STAT_NAME = {"running", "dead", "suspended", "normal"}; + + private CoroutineLib() { + } + + public static void open(LuaState L) { + LuaTable t = new LuaTable(); + setFn(t, "create", CoroutineLib::cocreate); + setFn(t, "resume", CoroutineLib::coresume); + setFn(t, "running", CoroutineLib::corunning); + setFn(t, "status", CoroutineLib::costatus); + setFn(t, "wrap", CoroutineLib::cowrap); + setFn(t, "yield", CoroutineLib::coyield); + setFn(t, "isyieldable", CoroutineLib::yieldable); + setFn(t, "close", CoroutineLib::coclose); + /* + * 进 _LOADED 而不只是 _G:参考实现里 require("coroutine") 拿到的 + * 就是这张表。少登记这一条,脚本里写 require("coroutine") 会直接 + * 报 module not found。 + */ + LuaAux.registerLib(L, "coroutine", t); + } + + private static void setFn(LuaTable t, String name, LuaFunction f) { + t.set(LuaString.latin1(name), LuaValue.ofFunction(f)); + } + + /** + * {@code lcorolib.c: getco}。 + *

+ * C 用的是 {@code luaL_argexpected(L, co, 1, "thread")},类型名取自 + * {@code luaL_typename(L, 1)} —— 缺参是 {@code no value},不是 + * {@code nil}。曾经直接拿 {@code L.arg(0)} 的 {@code typeName()}, + * 于是 {@code coroutine.status()} 报 {@code got nil},参考实现报 + * {@code got no value}。 + */ + private static LuaState getco(LuaState L, String fname) { + LuaValue v = L.arg(0); + if (L.isNoneArg(1) || v.tt != Lua.VTHREAD) { + throw L.libError("bad argument #1 to 'coroutine." + fname + "' (thread expected, got " + + L.argTypeName(1) + ")"); + } + return v.asThread(); + } + + /** + * {@code lcorolib.c: auxresume}。 + * + * @return 结果个数;{@code -1} 表示出错(错误对象已压在 {@code L} 上) + */ + private static int auxresume(LuaState L, LuaState co, int narg) { + /* + * 两处 {@code lua_checkstack} 都不许抛:失败要报的是 + * {@code too many arguments to resume} / {@code too many results to + * resume},不是 {@code stack overflow}。差在这两句上,上游 + * {@code coroutine.lua} 末尾 {@code table.unpack({}, 1, lim)} 那一组 + * 用例就过不去——参考实现给的是「结果太多搬不回来」, + * 而不是「协程自己栈溢出了」。 + */ + if (!co.checkStackOk(narg)) { + L.push(LuaValue.ofString(LuaString.latin1("too many arguments to resume"))); + return -1; + } + LuaState.xmove(L, co, narg); + int[] nres = new int[1]; + int status = co.resume(L, narg, nres); + if (status == LuaState.LUA_OK || status == LuaState.LUA_YIELD) { + if (!L.checkStackOk(nres[0] + 1)) { + co.pop(nres[0]); // lua_pop(co, nres):结果照样从协程上摘掉 + L.push(LuaValue.ofString(LuaString.latin1("too many results to resume"))); + return -1; + } + LuaState.xmove(co, L, nres[0]); + return nres[0]; + } + LuaState.xmove(co, L, 1); // 搬错误对象 + return -1; + } + + /** {@code luaB_coresume} */ + private static int coresume(LuaState L) { + LuaState co = getco(L, "resume"); + int r = auxresume(L, co, L.argCount() - 1); + if (r < 0) { + L.push(LuaValue.ofBoolean(false)); + insert(L, -2); + return 2; + } + L.push(LuaValue.ofBoolean(true)); + insert(L, -(r + 1)); + return r + 1; + } + + /** + * 把栈顶元素搬到 {@code idx} 处,上面的元素整体上移一格。 + *

+ * 对应 {@code lua_insert}。{@code resume} 要把 {@code true/false} 插到 + * 所有结果前面,就是这个操作。 + */ + private static void insert(LuaState L, int idx) { + int p = (idx < 0) ? L.top + idx : idx; + LuaValue v = new LuaValue(L.stack[L.top - 1]); + for (int i = L.top - 1; i > p; i--) { + L.stack[i].set(L.stack[i - 1]); + } + L.stack[p].set(v); + } + + /** {@code luaB_auxwrap} */ + private static int auxwrap(LuaState L) { + LuaState co = L.upvalue(0).asThread(); + int r = auxresume(L, co, L.argCount()); + if (r < 0) { + int stat = co.getStatus(); + if (stat != LuaState.LUA_OK && stat != LuaState.LUA_YIELD) { + /* + * {@code lua_closethread(co, L)} + {@code lua_xmove(co, L, 1)}。 + *

+ * 注意 {@code xmove} 是往 {@code L} 压一个值,不是覆盖: + * {@code auxresume} 搬过来的那个还在下面,真正被当作错误对象的 + * 是这次新压上来的栈顶。 + *

+ * 而 {@code luaE_resetthread} 要交出的是 + * {@code luaD_seterrorobj(L, status, L->stack + 1)} 的结果, + * 源头是「搬走一格之后的栈顶」——所以 {@code luaB_error} 必须照 + * C 版写(先 {@code settop(1)} 再 {@code where}+{@code concat}), + * 那一格才正好是带位置的错误对象。 + */ + LuaValue prev = new LuaValue(L.stack[L.top - 1]); // auxresume 刚搬上来的错误对象 + if (co.closeThreadValue(L, prev) != null) { + LuaState.xmove(co, L, 1); + } + } + LuaValue err = L.stack[L.top - 1]; + if (err.isString()) { + // luaL_where(L, 1) + concat:wrap 出来的函数崩了要带位置 + LuaString prefix = L.whereAt(1); + err = LuaValue.ofString(prefix.concat(err.asString())); + L.stack[L.top - 1].set(err); + /* + * {@code where} 非 null = 「位置已经加过了」。这里哪怕 + * {@code prefix} 是空串也要传非 null——{@code pcall} 见到 + * {@code null} 会再补一次位置,报出 + * {@code file:3: file:2: boom} 这种叠前缀。 + */ + throw new LuaError(err, prefix.toJavaString()); + } + throw new LuaError(err, ""); + } + return r; + } + + /** {@code luaB_cocreate} */ + private static int cocreate(LuaState L) { + createCo(L, "coroutine.create"); + return 1; + } + + /** + * {@code create} 与 {@code wrap} 的公共部分。 + *

+ * 报错里的函数名要由调用方给:{@code wrap} 内部调的就是 + * {@code luaB_cocreate},但参考实现报的是 + * {@code bad argument #1 to 'coroutine.wrap'}——因为 C 版的名字是从 + * 「这一帧是被谁调用的」倒推的,而 cocreate 是被 cowrap 直接调的, + * 倒推出来的名字是最外层那个 Lua 看得见的名字。 + */ + private static void createCo(LuaState L, String fname) { + LuaValue f = L.arg(0); + if (L.isNoneArg(1) || !f.isFunction()) { + /* + * 同样是 {@code luaL_argexpected}:缺参报 {@code got no value}。 + */ + throw L.libError("bad argument #1 to '" + fname + "' (function expected, got " + + L.argTypeName(1) + ")"); + } + LuaState nl = L.newThread(); + L.push(LuaValue.ofThread(nl)); // 这个才是 create 的返回值 + /* + * xmove 搬的是栈顶,所以必须先把函数复制一份压上来,否则搬走的 + * 是刚压的协程对象——那样 create 会返回上一个栈槽里的东西(多半就是 + * 那个函数),表现是 type(co) 变成 "function"。 + */ + L.push(new LuaValue(L.arg(0))); + LuaState.xmove(L, nl, 1); // 把函数搬到新线程的栈上 + } + + /** {@code luaB_cowrap} */ + private static int cowrap(LuaState L) { + createCo(L, "coroutine.wrap"); // 栈顶现在是新协程 + LuaValue co = new LuaValue(L.stack[L.top - 1]); + CClosure cl = new CClosure(CoroutineLib::auxwrap, 1); + cl.upvalue[0] = co; + L.stack[L.top - 1].set(LuaValue.ofCClosure(cl)); + return 1; + } + + /** {@code luaB_yield} */ + private static int coyield(LuaState L) { + L.yieldk(L.argCount()); + return 0; // 不可达:yieldk 一定抛 + } + + /** {@code lcorolib.c: auxstatus} */ + private static int auxStatus(LuaState L, LuaState co) { + if (L == co) { + return COS_RUN; + } + switch (co.getStatus()) { + case LuaState.LUA_YIELD: + return COS_YIELD; + case LuaState.LUA_OK: + if (co.hasFrames()) { + return COS_NORM; // 正在跑(被别人 resume 起来的) + } + return (co.getTop() == 0) ? COS_DEAD : COS_YIELD; // 初始状态 + default: + return COS_DEAD; + } + } + + /** {@code luaB_costatus} */ + private static int costatus(LuaState L) { + LuaState co = getco(L, "status"); + L.push(LuaValue.ofString(LuaString.latin1(STAT_NAME[auxStatus(L, co)]))); + return 1; + } + + /** {@code luaB_yieldable} */ + private static int yieldable(LuaState L) { + LuaState co = (L.argCount() == 0 || L.arg(0).isNone()) ? L : getco(L, "isyieldable"); + L.push(LuaValue.ofBoolean(co.isYieldable())); + return 1; + } + + /** {@code luaB_corunning} */ + private static int corunning(LuaState L) { + int ismain = L.pushThread(); + L.push(LuaValue.ofBoolean(ismain != 0)); + return 2; + } + + /** {@code luaB_close} */ + private static int coclose(LuaState L) { + LuaState co = getco(L, "close"); + int status = auxStatus(L, co); + switch (status) { + case COS_DEAD: + case COS_YIELD: { + LuaValue err = co.closeThreadValue(L); + if (err == null) { + L.push(LuaValue.ofBoolean(true)); + return 1; + } + L.push(LuaValue.ofBoolean(false)); + LuaState.xmove(co, L, 1); + return 2; + } + default: + throw L.libError("cannot close a " + STAT_NAME[status] + " coroutine"); + } + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/DebugLib.java b/src/main/java/com/github/esrrhs/fakescript/lua/DebugLib.java new file mode 100644 index 0000000..11c1be6 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/DebugLib.java @@ -0,0 +1,1433 @@ +package com.github.esrrhs.fakescript.lua; + +import java.nio.charset.StandardCharsets; + +/** + * {@code debug} 库 —— 对应 {@code ldblib.c},外加 {@code ldebug.c} 里那部分 + * 只在调试接口才用到的东西({@code lua_getstack} / {@code lua_getinfo} / + * {@code luaG_findlocal})与 {@code lauxlib.c} 的 {@code luaL_traceback}。 + *

+ * 分成两块搬: + *

    + *
  • 第一轮:{@code getinfo} / {@code getlocal} / {@code setlocal} / + * upvalue 那四个 / {@code traceback} / {@code getregistry} / + * 元表那两个 / userdata 那两个 / {@code setcstacklimit} / {@code debug};
  • + *
  • 第二轮:{@code sethook} / {@code gethook},以及它们要求的 + * {@code ldo.c} 的 {@code luaD_hook} 与 {@code ldebug.c} 的 + * {@code luaG_traceexec} / {@code luaG_tracecall}。调用点分散在 + * {@link Vm} 的主循环与 {@link LuaState} 的 {@code precallC} / + * {@code poscall} 里,见 {@link #dHook}。
  • + *
+ * + *

与错误信息的差别

+ * {@code LuaState} 里已经有 {@code findSetReg} / {@code basicGetObjName} / + * {@code funcNameFromCode}——那是给 {@code luaG_runerror} 拼 + * 「attempt to call a nil value (global 'x')」用的。看起来是同一套符号执行, + * 但取名的起点不同:错误信息问的是「当前这一帧在调什么」, + * 而 {@code getinfo('n')} 问的是「{@code ci} 是被上一帧怎么调的」 + * ({@code ci->previous})。混用会让 {@code debug.getinfo(2).name} 整体错一层。 + */ +public final class DebugLib { + + private DebugLib() { + } + + /** {@code lua_Debug} */ + public static final class LuaDebug { + /** 钩子事件:{@link Lua#LUA_HOOKCALL} 等。只有钩子路径会用到 */ + int event; + + /** {@code i_ci}:对应的调用帧 */ + CallInfo iCi; + + /** + * {@link #iCi} 所属的线程。 + *

+ * C 版不需要这个字段——{@code CallInfo} 里存的是栈指针, + * {@code luaL_traceback(co, ...)} 拿着协程的 {@code CallInfo} 也能直接 + * 解引用到协程的栈槽。这里 {@link CallInfo} 存的是下标,必须再记住是 + * 谁的下标,否则 {@code debug.traceback(co)} 会用主线程的栈去解释 + * 协程的帧号。 + */ + LuaState state; + LuaString source; + /** + * {@code luaO_chunkid} 的结果。 + *

+ * 这里存的是字节串而不是 Java String:chunk 名可以是任意字节 + * ({@code load(f, "\xff")} 合法),走 Java String 会被 UTF-8 解码 + * 替换成 {@code U+FFFD},{@code short_src} 与 + * {@code debug.traceback} 就都跟着错。 + */ + LuaString shortSrc; + int linedefined; + int lastlinedefined; + String what; + int currentline = -1; + int nups; + int nparams; + boolean isvararg; + boolean istailcall; + String name; + String namewhat = ""; + int ftransfer; + int ntransfer; + + /** + * 给 {@code funcnamefromcall} 传出名字用的「出参」。 + *

+ * C 版是 {@code const char **name} 直接指向 {@code ar->name};Java 没有 + * 指向字段的指针,用一个 {@code String[]} 当出参。这是 C 里 + * 「名字与名字种类分开返回」的惯例——{@code funcnamefromcode} 返回种类 + * 字符串({@code NULL} 表示没找到),名字本身另走一条通道。 + */ + final String[] nameHolder = new String[1]; + } + + /* ==================== 开库 ==================== */ + + public static void open(LuaState L) { + LuaTable d = new LuaTable(); + LuaAux.setFn(d, "getregistry", DebugLib::getRegistry); + LuaAux.setFn(d, "getmetatable", DebugLib::getMetatable); + LuaAux.setFn(d, "setmetatable", DebugLib::setMetatable); + LuaAux.setFn(d, "getuservalue", DebugLib::getUserValue); + LuaAux.setFn(d, "setuservalue", DebugLib::setUserValue); + LuaAux.setFn(d, "getinfo", DebugLib::getInfoFn); + LuaAux.setFn(d, "getlocal", DebugLib::getLocalFn); + LuaAux.setFn(d, "setlocal", DebugLib::setLocalFn); + LuaAux.setFn(d, "getupvalue", DebugLib::getUpValue); + LuaAux.setFn(d, "setupvalue", DebugLib::setUpValue); + LuaAux.setFn(d, "upvalueid", DebugLib::upValueId); + LuaAux.setFn(d, "upvaluejoin", DebugLib::upValueJoin); + LuaAux.setFn(d, "traceback", DebugLib::traceback); + LuaAux.setFn(d, "setcstacklimit", DebugLib::setCStackLimit); + LuaAux.setFn(d, "debug", DebugLib::debugFn); + LuaAux.setFn(d, "sethook", DebugLib::setHook); + LuaAux.setFn(d, "gethook", DebugLib::getHook); + LuaAux.registerLib(L, "debug", d); + } + + /* ==================== ldebug.c:栈帧与信息 ==================== */ + + /** + * {@code lua_getstack}:回跳 {@code level} 层。 + *

+ * {@code level < 0} 直接失败;{@code level == 0} 是「主 chunk 外的那一层」, + * 也就是 C 的 {@code base_ci},同样失败——所以 + * {@code debug.getinfo(2)} 在只有一层时给 {@code nil}。 + */ + static boolean getStack(LuaState L, int level, LuaDebug ar) { + if (level < 0) { + return false; + } + CallInfo ci = L.ci; + while (level > 0 && ci != null && ci.previous != null) { + ci = ci.previous; + level--; + } + if (level == 0 && ci != null && ci.previous != null) { + ar.iCi = ci; + ar.state = L; + return true; + } + return false; + } + + /** {@code currentpc}:{@code savedpc} 是「下一条」,当前这条要减一 */ + private static int currentPc(CallInfo ci) { + return (ci.savedpc > 0) ? ci.savedpc - 1 : 0; + } + + /** + * {@code getcurrentline}:{@code ldebug.c} 的 {@code currentline}, + * 但按 {@link #auxGetInfo} 的要求先判 {@code isLua}——C 版把 + * {@code lua_assert(isLua(ci))} 放在 {@code currentpc} 里,调用点在外层判。 + */ + private static int currentLine(LuaState L, CallInfo ci) { + if (!ci.isLua) { + return -1; + } + LuaValue f = L.stack[ci.func]; + if (f.tt != Lua.VLIF) { + return -1; + } + return ((LClosure) f.o).p.getFuncline(currentPc(ci)); + } + + /* ==================== ldebug.c:名字 ==================== */ + + /** {@code funcinfo}:源名、行号范围、what */ + private static void funcInfo(LuaDebug ar, Object cl) { + if (!(cl instanceof LClosure)) { + ar.source = LuaString.latin1("=[C]"); + ar.linedefined = -1; + ar.lastlinedefined = -1; + ar.what = "C"; + } else { + Proto p = ((LClosure) cl).p; + ar.source = (p.source == null) ? LuaString.latin1("=?") : p.source; + ar.linedefined = p.linedefined; + ar.lastlinedefined = p.lastlinedefined; + ar.what = (p.linedefined == 0) ? "main" : "Lua"; + } + ar.shortSrc = LuaString.of(LuaState.chunkId(ar.source.bytes)); + } + + /** {@code getfuncname}:被调函数是怎么被上一帧调的 */ + private static String getFuncName(LuaState L, CallInfo ci, String[] name) { + if (ci != null && !ci.tail) { + return funcNameFromCall(L, ci.previous, name); + } + return null; + } + + /** + * {@code funcnamefromcall}。 + */ + private static String funcNameFromCall(LuaState L, CallInfo ci, String[] name) { + /* + * 顺序照抄上游:{@code CIST_HOOKED} 在 {@code isLua} 之前判。 + * 这不是风格问题——被钩子跟踪的那一帧常常是 C 帧(比如 + * {@code pcall} 的调用钩子是在 pcall 自己的 C 帧上打的标记), + * 挪到 {@code isLua} 之后就永远走不到,回溯里那一层会错印成 + * {@code in function <文件:行>} 而不是 {@code in hook '?'}。 + */ + if (ci == null) { + return null; + } + if (ci.hooked) { + name[0] = "?"; + return "hook"; + } + if (ci.fin) { + name[0] = "__gc"; + return "metamethod"; + } + // base_ci 就是这条分支的常客:它的 isLua 为 false,等价于 C 的 + // "ci->callstatus & CIST_C",于是「主 chunk 是被谁调的」永远没有名字 + if (!ci.isLua) { + return null; + } + LuaValue f = L.stack[ci.func]; + if (f.tt != Lua.VLIF) { + return null; + } + return LuaState.funcNameFromCode(((LClosure) f.o).p, currentPc(ci), name); + } + + /* ==================== ldebug.c:getinfo ==================== */ + + /** {@code auxgetinfo} */ + private static boolean auxGetInfo(LuaState L, String what, LuaDebug ar, Object cl, CallInfo ci) { + boolean status = true; + for (int i = 0; i < what.length(); i++) { + switch (what.charAt(i)) { + case 'S': + funcInfo(ar, cl); + break; + case 'l': + ar.currentline = (ci != null && ci.isLua) ? currentLine(L, ci) : -1; + break; + case 'u': + ar.nups = nupvaluesOf(cl); + if (!(cl instanceof LClosure)) { + ar.isvararg = true; + ar.nparams = 0; + } else { + Proto p = ((LClosure) cl).p; + ar.isvararg = p.isVararg; + ar.nparams = p.numparams; + } + break; + case 't': + ar.istailcall = (ci != null) && ci.tail; + break; + case 'n': { + String kind = getFuncName(L, ci, ar.nameHolder); + if (kind == null) { + ar.namewhat = ""; + ar.name = null; + } else { + ar.namewhat = kind; + ar.name = ar.nameHolder[0]; + } + break; + } + case 'r': + /* + * 传递信息只在返回钩子期间有效:{@code luaD_hook} 进钩子前 + * 把 {@code CIST_TRAN} 打在这一帧上、出来就清掉。所以 + * {@code debug.getinfo(2, 'r')} 在钩子里能读到返回值个数, + * 在普通代码里一律是 0/0。 + */ + if (ci == null || !ci.tran) { + ar.ftransfer = 0; + ar.ntransfer = 0; + } else { + ar.ftransfer = ci.ftransfer; + ar.ntransfer = ci.ntransfer; + } + break; + case 'L': + case 'f': + break; // 由 getInfo 处理(它们要往栈上推东西) + default: + status = false; + break; + } + } + return status; + } + + /** 取闭包的 upvalue 个数:轻量 C 函数没有闭包,给 0 */ + private static int nupvaluesOf(Object cl) { + if (cl instanceof LClosure) { + return ((LClosure) cl).p.sizeupvalues; + } + if (cl instanceof CClosure) { + return ((CClosure) cl).upvalue.length; + } + return 0; + } + + /** + * {@code lua_getinfo}。 + *

+ * {@code what} 以 {@code >} 开头表示「信息取自栈顶那个函数」而不是某个栈层。 + */ + static boolean getInfo(LuaState L, String what, LuaDebug ar) { + CallInfo ci; + LuaValue func; + if (!what.isEmpty() && what.charAt(0) == '>') { + ci = null; + func = new LuaValue(L.stack[L.top - 1]); + L.top--; // 弹出函数('f' 分支会把它推回来) + what = what.substring(1); + } else { + ci = ar.iCi; + /* + * 必须拷出来:这里拿到的是栈上那个活的槽对象,而下面 + * L.push(func) 若推的是同一个引用,表里就存了一个会随栈变化的别名。 + * C 版是 setobj2s 的按值复制,等价的是 new LuaValue(...)。 + */ + func = new LuaValue(L.stack[ci.func]); + } + Object cl = null; + if (func.tt == Lua.VLIF) { + cl = func.o; + } else if (func.tt == Lua.VCCL) { + cl = func.o; + } + // VLCF(轻量 C 函数)没有闭包对象,cl 保持 null —— 与 C 的 + // "ttisclosure(func) ? clvalue(func) : NULL" 一致 + boolean status = auxGetInfo(L, what, ar, cl, ci); + if (what.indexOf('f') >= 0) { + L.push(func); + } + if (what.indexOf('L') >= 0) { + L.push(collectValidLines(cl)); + } + return status; + } + + /** {@code collectvalidlines}:所有「有指令落在上面」的行 */ + private static LuaValue collectValidLines(Object cl) { + if (!(cl instanceof LClosure)) { + return LuaValue.ofNil(); + } + Proto p = ((LClosure) cl).p; + LuaTable t = new LuaTable(); + if (p.sizelineinfo > 0) { + int currentline = p.linedefined; + int i; + if (!p.isVararg) { + i = 0; + } else { + // 变参函数的第一条是 OP_VARARGPREP,不算 + currentline = nextLine(p, currentline, 0); + i = 1; + } + for (; i < p.sizelineinfo; i++) { + currentline = nextLine(p, currentline, i); + t.set(currentline, LuaValue.ofBoolean(true)); + } + } + return LuaValue.ofTable(t); + } + + /** {@code nextline}:{@code ABSLINEINFO} 那一条的行号存在 abslineinfo 里 */ + private static int nextLine(Proto p, int currentline, int pc) { + if (pc < p.lineinfo.length && p.lineinfo[pc] != Proto.ABSLINEINFO) { + return currentline + p.lineinfo[pc]; + } + return p.getFuncline(pc); + } + + /* ==================== ldebug.c:局部变量 ==================== */ + + /** {@code findvararg}:负下标访问变长参数 */ + private static String findVarArg(LuaState L, CallInfo ci, int n, int[] posOut) { + LuaValue f = L.stack[ci.func]; + if (f.tt == Lua.VLIF && ((LClosure) f.o).p.isVararg) { + int nextra = ci.nextraargs; + if (n >= -nextra) { + posOut[0] = ci.func - nextra - (n + 1); + return "(vararg)"; + } + } + return null; + } + + /** {@code luaG_findlocal} */ + private static String findLocal(LuaState L, CallInfo ci, int n, int[] posOut) { + int base = ci.func + 1; + String name = null; + if (ci.isLua) { + if (n < 0) { + return findVarArg(L, ci, n, posOut); + } + LuaValue f = L.stack[ci.func]; + if (f.tt == Lua.VLIF) { + LuaString s = ((LClosure) f.o).p.getLocalName(n, currentPc(ci)); + if (s != null) { + name = s.toLatin1String(); + } + } + } + if (name == null) { + int limit = (ci == L.ci) ? L.top : nextFrameFunc(L, ci); + if (limit - base >= n && n > 0) { + name = ci.isLua ? "(temporary)" : "(C temporary)"; + } else { + return null; + } + } + posOut[0] = base + (n - 1); + return name; + } + + /** + * C 的 {@code ci->next->func.p}:本帧里面那一层(被调用者)的起点。 + *

+ * 本实现只存了 {@code previous} 链,{@code next} 要反着找回来。 + */ + private static int nextFrameFunc(LuaState L, CallInfo ci) { + for (CallInfo c = L.ci; c != null; c = c.previous) { + if (c.previous == ci) { + return c.func; + } + } + return L.top; + } + + /** {@code lua_getlocal} */ + private static String getLocal(LuaState L, LuaDebug ar, int n) { + if (ar == null) { + // 查非活动函数:只看入口处还活着的参数 + LuaValue f = L.stack[L.top - 1]; + if (f.tt != Lua.VLIF) { + return null; + } + LuaString s = ((LClosure) f.o).p.getLocalName(n, 0); + return (s == null) ? null : s.toLatin1String(); + } + int[] pos = new int[1]; + String name = findLocal(L, ar.iCi, n, pos); + if (name != null) { + L.push(new LuaValue(L.stack[pos[0]])); + } + return name; + } + + /** {@code lua_setlocal} */ + private static String setLocal(LuaState L, LuaDebug ar, int n) { + int[] pos = new int[1]; + String name = findLocal(L, ar.iCi, n, pos); + if (name != null) { + L.stack[pos[0]].set(L.stack[L.top - 1]); + L.top--; + } + return name; + } + + /* ==================== ldebug.c:upvalue ==================== */ + + /** {@code auxupvalue}:返回名字,{@code valOut[0]} 是那个值的槽 */ + private static String auxUpValue(LuaValue f, int n, LuaValue[] valOut, Object[] ownerOut) { + if (f.tt == Lua.VCCL) { + CClosure c = (CClosure) f.o; + if (n <= 0 || n > c.upvalue.length) { + return null; + } + valOut[0] = c.upvalue[n - 1]; + if (ownerOut != null) { + /* + * C 的 {@code lua_upvalueid} 给 C 闭包返回的是 + * {@code &f->upvalue[n-1]}——槽的地址,不是闭包本身。 + * 这里槽对象是 {@link LuaValue} 且永不被替换(只 {@code set}), + * 身份稳定,与 C 的取址等价。注意与 {@code aux_upvalue} 的 + * {@code *owner = obj2gco(f)} 不同:那是给写屏障用的,我们不做。 + */ + ownerOut[0] = c.upvalue[n - 1]; + } + return ""; + } + if (f.tt == Lua.VLIF) { + LClosure cl = (LClosure) f.o; + Proto p = cl.p; + if (n <= 0 || n > p.sizeupvalues) { + return null; + } + valOut[0] = cl.upvals[n - 1].value(); + if (ownerOut != null) { + ownerOut[0] = cl.upvals[n - 1]; + } + /* + * 这里不能用 {@link LuaState#upvalName}。C 版有两个不同的函数: + * {@code lapi.c: aux_upvalue} 在名字为空时给 {@code "(no name)"}, + * 而 {@code ldebug.c: upvalname} 给 {@code "?"}。差别只在调试信息被 + * {@code string.dump(f, true)} 剥掉时才看得见——上游 + * {@code db.lua} 断言 {@code debug.getupvalue(f, 1)} 拿到 + * {@code "(no name)"},给 {@code "?"} 就过不去。 + */ + LuaString nm = (p.upvalues[n - 1] != null) ? p.upvalues[n - 1].name : null; + return (nm == null) ? "(no name)" : nm.toLatin1String(); + } + return null; + } + + /* ==================== ldblib.c:各函数 ==================== */ + + /** {@code getthread}:第一个参数可能是协程 */ + private static LuaState getThread(LuaState L, int[] arg) { + if (!L.isNoneArg(1) && L.arg1(1).tt == Lua.VTHREAD) { + arg[0] = 1; + return (LuaState) L.arg1(1).o; + } + arg[0] = 0; + return L; + } + + private static int getRegistry(LuaState L) { + L.push(LuaValue.ofTable(L.registry)); + return 1; + } + + private static int getMetatable(LuaState L) { + LuaAux.checkAny(L, 1); + LuaTable mt = LuaTm.metatableOf(L, L.arg1(1)); + if (mt == null) { + L.push(LuaValue.ofNil()); + } else { + L.push(LuaValue.ofTable(mt)); + } + return 1; + } + + private static int setMetatable(LuaState L) { + /* + * 判的是「第 2 个参数的原始类型」,不能先把缺省折算成 nil: + * C 版是 {@code int t = lua_type(L, 2)}——参数不存在时 t 是 + * {@code LUA_TNONE}(-1),一样报错。先折成 nil 再判会让 + * {@code debug.setmetatable()} 静默通过,而参考实现报 + * {@code nil or table expected, got no value}。 + */ + int t = L.isNoneArg(2) ? Lua.TNONE : L.arg1(2).baseType(); + LuaAux.argExpected(L, t == Lua.TNIL || t == Lua.TTABLE, 2, "nil or table"); + L.setTop(2); // 第二个参数不存在时补出一个 nil + LuaValue v = L.arg1(2); + LuaValue obj = L.arg1(1); + if (v.isNil()) { + LuaTm.setMetatable(L, obj, null); + } else { + LuaTm.setMetatable(L, obj, (LuaTable) v.o); + } + L.setTop(1); + return 1; + } + + private static int getUserValue(LuaState L) { + int n = (int) LuaAux.optInteger(L, 2, 1); + if (L.isNoneArg(1) || L.arg1(1).tt != Lua.VUSERDATA) { + L.push(LuaValue.ofNil()); + return 1; + } + // 本实现的 userdata 不带用户值(lua_newuserdatauv 的 nuvalue 总为 0) + L.push(LuaValue.ofNil()); + return 1; + } + + private static int setUserValue(LuaState L) { + /* + * 序号取在第 3 个参数上({@code luaL_optinteger(L, 3, 1)}),而且要在 + * 类型检查之前——所以 {@code debug.setuservalue(1, 2, "x")} 报的是 + * 「#3 不是数」,不是「#1 不是 userdata」。 + */ + LuaAux.optInteger(L, 3, 1); + LuaAux.checkType(L, 1, Lua.TUSERDATA); + LuaAux.checkAny(L, 2); + L.setTop(2); + L.push(LuaValue.ofNil()); // 本实现的 userdata 不带用户值,必失败 + return 1; + } + + private static int getInfoFn(LuaState L) { + int[] argHolder = new int[1]; + LuaState L1 = getThread(L, argHolder); + int arg = argHolder[0]; + LuaString optStr = LuaAux.optString(L, arg + 2, LuaString.latin1("flnSrtu")); + /* + * what 是 const char*:C 的 lua_getinfo 扫到第一个 \0 就收工,后面那几个 + * 字符连看都不看。不截断的话 debug.getinfo(f, "S\0ln") 会把 'l' 'n' + * 也当成 option 解析 —— 而它们在这里是合法的、在别处却可能不合法, + * 于是凭空多出一条 "invalid option"。 + */ + String options = optStr.toCString().toJavaString(); + LuaAux.argCheck(L, options.isEmpty() || options.charAt(0) != '>', arg + 2, + "invalid option '>'"); + LuaDebug ar = new LuaDebug(); + if (!L.isNoneArg(arg + 1) && L.arg1(arg + 1).baseType() == Lua.TFUNCTION) { + options = ">" + options; + // 把函数放到 L1 的栈顶 + LuaValue f = new LuaValue(L.arg1(arg + 1)); + if (L1 == L) { + L.push(f); + } else { + L1.push(f); + } + } else { + int level = (int) LuaAux.checkInteger(L, arg + 1); + if (!getStack(L1, level, ar)) { + L.push(LuaValue.ofNil()); // 层号越界 + return 1; + } + } + if (!getInfo(L1, options, ar)) { + throw L.argError(arg + 2, "invalid option"); + } + LuaTable t = new LuaTable(); + L.push(LuaValue.ofTable(t)); + if (options.indexOf('S') >= 0) { + t.set("source", LuaValue.ofString(ar.source)); + t.set("short_src", LuaValue.ofString(ar.shortSrc)); + t.set("linedefined", LuaValue.ofInteger(ar.linedefined)); + t.set("lastlinedefined", LuaValue.ofInteger(ar.lastlinedefined)); + t.set("what", LuaValue.ofString(LuaString.utf8(ar.what))); + } + if (options.indexOf('l') >= 0) { + t.set("currentline", LuaValue.ofInteger(ar.currentline)); + } + if (options.indexOf('u') >= 0) { + t.set("nups", LuaValue.ofInteger(ar.nups)); + t.set("nparams", LuaValue.ofInteger(ar.nparams)); + t.set("isvararg", LuaValue.ofBoolean(ar.isvararg)); + } + if (options.indexOf('n') >= 0) { + t.set("name", (ar.name == null) ? LuaValue.ofNil() + : LuaValue.ofString(LuaString.latin1(ar.name))); + t.set("namewhat", LuaValue.ofString(LuaString.latin1(ar.namewhat))); + } + if (options.indexOf('r') >= 0) { + t.set("ftransfer", LuaValue.ofInteger(ar.ftransfer)); + t.set("ntransfer", LuaValue.ofInteger(ar.ntransfer)); + } + if (options.indexOf('t') >= 0) { + t.set("istailcall", LuaValue.ofBoolean(ar.istailcall)); + } + // 'L' 与 'f' 的结果已经压在 L1 栈顶,取下来装进表 + if (options.indexOf('L') >= 0) { + popIntoField(L, L1, t, "activelines"); + } + if (options.indexOf('f') >= 0) { + popIntoField(L, L1, t, "func"); + } + return 1; + } + + /** + * {@code treatstackoption}。 + *

+ * C 版是 {@code lua_rotate(L, -2, 1)} 再 {@code lua_setfield(L, -2, fname)}: + * 结果表是后压的,{@code lua_getinfo} 的产物在它底下,所以要先 + * 交换再 {@code setfield}。直接 {@code L.top--} 弹掉的是结果表本人, + * 于是 {@code debug.getinfo(1).func} 里存的成了那张表自己, + * 而函数返回的是表底下的东西——症状是 {@code debug.getinfo(1)} 返回一个 + * 奇怪的函数而不是表。 + */ + private static void popIntoField(LuaState L, LuaState L1, LuaTable t, String fname) { + LuaValue v; + if (L == L1) { + // 栈形如 [..., obj, table]:把 obj 从 table 底下掏出来,再复位栈顶 + v = new LuaValue(L.stack[L.top - 2]); + L.top -= 2; + L.push(LuaValue.ofTable(t)); + } else { + v = new LuaValue(L1.stack[--L1.top]); + } + t.set(fname, v); + } + + private static int getLocalFn(LuaState L) { + int[] argHolder = new int[1]; + LuaState L1 = getThread(L, argHolder); + int arg = argHolder[0]; + int nvar = (int) LuaAux.checkInteger(L, arg + 2); + if (!L.isNoneArg(arg + 1) && L.arg1(arg + 1).baseType() == Lua.TFUNCTION) { + L.push(new LuaValue(L.arg1(arg + 1))); + String name = getLocal(L, null, nvar); + L.push((name == null) ? LuaValue.ofNil() : LuaValue.ofString(LuaString.utf8(name))); + return 1; + } + int level = (int) LuaAux.checkInteger(L, arg + 1); + LuaDebug ar = new LuaDebug(); + if (!getStack(L1, level, ar)) { + throw L.argError(arg + 1, "level out of range"); + } + String name = getLocal(L1, ar, nvar); + if (name != null) { + LuaValue v; + if (L1 == L) { + v = new LuaValue(L.stack[--L.top]); + } else { + v = new LuaValue(L1.stack[--L1.top]); + } + L.push((name == null) ? LuaValue.ofNil() : LuaValue.ofString(LuaString.utf8(name))); + L.push(v); + return 2; + } + L.push(LuaValue.ofNil()); + return 1; + } + + private static int setLocalFn(LuaState L) { + int[] argHolder = new int[1]; + LuaState L1 = getThread(L, argHolder); + int arg = argHolder[0]; + int level = (int) LuaAux.checkInteger(L, arg + 1); + int nvar = (int) LuaAux.checkInteger(L, arg + 2); + LuaDebug ar = new LuaDebug(); + if (!getStack(L1, level, ar)) { + throw L.argError(arg + 1, "level out of range"); + } + LuaAux.checkAny(L, arg + 3); + L.setTop(arg + 3); + LuaValue v = new LuaValue(L.stack[L.top - 1]); + if (L1 == L) { + L.push(v); + } else { + L1.push(v); + } + String name = setLocal(L1, ar, nvar); + if (name == null) { + L1.top--; // 没写进去,把值弹掉 + } + L.push((name == null) ? LuaValue.ofNil() : LuaValue.ofString(LuaString.utf8(name))); + return 1; + } + + /** {@code auxupvalue} 的两个入口共用的实现 */ + private static int auxUpValueFn(LuaState L, boolean get) { + int n = (int) LuaAux.checkInteger(L, 2); + LuaAux.checkType(L, 1, Lua.TFUNCTION); + LuaValue f = L.arg1(1); + LuaValue[] val = new LuaValue[1]; + String name = auxUpValue(f, n, val, null); + if (name == null) { + return 0; + } + if (get) { + L.push(new LuaValue(val[0])); + L.push(LuaValue.ofString(LuaString.latin1(name))); + // lua_insert(L, -(get+1)):名字挪到值下面,打印出来才是「名字 值」 + LuaValue nm = new LuaValue(L.stack[L.top - 1]); + L.stack[L.top - 1].set(L.stack[L.top - 2]); + L.stack[L.top - 2].set(nm); + return 2; + } + LuaValue v = new LuaValue(L.stack[L.top - 1]); + L.top--; + val[0].set(v); + L.push(LuaValue.ofString(LuaString.latin1(name))); + return 1; + } + + private static int getUpValue(LuaState L) { + return auxUpValueFn(L, true); + } + + private static int setUpValue(LuaState L) { + LuaAux.checkAny(L, 3); + return auxUpValueFn(L, false); + } + + private static int upValueId(LuaState L) { + int n = (int) LuaAux.checkInteger(L, 2); + LuaAux.checkType(L, 1, Lua.TFUNCTION); + LuaValue f = L.arg1(1); + LuaValue[] val = new LuaValue[1]; + Object[] owner = new Object[1]; + String name = auxUpValue(f, n, val, owner); + if (name == null || owner[0] == null) { + L.push(LuaValue.ofNil()); + return 1; + } + L.push(LuaValue.ofLightUserdata(owner[0])); + return 1; + } + + private static int upValueJoin(LuaState L) { + int n1 = (int) LuaAux.checkInteger(L, 2); + LuaAux.checkType(L, 1, Lua.TFUNCTION); + LuaValue f1 = L.arg1(1); + Object[] owner1 = new Object[1]; + if (auxUpValue(f1, n1, new LuaValue[1], owner1) == null) { + throw L.argError(2, "invalid upvalue index"); + } + int n2 = (int) LuaAux.checkInteger(L, 4); + LuaAux.checkType(L, 3, Lua.TFUNCTION); + LuaValue f2 = L.arg1(3); + Object[] owner2 = new Object[1]; + if (auxUpValue(f2, n2, new LuaValue[1], owner2) == null) { + throw L.argError(4, "invalid upvalue index"); + } + LuaAux.argCheck(L, f1.tt == Lua.VLIF, 1, "Lua function expected"); + LuaAux.argCheck(L, f2.tt == Lua.VLIF, 3, "Lua function expected"); + ((LClosure) f1.o).setUpVal(n1 - 1, ((LClosure) f2.o).upvals[n2 - 1]); + return 0; + } + + /* ==================== 钩子 ==================== */ + + /** + * 注册表里那张「线程 → 钩子函数」的表。 + *

+ * 为什么不直接把函数存在 {@code lua_State} 上:{@code debug.sethook} 允许给 + * 别的线程设钩子({@code debug.sethook(co, f, "l")}),而每个线程的 + * 钩子是各自独立的。用线程做键的表正好表达这一点。 + */ + private static final LuaString HOOKKEY = LuaString.latin1("_HOOKKEY"); + + /** 事件名的顺序必须与 {@code Lua.LUA_HOOK*} 的取值一一对应 */ + private static final String[] HOOKNAMES = + { "call", "return", "line", "count", "tail call" }; + + /** + * {@code ldblib.c: hookf} —— {@code debug.sethook} 装的那个钩子。 + *

+ * 它是在「被跟踪的那一帧」上被调用的,{@code ar.iCi} 就指向那一帧; + * 这里真正做的是「查出用户注册的那个 Lua 函数并调它」。 + *

+ * 用 {@code callNoYield} 而不是 {@code call}:C 版这里是 + * {@code lua_call},而 {@code lua_call} 走 {@code luaD_callnoyield}, + * 会把「不可 yield」的层数加一。后果是钩子函数里 yield 一律报错—— + * 实测参考实现:主线程上是 + * {@code attempt to yield from outside a coroutine},协程里是 + * {@code attempt to yield across a C-call boundary},两条都让程序/协程死掉。 + * 换成 {@code call} 就会「成功 yield」,与参考实现相反。 + */ + private static final LuaHook HOOKF = DebugLib::hookf; + + private static void hookf(LuaState L, LuaDebug ar) { + LuaValue ht = L.registry.get(HOOKKEY); + if (ht == null || ht.tt != Lua.TTABLE) { + return; + } + LuaValue fn = ht.asTable().get(LuaValue.ofThread(L)); + if (fn == null || fn.baseType() != Lua.TFUNCTION) { + return; + } + int f = L.top; + L.checkStack(3); + L.stack[L.top++].set(fn); + L.stack[L.top++].set(LuaValue.ofString(LuaString.utf8(HOOKNAMES[ar.event]))); + if (ar.currentline >= 0) { + L.stack[L.top++].set(LuaValue.ofInteger(ar.currentline)); + } else { + L.stack[L.top++].setNil(); + } + getInfo(L, "lS", ar); // 补齐 currentline 与 source,钩子常用 + L.callNoYield(f, 0); + } + + /** + * {@code makemask}:{@code "crl"} 这样的字符串转成掩码。 + *

+ * C 用的是 {@code strchr(smask, 'c')} 而 {@code smask} 是 {@code const char*}, + * 所以第一个 {@code \0} 之后的字符看不见:{@code "c\0l"} 只装调用钩子, + * {@code "l\0c"} 只装行钩子。实测两者打出来的计数完全相反。 + */ + private static int makeMask(LuaString smask, int count) { + String s = smask.toCString().toJavaString(); + int mask = 0; + if (s.indexOf('c') >= 0) { + mask |= Lua.LUA_MASKCALL; + } + if (s.indexOf('r') >= 0) { + mask |= Lua.LUA_MASKRET; + } + if (s.indexOf('l') >= 0) { + mask |= Lua.LUA_MASKLINE; + } + if (count > 0) { + mask |= Lua.LUA_MASKCOUNT; + } + return mask; + } + + /** + * {@code unmakemask}:掩码转回字符串。 + *

+ * 只输出 {@code c / r / l}——计数是单独一个返回值,不占字符。 + */ + private static String unmakeMask(int mask) { + StringBuilder b = new StringBuilder(); + if ((mask & Lua.LUA_MASKCALL) != 0) { + b.append('c'); + } + if ((mask & Lua.LUA_MASKRET) != 0) { + b.append('r'); + } + if ((mask & Lua.LUA_MASKLINE) != 0) { + b.append('l'); + } + return b.toString(); + } + + /** {@code luaL_getsubtable} 的等价物,拿不到就建一张弱键表 */ + private static LuaTable hookTable(LuaState L) { + LuaValue v = L.registry.get(HOOKKEY); + if (v != null && v.tt == Lua.TTABLE) { + return (LuaTable) v.o; + } + LuaTable t = new LuaTable(); + // 弱键:协程死后不该被钩子表吊住;以自己为元表是上游的写法 + t.set("__mode", LuaValue.ofString(LuaString.utf8("k"))); + t.setMetatable(t); + L.registry.set(HOOKKEY, LuaValue.ofTable(t)); + return t; + } + + private static int setHook(LuaState L) { + int[] argHolder = new int[1]; + LuaState L1 = getThread(L, argHolder); + int arg = argHolder[0]; + LuaHook func; + int mask; + int count; + if (L.isNoneArg(arg + 1) || L.arg1(arg + 1).isNil()) { + L.setTop(arg + 1); + func = null; + mask = 0; + count = 0; + } else { + /* + * 先取掩码再查第 1 个参数的类型:C 版 + * {@code luaL_checkstring(L, arg+2)} 在前,所以 + * {@code debug.sethook(1, 2)} 报的是「#2 不是字符串」, + * 而不是「#1 不是函数」。 + */ + LuaString smask = LuaAux.checkString(L, arg + 2); + LuaAux.checkType(L, arg + 1, Lua.TFUNCTION); + count = (int) LuaAux.optInteger(L, arg + 3, 0); + func = HOOKF; + mask = makeMask(smask, count); + } + LuaTable ht = hookTable(L); + ht.set(LuaValue.ofThread(L1), new LuaValue(L.arg1(arg + 1))); + L1.setHook(func, mask, count); + return 0; + } + + private static int getHook(LuaState L) { + int[] argHolder = new int[1]; + LuaState L1 = getThread(L, argHolder); + int arg = argHolder[0]; + int mask = L1.getHookMask(); + LuaHook hook = L1.getHook(); + if (hook == null) { + L.push(LuaValue.ofNil()); // luaL_pushfail + return 1; // 没有钩子时只有一个返回值 + } else if (hook != HOOKF) { + L.push(LuaValue.ofString(LuaString.utf8("external hook"))); + } else { + LuaTable ht = hookTable(L); + LuaValue f = ht.get(LuaValue.ofThread(L1)); // rawget 的等价物 + L.push((f == null) ? LuaValue.ofNil() : new LuaValue(f)); + } + L.push(LuaValue.ofString(LuaString.utf8(unmakeMask(mask)))); + L.push(LuaValue.ofInteger(L1.getHookCount())); + return 3; + } + + /* ---------- ldo.c / ldebug.c:钩子的调用点 ---------- */ + + /** + * {@code ldo.c: luaD_hook}。 + *

+ * 两处「先扩大再收窄」是本函数的要害: + *

    + *
  • 进来时若 {@code L.top < ci.top} 就抬到 {@code ci.top}——Lua 帧的 + * {@code top} 是「本帧可能用到的最高处」,此时栈上那些值都还活着, + * 钩子(以及它内部可能的栈扩容)不能把它们盖掉;
  • + *
  • 再保证 {@code ci.top} 至少是 {@code L.top + LUA_MINSTACK}——钩子自己 + * 要起新帧,得给它留出 C 函数保证的那 20 个槽。
  • + *
+ * 出去时要把这三个都还原:{@code L.top}、{@code ci.top}、 + * {@code CIST_HOOKED}。 + *

+ * 钩子抛错时不还原。C 版在这里 {@code luaD_throw} 一跳到底,还原动作 + * 全被跳过;{@code allowhook} 与 {@code CIST_HOOKED} 要到 + * {@code luaD_pcall} 才恢复。这个差别是能观测到的:钩子函数出错时参考实现的 + * 回溯里那一层印成 {@code in hook '?'},而「出错立刻清标记」会把它印成 + * {@code in function <文件:行>}。 + */ + static void dHook(LuaState L, int event, int line, int ftransfer, int ntransfer) { + LuaHook hook = L.hook; + if (hook == null || !L.allowHook) { + return; + } + CallInfo ci = L.ci; + int savedTop = L.top; + int savedCiTop = ci.top; + LuaDebug ar = new LuaDebug(); + ar.event = event; + ar.currentline = line; + ar.iCi = ci; + ar.state = L; + boolean tran = ntransfer != 0; + if (tran) { + ci.ftransfer = ftransfer; + ci.ntransfer = ntransfer; + } + if (ci.isLua && L.top < ci.top) { + L.top = ci.top; + } + L.checkStack(LuaState.MINSTACK); + if (ci.top < L.top + LuaState.MINSTACK) { + ci.top = L.top + LuaState.MINSTACK; + } + L.allowHook = false; + ci.hooked = true; + ci.tran = tran; + boolean ok = false; + try { + hook.hook(L, ar); + ok = true; + } finally { + if (ok) { + L.allowHook = true; + ci.top = savedCiTop; + L.top = savedTop; + ci.hooked = false; + ci.tran = false; + } + } + } + + /** + * {@code ldo.c: luaD_hookcall}:Lua 函数的调用钩子。 + *

+ * 那句「{@code savedpc} 先加一再加回来」是因为钩子的约定是 + * 「{@code savedpc} 已经越过当前这条指令」——{@code debug.getinfo(2, 'l')} + * 要报的是调用发生的那一行,而此时 {@code savedpc} 还停在 + * {@code OP_CALL} 上。 + */ + static void dHookCall(LuaState L, CallInfo ci) { + L.oldpc = 0; + if ((L.hookmask & Lua.LUA_MASKCALL) != 0) { + int event = ci.tail ? Lua.LUA_HOOKTAILCALL : Lua.LUA_HOOKCALL; + Proto p = ((LClosure) L.stack[ci.func].o).p; + ci.savedpc++; + dHook(L, event, -1, 1, p.numparams); + ci.savedpc--; + } + } + + /** + * {@code ldo.c: rethook}:返回钩子,并顺手更新 {@code oldpc}。 + *

+ * 变参函数要把 {@code ci.func} 先退回「虚拟 func」再算偏移:真实 + * {@code func} 与虚拟 {@code func} 之间隔着 {@code nextraargs + numparams + 1} + * 格,照真实 {@code func} 算出来的 {@code ftransfer} 会是个负数。 + */ + static void rethook(LuaState L, CallInfo ci, int nres) { + if ((L.hookmask & Lua.LUA_MASKRET) != 0) { + int firstres = L.top - nres; + int delta = 0; + if (ci.isLua) { + Proto p = ((LClosure) L.stack[ci.func].o).p; + if (p.isVararg) { + delta = ci.nextraargs + p.numparams + 1; + } + } + ci.func += delta; + dHook(L, Lua.LUA_HOOKRET, -1, firstres - ci.func, nres); + ci.func -= delta; + } + CallInfo prev = ci.previous; + if (prev != null && prev.isLua) { + L.oldpc = pcRel(prev.savedpc); + } + } + + /** {@code pcRel(pc, p)}:{@code p->code} 在本实现里恒为下标 0 */ + private static int pcRel(int pc) { + return pc - 1; + } + + /** {@code ldebug.c: changedline}:{@code oldpc} 到 {@code newpc} 之间换过行吗 */ + private static boolean changedLine(Proto p, int oldpc, int newpc) { + if (p.sizelineinfo == 0) { + return false; // 没有调试信息 + } + if (newpc - oldpc < Proto.MAXIWTHABS / 2) { + int delta = 0; + int pc = oldpc; + while (true) { + if (pc + 1 >= p.sizelineinfo) { + break; + } + int lineinfo = p.lineinfo[++pc]; + if (lineinfo == Proto.ABSLINEINFO) { + break; // 算不出差值,落到下面显式比 + } + delta += lineinfo; + if (pc == newpc) { + return delta != 0; + } + } + } + return p.getFuncline(oldpc) != p.getFuncline(newpc); + } + + /** + * {@code lopcodes.h: isIT(i)}:这条指令的操作数个数由 {@code L->top} 决定吗。 + *

+ * 是的话 {@code L->top} 此刻是有意义的,不能拿 {@code ci->top} 盖掉; + * 不是的话 {@code L->top} 只是「上次用到哪儿」,必须抬到 {@code ci->top} + * 才能给钩子腾出干净的栈。 + */ + private static boolean isIT(int instr) { + switch (OpCode.getOpCode(instr)) { + case OpCode.OP_CALL: + case OpCode.OP_TAILCALL: + case OpCode.OP_RETURN: + case OpCode.OP_SETLIST: + case OpCode.OP_VARARGPREP: + return OpCode.getArgB(instr) == 0; + default: + return false; + } + } + + /** + * {@code ldebug.c: luaG_traceexec}:每条指令前查一次行/计数钩子。 + * + * @param pc 即将执行的那条指令的下标 + * @return 新的 {@code trap} + */ + static boolean traceExec(LuaState L, CallInfo ci, int pc) { + int mask = L.hookmask; + Proto p = ((LClosure) L.stack[ci.func].o).p; + if ((mask & (Lua.LUA_MASKLINE | Lua.LUA_MASKCOUNT)) == 0) { + ci.trap = false; // 不需要再停 + return false; + } + pc++; // 参考点永远是「下一条」 + ci.savedpc = pc; + boolean counthook = (mask & Lua.LUA_MASKCOUNT) != 0 && (--L.hookcount == 0); + if (counthook) { + L.resetHookCount(); + } else if ((mask & Lua.LUA_MASKLINE) == 0) { + return true; // 没有行钩子、计数也没到点,这次没事 + } + if (!isIT(p.code[ci.savedpc - 1])) { + L.top = ci.top; + } + if (counthook) { + dHook(L, Lua.LUA_HOOKCOUNT, -1, 0, 0); + } + if ((mask & Lua.LUA_MASKLINE) != 0) { + int oldpc = (L.oldpc < p.sizecode) ? L.oldpc : 0; + int npci = pcRel(pc); + if (npci <= oldpc || changedLine(p, oldpc, npci)) { + dHook(L, Lua.LUA_HOOKLINE, p.getFuncline(npci), 0, 0); + } + L.oldpc = npci; + } + return true; + } + + /** + * {@code ldebug.c: luaG_tracecall}:进一个 Lua 函数时先报调用钩子。 + *

+ * 变参函数这里返回 {@code false}(把 {@code trap} 关掉),它的调用钩子推迟到 + * {@code OP_VARARGPREP} 之后——那时参数才归位,报出来的 + * {@code numparams} 才对得上。{@link Vm} 里 {@code OP_VARARGPREP} 之后的 + * {@code updatetrap} 会把 {@code trap} 从帧里读回来重新置 1。 + */ + static boolean traceCall(LuaState L, CallInfo ci) { + Proto p = ((LClosure) L.stack[ci.func].o).p; + ci.trap = true; + if (ci.savedpc == 0) { // 第一条指令(不是从 yield 恢复的) + if (p.isVararg) { + return false; + } + dHookCall(L, ci); + } + return true; + } + + /* ==================== traceback ==================== */ + + /** + * {@code LEVELS1 / LEVELS2}:回溯太长时只留头尾。 + *

+ * 上游是 10 和 11——加起来才是 21,且头尾不等长。凭印象写成 12/10 + * 会让「跳过多少层」那个数字差 1,而且只在深栈上才暴露。 + */ + private static final int LEVELS1 = 10; + private static final int LEVELS2 = 11; + + private static int lastLevel(LuaState L) { + LuaDebug ar = new LuaDebug(); + int li = 1; + int le = 1; + while (getStack(L, le, ar)) { + li = le; + le *= 2; + } + while (li < le) { + int m = (li + le) / 2; + if (getStack(L, m, ar)) { + li = m + 1; + } else { + le = m; + } + } + return le - 1; + } + + private static int traceback(LuaState L) { + int[] argHolder = new int[1]; + LuaState L1 = getThread(L, argHolder); + int arg = argHolder[0]; + LuaValue m = L.isNoneArg(arg + 1) ? LuaValue.ofNil() : L.arg1(arg + 1); + if (!m.isString() && !m.isNumber() && !m.isNil()) { + L.push(new LuaValue(m)); // 非字符串原样返回 + return 1; + } + // luaL_traceback 用 strlen(msg) 取长度: 之后的内容不进回溯 + LuaString msg = m.isNil() ? null : LuaAux.asString(m).toCString(); + int level = (int) LuaAux.optInteger(L, arg + 2, (L == L1) ? 1 : 0); + L.push(LuaValue.ofString(traceback(L, L1, msg, level))); + return 1; + } + + /** + * {@code lauxlib.c: luaL_traceback} 的字符串版本。 + *

+ * 抽出来是因为除了 {@code debug.traceback} 还有第二个调用方:{@code lua.c} 的 + * {@code msghandler}(未捕获错误的默认处理函数)也调它。两边共一份, + * 免得「{@code debug.traceback} 对的、未捕获错误的回溯少一层」这种事发生。 + *

+ * 注意 {@code L} 与 {@code L1} 的分工:回溯的文本建在 {@code L} 上, + * 但帧取自 {@code L1}——{@code debug.traceback(co)} 就是靠这个区别 + * 拿到协程自己的栈。 + */ + static LuaString traceback(LuaState L, LuaState L1, LuaString msg, int level) { + /* + * 用字节缓冲而不是 StringBuilder:msg 与 short_src 都是 Lua 字节串, + * 可以是任意字节。走 Java String 会先按 UTF-8 解码(非法字节变 + * U+FFFD)再编码回去,内容就变了 —— 实测 + * debug.traceback("\xff") 一度返回 "\xef\xbf\xbd" 打头的一串。 + */ + LuaAux.Buf b = new LuaAux.Buf(); + if (msg != null) { + b.addString(msg); + b.addChar('\n'); + } + b.addAscii("stack traceback:"); + int last = lastLevel(L1); + int limit2show = (last - level > LEVELS1 + LEVELS2) ? LEVELS1 : -1; + LuaDebug ar = new LuaDebug(); + while (getStack(L1, level++, ar)) { + if (limit2show-- == 0) { + int n = last - level - LEVELS2 + 1; + b.addAscii("\n\t...\t(skipping "); + b.addAscii(Integer.toString(n)); + b.addAscii(" levels)"); + level += n; + } else { + getInfo(L1, "Slnt", ar); + b.addAscii("\n\t"); + b.addString(ar.shortSrc); + if (ar.currentline <= 0) { + b.addAscii(": in "); + } else { + b.addAscii(":"); + b.addAscii(Integer.toString(ar.currentline)); + b.addAscii(": in "); + } + b.addString(pushFuncName(L, ar)); + if (ar.istailcall) { + b.addAscii("\n\t(...tail calls...)"); + } + } + } + return b.result(); + } + + /** + * {@code lauxlib.c: pushfuncname} 的字符串版本。 + *

+ * 全局名优先于代码里的名字:{@code error} 在代码里是 + * {@code global 'error'}({@code GETTABUP _ENV}),但回溯打印的是 + * {@code function 'error'}——第一支 {@code pushglobalfuncname} 在 + * {@code _LOADED._G} 里按值相等反查到了这个名字。所以这里必须先查全局, + * 只查 {@code ar.namewhat} 会整体差一层「修饰语」。 + */ + private static LuaString pushFuncName(LuaState L, LuaDebug ar) { + /* + * 同样返回字节串:{@code function } 里嵌的是 + * chunk 名,可以是任意字节。走 Java String 再 {@code addAscii} 会把 + * 0xFF 截成 0xFD、把 U+4E2D 截成 '-'(实测 {@code load(f,"=\xe4\xb8\xad")} + * 的回溯打印成 {@code function <-:1>})。 + */ + String g = globalFuncName(L, ar); + if (g != null) { + return LuaString.latin1("function '" + g + "'"); + } + if (ar.namewhat != null && !ar.namewhat.isEmpty()) { + return LuaString.latin1(ar.namewhat + " '" + ar.name + "'"); + } + if ("main".equals(ar.what)) { + return LuaString.latin1("main chunk"); + } + if (!"C".equals(ar.what)) { + return LuaString.latin1("function <").concat(ar.shortSrc) + .concat(LuaString.latin1(":" + ar.linedefined + ">")); + } + return LuaString.latin1("?"); + } + + /** + * {@code lauxlib.c: pushglobalfuncname}:在 {@code _LOADED} 里按值反查两层的名字。 + *

+ * 与 {@code LuaState.globalFuncName} 是同一件事,只是取函数的来源不同—— + * 那边取「当前 C 帧的函数」,这里取 {@code ar.iCi} 那一帧的,而后者可能属于 + * 另一个线程({@code debug.traceback(co)})。 + */ + private static String globalFuncName(LuaState L, LuaDebug ar) { + if (ar.iCi == null || ar.state == null) { + return null; + } + LuaValue f = ar.state.stack[ar.iCi.func]; + String s = LuaState.findFieldName(L.loadedTable(), f, 2); + if (s == null) { + return null; + } + // 上游找到的是 "_G.error",把前缀去掉 + return s.startsWith("_G.") ? s.substring(3) : s; + } + + /* ==================== 杂项 ==================== */ + + /** {@code lua_setcstacklimit}:本实现没有 C 栈深度限制,一律给上限 */ + private static final int MAXCCALLS = 200; + + private static int setCStackLimit(LuaState L) { + LuaAux.checkInteger(L, 1); + L.push(LuaValue.ofInteger(MAXCCALLS)); + return 1; + } + + /** {@code ldblib.c: db_debug} 里 {@code fgets} 的缓冲区大小 */ + private static final int DB_BUFSIZ = 250; + + /** {@code "lua_debug> "}:注意末尾那个空格,且没有换行 */ + private static final byte[] DB_PROMPT = "lua_debug> ".getBytes(StandardCharsets.UTF_8); + + /** + * {@code db_debug}:交互调试器。 + *

+ * 三处必须与 C 一字不差,否则逐字节比对立刻红: + *

    + *
  • 每次读之前先往 stderr 打 {@code "lua_debug> "} + * ({@code lua_writestringerror},无换行、无缓冲)。原来漏了这一句, + * 于是参考实现的输出里凭空多一个 {@code lua_debug> };
  • + *
  • 是循环,不是读一行就返回:C 一直读到 EOF 或某一行正好是 + * {@code "cont\n"},中间每行都当 chunk 执行。原来只跑第一行;
  • + *
  • 命令出错时把消息打到 stderr 并继续下一轮,不是抛出去。 + * C 是 {@code lua_writestringerror("%s\n", luaL_tolstring(L, -1))}。
  • + *
+ *

+ * 命令的 chunk 名是 {@code =(debug command)},且每次执行完 + * {@code lua_settop(L, 0)}——本实现的 {@link LuaState#setTop} 与 C 一样是 + * 相对当前帧的,所以直接写 0。 + *

+ * 差分用例里 stdin 是 {@code /dev/null}(见 {@code ReferenceLua#runBytes}), + * 所以第一轮就 EOF:表现为「只打一个提示符然后返回」。 + */ + private static int debugFn(LuaState L) { + byte[] buf = new byte[DB_BUFSIZ]; + for (;;) { + writeErr(L, DB_PROMPT); + int n = fgets(L.in, buf); + if (n < 0 || isCont(buf, n)) { + return 0; + } + byte[] src = java.util.Arrays.copyOf(buf, n); + LuaValue err; + try { + err = L.runProtected(src, "=(debug command)"); + } catch (LuaError e) { + // luaL_loadbuffer 那一步就失败:消息同样要打出来 + err = e.value; + } + if (err != null) { + writeErr(L, LuaAux.toLString(L, err).bytes); + writeErr(L, new byte[] { '\n' }); + } + L.setTop(0); + } + } + + /** + * C 的 {@code fgets}:最多读 {@code buf.length - 1} 个字节,遇到 {@code '\n'} + * 连同换行一起收进来;EOF 时一个字节都没读到才返回 -1。 + * + * @return 读到的字节数,或 -1 表示 EOF + */ + private static int fgets(java.io.InputStream in, byte[] buf) { + int n = 0; + try { + while (n + 1 < buf.length) { + int c = in.read(); + if (c < 0) { + break; + } + buf[n++] = (byte) c; + if (c == '\n') { + break; + } + } + } catch (java.io.IOException e) { + // C 的 fgets 出错也返回 NULL;n == 0 即等价 + } + return (n == 0) ? -1 : n; + } + + /** {@code strcmp(buffer, "cont\n") == 0} */ + private static boolean isCont(byte[] buf, int n) { + return n == 5 && buf[0] == 'c' && buf[1] == 'o' && buf[2] == 'n' + && buf[3] == 't' && buf[4] == '\n'; + } + + /** {@code lua_writestringerror}:写字节 + 立刻 flush(C 的 stderr 无缓冲) */ + private static void writeErr(LuaState L, byte[] b) { + L.err.write(b, 0, b.length); + L.err.flush(); + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/Dump.java b/src/main/java/com/github/esrrhs/fakescript/lua/Dump.java new file mode 100644 index 0000000..da4d73b --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/Dump.java @@ -0,0 +1,235 @@ +package com.github.esrrhs.fakescript.lua; + +import java.io.ByteArrayOutputStream; + +/** + * 写预编译 chunk —— 对应 {@code ldump.c},{@link Undump} 的逆操作。 + *

+ * 对外只有一处入口:{@code string.dump}。{@code luac} 用的是同一份代码, + * 但那是命令行工具,不在运行期里,这里不搬。 + * + *

{@code strip}

+ * 剥掉调试信息(行号、局部变量名、upvalue 名)后,chunk 仍能正常执行, + * 只是报错时没有行号:{@code Proto.getFuncline} 在 {@code sizelineinfo == 0} + * 时返回 {@code -1}。剥的时候源名也要一起剥——否则行号没了而源名还在, + * 反而暴露了它本该隐藏的东西。 + */ +public final class Dump { + + private Dump() { + } + + /** 写出器:{@code lua_Writer},返回非 0 表示失败 */ + public interface Writer { + int write(byte[] b, int off, int len); + } + + private static final byte[] SIGNATURE = { 0x1b, 'L', 'u', 'a' }; + private static final int LUAC_VERSION = 0x54; + private static final int LUAC_FORMAT = 0; + private static final byte[] LUAC_DATA = { 0x19, (byte) 0x93, '\r', '\n', 0x1a, '\n' }; + private static final long LUAC_INT = 0x5678L; + private static final double LUAC_NUM = 370.5; + private static final int SIZE_INSTRUCTION = 4; + private static final int SIZE_INTEGER = 8; + private static final int SIZE_NUMBER = 8; + + /** 每个字节存 7 位,64 位最多 10 组 */ + private static final int DIBS = 10; + + private static final class DumpState { + final Writer w; + final boolean strip; + int status; + + DumpState(Writer w, boolean strip) { + this.w = w; + this.strip = strip; + } + } + + private static void dumpBlock(DumpState D, byte[] b, int off, int size) { + if (D.status == 0 && size > 0) { + D.status = D.w.write(b, off, size); + } + } + + private static void dumpByte(DumpState D, int y) { + dumpBlock(D, new byte[] { (byte) y }, 0, 1); + } + + /** 变长无符号数:倒着填,最后一组的最高位置 1(与 {@link Undump} 的读法对称) */ + private static void dumpSize(DumpState D, long x) { + byte[] buff = new byte[DIBS]; + int n = 0; + do { + buff[DIBS - (++n)] = (byte) (x & 0x7f); + x >>>= 7; + } while (x != 0); + buff[DIBS - 1] |= (byte) 0x80; + dumpBlock(D, buff, DIBS - n, n); + } + + private static void dumpInt(DumpState D, int x) { + dumpSize(D, x); + } + + private static void dumpInteger(DumpState D, long x) { + byte[] b = new byte[SIZE_INTEGER]; + Undump.putInteger(b, 0, x); // 小端 + dumpBlock(D, b, 0, SIZE_INTEGER); + } + + private static void dumpNumber(DumpState D, double x) { + dumpInteger(D, Double.doubleToRawLongBits(x)); + } + + private static void dumpString(DumpState D, LuaString s) { + if (s == null) { + dumpSize(D, 0); + } else { + dumpSize(D, s.length() + 1); // 0 留给「没有」 + dumpBlock(D, s.bytes, 0, s.length()); + } + } + + private static void dumpCode(DumpState D, Proto f) { + dumpInt(D, f.sizecode); + byte[] b = new byte[f.sizecode * SIZE_INSTRUCTION]; + for (int i = 0; i < f.sizecode; i++) { + int v = f.code[i]; + for (int k = 0; k < SIZE_INSTRUCTION; k++) { // 小端 + b[i * SIZE_INSTRUCTION + k] = (byte) ((v >>> (8 * k)) & 0xFF); + } + } + dumpBlock(D, b, 0, b.length); + } + + private static void dumpConstants(DumpState D, Proto f) { + dumpInt(D, f.sizek); + for (int i = 0; i < f.sizek; i++) { + LuaValue o = f.k[i]; + int tt = o.tt; + switch (tt) { + case Lua.TNIL: + dumpByte(D, Lua.VNIL); + break; + case Lua.TBOOLEAN: + dumpByte(D, o.i != 0 ? Lua.VTRUE : Lua.VFALSE); + break; + case Lua.VNUMINT: + dumpByte(D, Lua.VNUMINT); + dumpInteger(D, o.i); + break; + case Lua.VNUMFLT: + dumpByte(D, Lua.VNUMFLT); + dumpNumber(D, o.n); + break; + case Lua.VSHRSTR: + case Lua.VLNGSTR: + dumpByte(D, tt); + dumpString(D, (LuaString) o.o); + break; + default: + // 常量表里只可能有这几种——Parser 不会别的东西塞进来 + throw new IllegalStateException("bad constant type " + tt); + } + } + } + + private static void dumpProtos(DumpState D, Proto f) { + dumpInt(D, f.sizep); + for (int i = 0; i < f.sizep; i++) { + dumpFunction(D, f.p[i], f.source); + } + } + + private static void dumpUpvalues(DumpState D, Proto f) { + dumpInt(D, f.sizeupvalues); + for (int i = 0; i < f.sizeupvalues; i++) { + dumpByte(D, f.upvalues[i].instack ? 1 : 0); + dumpByte(D, f.upvalues[i].idx); + dumpByte(D, f.upvalues[i].kind); + } + } + + private static void dumpDebug(DumpState D, Proto f) { + int n = D.strip ? 0 : f.sizelineinfo; + dumpInt(D, n); + dumpBlock(D, f.lineinfo, 0, n); + n = D.strip ? 0 : f.sizeabslineinfo; + dumpInt(D, n); + for (int i = 0; i < n; i++) { + dumpInt(D, f.abslineinfo[i].pc); + dumpInt(D, f.abslineinfo[i].line); + } + n = D.strip ? 0 : f.sizelocvars; + dumpInt(D, n); + for (int i = 0; i < n; i++) { + dumpString(D, f.locvars[i].varname); + dumpInt(D, f.locvars[i].startpc); + dumpInt(D, f.locvars[i].endpc); + } + n = D.strip ? 0 : f.sizeupvalues; + dumpInt(D, n); + for (int i = 0; i < n; i++) { + dumpString(D, f.upvalues[i].name); + } + } + + private static void dumpFunction(DumpState D, Proto f, LuaString psource) { + /* + * 与父函数同源(或本来就剥调试信息)就不重复写源名。 + * 比的是引用而不是内容——C 版是 {@code f->source == psource}, + * 两个指针比。短字符串驻留之后这等价于比内容,长字符串则不驻留, + * 于是 C 会为每个内嵌函数各写一遍源名;照内容比会写得比 C 少。 + */ + if (D.strip || f.source == psource) { + dumpString(D, null); + } else { + dumpString(D, f.source); + } + dumpInt(D, f.linedefined); + dumpInt(D, f.lastlinedefined); + dumpByte(D, f.numparams); + dumpByte(D, f.isVararg ? 1 : 0); + dumpByte(D, f.maxstacksize); + dumpCode(D, f); + dumpConstants(D, f); + dumpUpvalues(D, f); + dumpProtos(D, f); + dumpDebug(D, f); + } + + private static void dumpHeader(DumpState D) { + dumpBlock(D, SIGNATURE, 0, SIGNATURE.length); + dumpByte(D, LUAC_VERSION); + dumpByte(D, LUAC_FORMAT); + dumpBlock(D, LUAC_DATA, 0, LUAC_DATA.length); + dumpByte(D, SIZE_INSTRUCTION); + dumpByte(D, SIZE_INTEGER); + dumpByte(D, SIZE_NUMBER); + dumpInteger(D, LUAC_INT); + dumpNumber(D, LUAC_NUM); + } + + /** {@code luaU_dump} */ + public static int dump(Proto f, Writer w, boolean strip) { + DumpState D = new DumpState(w, strip); + dumpHeader(D); + dumpByte(D, f.sizeupvalues); + dumpFunction(D, f, null); + return D.status; + } + + /** 把整颗函数树 dump 成字节数组;{@code string.dump} 用这个 */ + public static byte[] dumpBytes(Proto f, boolean strip) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + dump(f, (b, off, len) -> { + out.write(b, off, len); + return 0; + }, strip); + return out.toByteArray(); + } + +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/DynData.java b/src/main/java/com/github/esrrhs/fakescript/lua/DynData.java new file mode 100644 index 0000000..cdb4351 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/DynData.java @@ -0,0 +1,71 @@ +package com.github.esrrhs.fakescript.lua; + +import java.util.ArrayList; +import java.util.List; + +/** + * 解析过程中的动态结构 —— 对应 {@code lparser.h} 的 {@code Dyndata}。 + *

+ * 三个列表都由最外层的解析状态持有,层层嵌套的函数共享同一份: + * 局部变量栈要跨函数边界(内层看到外层的局部变量才能识别出 upvalue), + * 而 goto/标签列表本来就是整个 chunk 范围内的。 + */ +public final class DynData { + + /** 变量种类 */ + public static final int VDKREG = 0; + public static final int RDKCONST = 1; + public static final int RDKTOCLOSE = 2; + public static final int RDKCTC = 3; + + /** 活跃局部变量描述({@code Vardesc}) */ + public static final class Vardesc { + /** 编译期常量的值(仅 kind == RDKCTC 时有意义) */ + public LuaValue k; + /** {@code kind}:VDKREG / RDKCONST / RDKTOCLOSE / RDKCTC */ + public int kind = VDKREG; + /** 变量所在的寄存器 */ + public int ridx; + /** 在 {@code Proto.locvars} 里的下标 */ + public int pidx; + public LuaString name; + } + + /** 标签或待处理的 goto({@code Labeldesc}) */ + public static final class Labeldesc { + public LuaString name; + /** 代码位置 */ + public int pc; + /** 出现的行 */ + public int line; + /** 该位置上的活跃变量数 */ + public int nactvar; + /** 这个 goto 是否跨越了 upvalue */ + public boolean close; + } + + /** + * 局部变量栈。只增长不收缩:{@link #actvarN} 是逻辑长度。 + *

+ * 这一点必须照抄上游。C 里 {@code removevars} 先把 {@code actvar.n} 减掉, + * 然后还要顺着下标读一遍刚「删掉」的那些槽位去补调试信息的 + * {@code endpc}——靠的就是数组没被真收缩。用 {@code List.remove} 会在那里 + * 直接越界,或者更糟:读到的是别的变量。 + */ + public final List actvar = new ArrayList<>(); + + /** {@code actvar} 的逻辑长度 */ + public int actvarN; + + public final List gt = new ArrayList<>(); + public final List label = new ArrayList<>(); + + /** 在逻辑位置 {@code n} 放一个变量描述(必要时复用旧槽位) */ + public void setActvar(int n, Vardesc v) { + if (n < actvar.size()) { + actvar.set(n, v); + } else { + actvar.add(v); + } + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/ExpDesc.java b/src/main/java/com/github/esrrhs/fakescript/lua/ExpDesc.java new file mode 100644 index 0000000..4c5aabe --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/ExpDesc.java @@ -0,0 +1,131 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * 表达式描述符 —— 对应 {@code lparser.h} 的 {@code expdesc}。 + *

+ * 这是整个 codegen 的核心数据结构:解析一个表达式时,不急着生成代码, + * 而是先记下「这个表达式的值现在在哪儿」。等到不得不落地时(存进变量、当实参、 + * 参与运算)再决定最省指令的做法——比如 {@code t.x} 可以一直保持 + * VINDEXSTR 不落地,直到赋值时才发一条 {@code SETFIELD}。 + *

+ * C 版用一个 union 存不同 kind 的载荷;Java 没有 union,这里把所有字段摊平。 + * 唯一要小心的是 C 里那些「overlapping」的写法(比如 VLOCAL 的 + * {@code var.ridx} 与 VINDEXED 的 {@code ind.t} 共用存储),摊平之后反而 + * 不会出现「读了上一个 kind 留下的位」这类错误。 + */ +public final class ExpDesc { + + /* expkind —— ORDER 与上游严格一致 */ + /** 空表达式(最后一个表达式为空列表) */ + public static final int VVOID = 0; + public static final int VNIL = 1; + public static final int VTRUE = 2; + public static final int VFALSE = 3; + /** 常量在 {@code k} 里;info = 下标 */ + public static final int VK = 4; + /** 浮点常量 */ + public static final int VKFLT = 5; + /** 整数常量 */ + public static final int VKINT = 6; + /** 字符串常量(由词法层驻留) */ + public static final int VKSTR = 7; + /** 值已在某个固定寄存器里;info = 寄存器号 */ + public static final int VNONRELOC = 8; + /** 局部变量;varRidx = 寄存器号,varVidx = actvar 下标 */ + public static final int VLOCAL = 9; + /** upvalue;info = upvalues 下标 */ + public static final int VUPVAL = 10; + /** 编译期 {@code } 变量;info = actvar 里的绝对下标 */ + public static final int VCONST = 11; + /** 索引变量:indT = 表寄存器,indIdx = 键的 R 下标 */ + public static final int VINDEXED = 12; + /** 索引 upvalue:indT = 表 upvalue,indIdx = 键的 K 下标 */ + public static final int VINDEXUP = 13; + /** 常量整数索引:indT = 表寄存器,indIdx = 键值 */ + public static final int VINDEXI = 14; + /** 字面串索引:indT = 表寄存器,indIdx = 键的 K 下标 */ + public static final int VINDEXSTR = 15; + /** 表达式是一个测试/比较;info = 对应跳转指令的 pc */ + public static final int VJMP = 16; + /** 结果可以放进任意寄存器;info = 指令 pc */ + public static final int VRELOC = 17; + /** 函数调用;info = 指令 pc */ + public static final int VCALL = 18; + /** vararg 表达式;info = 指令 pc */ + public static final int VVARARG = 19; + + /** 当前 kind */ + public int k = VVOID; + + /** VKINT */ + public long ival; + /** VKFLT */ + public double nval; + /** VKSTR */ + public LuaString strval; + /** 通用下标(VK / VUPVAL / VNONRELOC / VJMP / VRELOC / VCALL / VVARARG) */ + public int info; + + /** VINDEX*:键 */ + public int indIdx; + /** VINDEX*:表(寄存器号或 upvalue 下标) */ + public int indT; + + /** VLOCAL:寄存器号 */ + public int varRidx; + /** VLOCAL:actvar 里的下标 */ + public int varVidx; + + /** 「为真则跳走」的跳转链表 */ + public int t = Codegen.NO_JUMP; + /** 「为假则跳走」的跳转链表 */ + public int f = Codegen.NO_JUMP; + + public ExpDesc() { + } + + /** 复制(等价于 C 的 {@code *e1 = *e2}) */ + public ExpDesc(ExpDesc src) { + set(src); + } + + public ExpDesc set(ExpDesc src) { + this.k = src.k; + this.ival = src.ival; + this.nval = src.nval; + this.strval = src.strval; + this.info = src.info; + this.indIdx = src.indIdx; + this.indT = src.indT; + this.varRidx = src.varRidx; + this.varVidx = src.varVidx; + this.t = src.t; + this.f = src.f; + return this; + } + + /** {@code vkisvar}:是不是变量(可赋值) */ + public boolean isVar() { + return VLOCAL <= k && k <= VINDEXSTR; + } + + /** {@code vkisindexed}:是不是索引变量 */ + public boolean isIndexed() { + return VINDEXED <= k && k <= VINDEXSTR; + } + + /** {@code hasjumps}:带跳转说明这是个短路表达式(and/or/比较) */ + public boolean hasJumps() { + return t != f; + } + + /* ---------- 便捷构造 ---------- */ + + /** 目前唯一被用到的一个:{@code Codegen} 给一元运算配一个假的第二个操作数 */ + public static ExpDesc ofInt(long v) { + ExpDesc e = new ExpDesc(); + e.k = VKINT; + e.ival = v; + return e; + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/FuncState.java b/src/main/java/com/github/esrrhs/fakescript/lua/FuncState.java new file mode 100644 index 0000000..b0a108a --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/FuncState.java @@ -0,0 +1,91 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * 单个函数的编译状态 —— 对应 {@code lparser.h} 的 {@code FuncState}。 + *

+ * 每进入一个函数体就压一个新的 FuncState,{@link #prev} 串起外层,upvalue 的 + * 解析就是顺着这条链往上找局部变量。 + *

+ * 与上游的一处结构性差异:没有 {@code nk} / {@code np} / {@code nabslineinfo}。 + * C 里这三个是「已用长度」,与数组的「已分配长度」分开;这里数组由 {@link Proto} + * 自己增长,两者恒等——留着两套计数只会给「改了一个忘了另一个」留位置。 + * 于是 {@code nk} 就是 {@code f.sizek},{@code np} 就是 {@code f.sizep}。 + *

+ * 常量缓存不在这里:上游的 {@code ls->h} 属于词法状态(整份源码共享, + * 跨函数),见 {@link Lexer#lookupConstant}。 + */ +public final class FuncState { + + /** 当前函数的原型 */ + public final Proto f; + /** 外层函数 */ + public final FuncState prev; + /** 词法状态(报错要用它带上位置信息) */ + public final Lexer ls; + /** 跨函数共享的动态结构 */ + public final DynData dyd; + + /** 下一条指令的写入位置(等价于 sizecode) */ + public int pc; + /** 最后一个跳转目标的位置(防止跨基本块做优化) */ + public int lasttarget; + /** 上一次存进 lineinfo 的行号 */ + public int previousline; + /** 本函数第一个局部变量在 dyd.actvar 里的下标 */ + public int firstlocal; + /** 本函数第一个标签在 dyd.label 里的下标 */ + public int firstlabel; + /** f.locvars 里已用的条目数 */ + public int ndebugvars; + /** 活跃局部变量数(= 寄存器栈上被变量占住的部分) */ + public int nactvar; + /** upvalue 数 */ + public int nups; + /** 第一个空闲寄存器 */ + public int freereg; + /** 距上次写绝对行号以来发出的指令数 */ + public int iwthabs; + /** 返回时需要关闭 upvalue */ + public boolean needclose; + /** + * 当前块({@code fs->bl})。 + *

+ * C 里 {@code BlockCnt} 是栈上对象,靠这个指针串成链;这里只能堆分配, + * 但链的用法(进出块时压弹)完全一样。 + */ + public Parser.BlockCnt bl; + + public FuncState(Proto f, FuncState prev, Lexer ls, DynData dyd) { + this.f = f; + this.prev = prev; + this.ls = ls; + this.dyd = dyd; + } + + /** + * {@code luaY_nvarstack}:寄存器栈上被局部变量占住的寄存器数。 + *

+ * 不能简单地返回 {@link #nactvar}:编译期常量({@code })不占寄存器, + * 所以要看 {@code nactvar} 之下「最高的那个真变量」用了哪个寄存器。 + * {@code local a = 1; local b = 2} 里 {@code b} 在 r0 而不是 r1。 + */ + public int nvarstack() { + return reglevel(this, nactvar); + } + + /** + * {@code lparser.c: reglevel}:把「编译器视角的变量序号」换算成寄存器号。 + *

+ * 从上往下找第一个不在常量区的变量,返回它的寄存器 +1。 + * 跳过 RDKCTC 是必须的:常量的 {@code ridx} 从来没被赋过值。 + */ + public static int reglevel(FuncState fs, int nvar) { + while (nvar-- > 0) { + DynData.Vardesc vd = fs.dyd.actvar.get(fs.firstlocal + nvar); + if (vd.kind != DynData.RDKCTC) { + return vd.ridx + 1; + } + } + return 0; + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/GcSentinel.java b/src/main/java/com/github/esrrhs/fakescript/lua/GcSentinel.java new file mode 100644 index 0000000..ee68c48 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/GcSentinel.java @@ -0,0 +1,59 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * {@code __gc} 的落地手段 —— 一个只被目标表引用的哨兵。 + *

+ * 为什么要有这个中间对象。要跑 {@code __gc(t)},就得在跑的那一刻还握着 + * {@code t}。Java 里所有「对象已死」的通知机制({@code WeakReference} 被清、 + * {@code Cleaner} / {@code PhantomReference} 入队)都发生在对象已经取不到 + * 之后——引用一清就再也拿不回来。而 C 版没有这个问题:{@code lgc.c: GCTM} 是在 + * mark 阶段之后、对象还在内存里时直接拿着指针调元方法。 + *

+ * 唯一能在对象尚可访问时被调用的是 {@link Object#finalize()}——它被调的那一刻 + * {@code this} 还能用,而且允许复活(把 {@code this} 重新交给别人保存)。 + * 于是这里绕一圈: + *

    + *
  1. 表持有一个哨兵(且只有表持有它),哨兵反过来强引用表;
  2. + *
  3. 脚本一丢开表,这一对一起变成不可达;
  4. + *
  5. JVM 调哨兵的 {@code finalize},哨兵趁机把表塞进待终结队列—— + * 表就此复活;
  6. + *
  7. 下一次 {@code collectgarbage()} 在 Lua 线程上排空队列, + * 真正调用 {@code __gc}。
  8. + *
+ * 第 3、4 步必须分开。{@code finalize} 跑在 JVM 的终结线程上,在那里调 + * Lua 函数等于在另一个线程上动 {@code LuaState},数据竞争无可避免;而且终结线程 + * 上抛异常会被 JVM 直接吞掉(连日志都没有)。所以那里只做「入队」这一件事。 + *

+ * 放在哨兵上而不是直接给 {@link LuaTable} 写 {@code finalize},是为了不给所有 + * 表付这份代价:带终结器的对象在 JVM 里至少要两轮 GC 才收得掉,还要进终结 + * 队列。只有真挂了 {@code __gc} 的表才需要它。 + *

+ * {@code finalize} 已被标记为 deprecated,这是唯一的出路,别无选择。若将来 + * {@code --finalization=disabled} 成为默认,这里会静默失效——那也是它该有的反应: + * 那时 {@code __gc} 确实没法跑了。 + */ +final class GcSentinel { + + private final Object owner; + + /** 队列挂在主线程上:哨兵是在 {@code newThread} 之前还是之后创建的无从保证 */ + private final LuaState main; + + GcSentinel(Object owner, LuaState main) { + this.owner = owner; + this.main = main; + } + + /** + * 表不可达 → 哨兵也不可达 → JVM 调这里。 + *

+ * 这里唯一的活就是把 {@code owner} 拽回可达集合。它只被 {@link #owner} 强 + * 引用着,而 {@code owner} 这条边不算「脚本的引用」,所以一旦脚本丢开表, + * 两者一起不可达——这正是我们要的判据。 + */ + @Override + @SuppressWarnings("deprecation") + protected void finalize() { + main.enqueueFinalizer(owner); + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/IoLib.java b/src/main/java/com/github/esrrhs/fakescript/lua/IoLib.java new file mode 100644 index 0000000..a3ea5fa --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/IoLib.java @@ -0,0 +1,1398 @@ +package com.github.esrrhs.fakescript.lua; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.nio.charset.StandardCharsets; + +/** + * io 库 —— 对应 {@code liolib.c}。 + *

+ * 文件句柄是带命名元表的 userdata(元表名 {@code FILEHANDLE*}),这是 + * 本库对内核提出的第一个硬要求:{@code luaL_testudata} 判的是「这个 userdata + * 的元表是不是注册表里那一张」,所以 userdata 的元表必须挂在对象上而不是类型上。 + * 之前的实现恰好只有类型元表,为此在阶段 7 补了 {@link LuaUserdata}。 + *

+ * 几处与 C 不同但刻意如此的地方(都记进了 README): + *

    + *
  1. 写不缓冲。C 的 {@code stdout} 定向到管道时是块缓冲, + * {@code io.write} 的输出要等 4KB 或进程退出才落地;这里每次写完立刻 flush。 + * 同一条流上的顺序不受影响,只有与子进程输出、{@code stderr} + * 交叉时才会看出差别。
  2. + *
  3. {@code f_tostring} 里的 {@code %p} 是地址,不可能与参考实现相同, + * 只能给个同形状的十六进制值(差分比对会把它抹成 {@code 0xADDR})。
  4. + *
  5. {@code setvbuf} 只认参数、总是成功,不真的改缓冲策略。
  6. + *
  7. {@code popen} 的子进程输出走管道而不是 {@code FILE*} + * ({@code pclose} 起来失败时拿不到 shell 的真实 errno)。被接管的 + * 那一条(读模式是子进程的 {@code stdout}、写模式是它的 {@code stdin}) + * 语义与 C 一致;没被接管的那两条是转发到 {@code L.out} / {@code L.err} + * 的,不是 {@code Redirect.INHERIT} —— 后者走 JVM 进程的 fd 1 / fd 2, + * 会绕过这两个流。代价是「转发线程什么时候写完」有个很小的时间窗, + * 靠 {@code pclose} 里 {@code join} 收口;那一刀没有用例盯得住 + * (窗口太小,破坏它矩阵照样全绿),它防的是偶发红,不是语义。
  8. + *
+ */ +public final class IoLib { + + private IoLib() { + } + + /** {@code LUA_FILEHANDLE} */ + static final String FILE_HANDLE = "FILE*"; + + /** {@code IO_INPUT} / {@code IO_OUTPUT}:注册表里那两个「默认文件」的键 */ + private static final String IO_INPUT = "_IO_input"; + private static final String IO_OUTPUT = "_IO_output"; + + /** {@code MAXARGLINE}:{@code f:lines} 最多几个读格式 */ + private static final int MAXARGLINE = 250; + + /** {@code L_MAXLENNUM}:读数字时最多攒几个字符 */ + private static final int L_MAXLENNUM = 200; + + /** {@code LUAL_BUFFERSIZE},只用于 {@code setvbuf} 的默认参数 */ + private static final int LUAL_BUFFERSIZE = 8192; + + /** {@code read_chars} 分块读用的块大小 */ + private static final int CHUNK = 8192; + + /** + * {@code read_chars} 放弃分配的上限。 + *

+ * 真值是平台相关的:macOS 上参考实现二分出来在 {@code 2^46} 附近断 + * ({@code 70368740245501} 还能成、{@code 70368740507645} 就不行), + * Linux 有过量提交、边界又不一样。所以这个数不进差分用例, + * 它只管「明显大到谁都分配不出来」的那一档;唯一确定性的输入是 + * 负数(C 里转 {@code size_t} 之后必然是天文数字)。 + */ + private static final long MAX_READ_ALLOC = 1L << 46; + + /* ==================== 注册 ==================== */ + + public static void open(LuaState L) { + LuaTable io = new LuaTable(); + LuaAux.setFn(io, "close", IoLib::ioClose); + LuaAux.setFn(io, "flush", IoLib::ioFlush); + LuaAux.setFn(io, "input", IoLib::ioInput); + LuaAux.setFn(io, "lines", IoLib::ioLines); + LuaAux.setFn(io, "open", IoLib::ioOpen); + LuaAux.setFn(io, "output", IoLib::ioOutput); + LuaAux.setFn(io, "popen", IoLib::ioPopen); + LuaAux.setFn(io, "read", IoLib::ioRead); + LuaAux.setFn(io, "tmpfile", IoLib::ioTmpfile); + LuaAux.setFn(io, "type", IoLib::ioType); + LuaAux.setFn(io, "write", IoLib::ioWrite); + LuaAux.registerLib(L, "io", io); + + createMeta(L); + + // 三个标准流。注意它们的 closef 是 io_noclose:关不掉,关了要给 + // (nil, "cannot close standard file") + createStdFile(L, io, stdFile(L, true, false, false), IO_INPUT, "stdin"); + createStdFile(L, io, stdFile(L, false, true, false), IO_OUTPUT, "stdout"); + createStdFile(L, io, stdFile(L, false, true, true), null, "stderr"); + } + + /** + * {@code createmeta}:建 {@code FILEHANDLE*} 元表,并把方法表挂到 + * {@code __index} 上。 + */ + private static void createMeta(LuaState L) { + LuaTable mt = LuaAux.newMetatable(L, FILE_HANDLE); + LuaAux.setFn(mt, "__gc", IoLib::fGc); + LuaAux.setFn(mt, "__close", IoLib::fGc); + LuaAux.setFn(mt, "__tostring", IoLib::fTostring); + LuaTable meth = new LuaTable(); + LuaAux.setFn(meth, "read", IoLib::fRead); + LuaAux.setFn(meth, "write", IoLib::fWrite); + LuaAux.setFn(meth, "lines", IoLib::fLines); + LuaAux.setFn(meth, "flush", IoLib::fFlush); + LuaAux.setFn(meth, "seek", IoLib::fSeek); + LuaAux.setFn(meth, "close", IoLib::fClose); + LuaAux.setFn(meth, "setvbuf", IoLib::fSetvbuf); + mt.set("__index", LuaValue.ofTable(meth)); + } + + private static void createStdFile(LuaState L, LuaTable io, LuaFile f, String key, + String fname) { + LuaValue v = newFileValue(L, f, IoLib::ioNoclose); + if (key != null) { + L.registry.set(key, v); + } + io.set(fname, v); + /* + * {@code liolib.c: createstdfile} 最后那句 + * {@code lua_setfield(L, -2, fname)} 是弹栈的。 + *

+ * 少了这一步,开库之后主线程栈上就永久压着三个 FILE*(stdin/stdout/ + * stderr)——栈顶凭空高 3 格。这不只是「多占三格」:{@code top} 之上的 + * 槽对脚本是看不见的,但递归深度是按 {@code top} 算的,于是同样的 + * {@code LUAI_MAXSTACK} 少递归三层。上游 {@code errors.lua} 打印的 + * 「expected stack overflow after N calls」正是对着这个数断言的。 + */ + L.pop(1); + } + + /* ==================== 文件句柄 ==================== */ + + /** + * {@code LStream}:{@code FILE*} 的替身。 + *

+ * C 里一个 {@code FILE*} 同时能读能写还能定位,Java 没有这种东西,所以按 + * 「真实文件走 {@link RandomAccessFile}、标准流与管道走 {@code InputStream} / + * {@code OutputStream}」分开存,读写时各自分派。 + *

+ * {@code closef == null} 就是 C 的 {@code isclosed}。 + */ + static final class LuaFile { + + RandomAccessFile raf; + InputStream in; + OutputStream out; + java.lang.Process proc; + + /** + * {@code io.popen} 的子进程里那些没被接管的流的转发线程。 + * 关句柄时要先 {@code join} 它们,否则子进程的最后几行输出可能还没 + * 落到 {@code L.out} / {@code L.err} 上,比对就时好时坏。 + */ + Thread[] pumps; + + /** + * 标准流要现用现取 {@code LuaState#out} / {@code #in} / {@code #err}。 + * 测试是在开库之后才把 {@code L.out} 换成内存缓冲区的,若是开库时就把 + * 流抓在手里,{@code io.write} 的输出就永远落不到那条缓冲上。 + */ + LuaState L; + boolean stdIn; + boolean stdOut; + boolean stdErr; + + boolean readable; + boolean writable; + boolean append; + + /** + * 打开的是目录。 + *

+ * POSIX 的 {@code fopen(dir, "r")} 是成功的({@code open(dir, O_RDONLY)} + * 允许),失败发生在第一次 {@code fgetc} / {@code fread} 上,errno 是 + * {@code EISDIR}(21)。实测(参考二进制): + *

+         * io.open(dir, "r")   -> file 句柄;f:read("l") -> nil, "Is a directory", 21
+         * io.open(dir, "w")   -> nil, "<path>: Is a directory", 21
+         * 
+ * Java 的 {@code RandomAccessFile} 在目录上是开都开不了的,所以这里 + * 单独记一个标记:打开时放行只读,读写一律报 21。 + */ + boolean dir; + + /** 目录被当零长文件看待时的虚拟位置 —— 目录 fd 上的 {@code lseek} 是允许的 */ + long dirPos; + + /** {@code ungetc} 的回退位;{@code -1} 表示空 */ + int pushback = -1; + + /** 相当于 {@code ferror(f)} */ + boolean err; + + /** 出错时的 errno */ + int errno; + + /** {@code closef}:{@code null} 表示已关闭 */ + LuaFunction closef; + + /** + * 写缓冲 —— 真实文件必须有,标准流不能有。 + *

+ * C 的 stdio 对文件是块缓冲的:{@code io.output(p):write("x")} 之后 + * 立刻 {@code io.input(p):read()} 读到的是空,因为那几个字节 + * 还在 {@code FILE*} 的缓冲里没落盘。不照做的话这类脚本就会在两边 + * 给出不同结果。标准流则相反——它们是行/无缓冲语义,写完就要看得见, + * 而且 {@code L.out} 本来就是个 {@code PrintStream}。 + */ + private final byte[] wbuf = new byte[WBUF_CAP]; + private int wlen; + + private static final int WBUF_CAP = 8192; + + /** 把写缓冲落盘;{@code raf == null} 时是空操作 */ + private boolean flushW() { + if (raf == null || wlen == 0) { + return true; + } + try { + raf.write(wbuf, 0, wlen); + wlen = 0; + return true; + } catch (IOException e) { + err = true; + errno = LuaAux.errnoOf(e); + return false; + } + } + + InputStream inStream() { + return stdIn ? L.in : in; + } + + OutputStream outStream() { + if (stdOut) { + return L.out; + } + if (stdErr) { + return L.err; + } + return out; + } + + /** {@code l_getc}:-1 表示 EOF */ + int getc() { + if (pushback >= 0) { + int c = pushback; + pushback = -1; + return c; + } + if (!readable) { + err = true; + errno = 9; // EBADF + return -1; + } + if (dir) { + err = true; + errno = 21; // EISDIR:目录读不出来,但打开是成功的 + return -1; + } + flushW(); // C 要求输出与输入之间必须有 fflush 或定位操作 + try { + return raf != null ? raf.read() : inStream().read(); + } catch (IOException e) { + err = true; + errno = LuaAux.errnoOf(e); + return -1; + } + } + + /** {@code ungetc}:{@code c == -1} 时是空操作(C 里 ungetc(EOF) 也是) */ + void ungetc(int c) { + pushback = c; + } + + /** {@code fread}:返回读到的字节数,0 表示 EOF 或出错 */ + int read(byte[] b, int off, int len) { + int done = 0; + if (pushback >= 0 && len > 0) { + b[off] = (byte) pushback; + pushback = -1; + done = 1; + } + if (done >= len) { + return done; + } + if (!readable) { + err = true; + errno = 9; // EBADF + return done; + } + if (dir) { + err = true; + errno = 21; // EISDIR + return done; + } + flushW(); + try { + int r = (raf != null) ? raf.read(b, off + done, len - done) + : inStream().read(b, off + done, len - done); + return (r < 0) ? done : done + r; + } catch (IOException e) { + err = true; + errno = LuaAux.errnoOf(e); + return done; + } + } + + /** {@code fwrite} */ + void write(byte[] b, int off, int len) { + if (len <= 0) { + return; + } + /* + * 与 {@code fwrite} 一致:写 0 个字节一定成功,而且不碰文件描述符。 + * {@code liolib.c: g_write} 对字符串走的是 + * {@code fwrite(s, sizeof(char), l, f) == l},{@code l == 0} 时 + * {@code fwrite} 返回 0、等于 {@code l}、算成功。所以往只读文件 + * 写空串在参考实现里是成功的 —— {@code f:write("")} 照样把文件本身 + * 返回来,而不是报 {@code Bad file descriptor}。 + *

+ * 早先这里先判 {@code !writable} 再看长度,于是空串也跟着报 EBADF。 + * 判据是实测的:{@code io.open(f):write("")} 参考给 1 个返回值(文件 + * 自己),{@code f:write("a")} 才给 3 个(nil, msg, errno)。 + *

+ * 注意 {@link #read} 那边本来就是先 {@code done >= len} 再判 + * {@code !readable},所以读 0 字节从来没踩到这个 —— 写这边漏了同一刀。 + */ + if (!writable) { + err = true; + errno = 9; // EBADF + return; + } + try { + if (raf != null) { + // "a" 是追加:不管当前位置在哪,写之前都回到末尾 + if (append) { + flushW(); + raf.seek(raf.length()); + } + if (wlen + len > wbuf.length) { + if (!flushW()) { + return; + } + } + if (len >= wbuf.length) { + raf.write(b, off, len); // 大块直接写,不绕缓冲 + } else { + System.arraycopy(b, off, wbuf, wlen, len); + wlen += len; + } + } else { + outStream().write(b, off, len); + outStream().flush(); // 标准流不缓冲 + } + } catch (IOException e) { + err = true; + errno = LuaAux.errnoOf(e); + } + } + + /** {@code l_fseek}:成功返回新位置,-1 表示失败(管道会失败) */ + long seek(long offset, int whence) { + if (dir) { + /* + * 目录 fd 上的 lseek 是允许的,位置照算;长度按 0 看。 + * 真实目录的 SEEK_END 给出的是目录自身的大小(本机 64), + * 那是文件系统定的、不比 —— 见用例 javadoc。 + */ + long base = (whence == 1) ? dirPos : 0; // 仅 SEEK_CUR 从当前位置起算 + long pos = base + offset; + if (pos < 0) { + err = true; + errno = 22; // EINVAL + return -1; + } + dirPos = pos; + return pos; + } + if (raf == null) { + err = true; + errno = 29; // ESPIPE + return -1; + } + flushW(); + try { + long base; + switch (whence) { + case 0: // SEEK_SET + base = 0; + break; + case 1: // SEEK_CUR + base = raf.getFilePointer(); + break; + default: // SEEK_END + base = raf.length(); + break; + } + /* + * 负的目标位置是 {@code EINVAL}(22),不是随便一个 IO 错。 + * C 的 {@code fseek} 在 {@code SEEK_SET} / {@code CUR} / {@code END} + * 三种起点上都是这个判据——实测参考实现三种都给 + * {@code nil, "Invalid argument", 22},而 Java 的 + * {@code RandomAccessFile.seek} 抛的是 "Negative seek offset", + * 落到 {@code errnoOf} 的兜底分支上就成了 {@code EIO}(5)。 + * 往 EOF 之后定位是允许的(返回新位置,不报错),别一起拦了。 + */ + long pos = base + offset; + if (pos < 0) { + err = true; + errno = 22; // EINVAL + return -1; + } + /* + * 成功的 {@code fseek} 会丢掉 ungetc 退回来的那个字节。 + * 实测的分歧:{@code f:read("n")} 在 "three" 上失败时把 't' 退了回去, + * 随后 {@code f:seek("set",0)} 再 {@code f:read(3)} —— 参考给 + * {@code "1 2"},我们给 {@code "t1 "}。 + */ + pushback = -1; + raf.seek(pos); + return raf.getFilePointer(); + } catch (IOException e) { + err = true; + errno = LuaAux.errnoOf(e); + return -1; + } + } + + /** {@code l_ftell} */ + long tell() { + if (dir) { + return dirPos; // 目录 fd 上的 ftell 是允许的 + } + if (raf == null) { + err = true; + errno = 29; // ESPIPE + return -1; + } + try { + // 缓冲里那几个字节也算在逻辑位置上:C 的 ftell 给的也是这个 + return raf.getFilePointer() + wlen; + } catch (IOException e) { + err = true; + errno = LuaAux.errnoOf(e); + return -1; + } + } + + /** {@code fflush} */ + void flush() { + try { + if (raf != null) { + flushW(); + raf.getFD().sync(); + } else if (writable) { + outStream().flush(); + } + } catch (IOException e) { + err = true; + errno = LuaAux.errnoOf(e); + } + } + + /** 真正关掉底层资源 */ + void closeRaw() { + try { + if (raf != null) { + flushW(); + raf.close(); + } + if (in != null) { + in.close(); + } + if (out != null) { + out.close(); + } + } catch (IOException e) { + err = true; + errno = LuaAux.errnoOf(e); + } + } + } + + private static LuaFile stdFile(LuaState L, boolean r, boolean w, boolean e) { + LuaFile f = new LuaFile(); + f.L = L; + f.stdIn = r; + f.stdOut = w && !e; + f.stdErr = e; + f.readable = r; + f.writable = w; + return f; + } + + /** {@code newprefile} 的建值部分:userdata + 命名元表,closef 留空 */ + private static LuaValue newPreFileValue(LuaState L, LuaFile f) { + L.push(LuaValue.ofUserdata(f)); + LuaAux.setMetatableByName(L, FILE_HANDLE); + return new LuaValue(L.stack[L.top - 1]); + } + + /** {@code newprefile} + 指定 {@code closef} */ + private static LuaValue newFileValue(LuaState L, LuaFile f, LuaFunction closef) { + LuaValue v = newPreFileValue(L, f); + f.closef = closef; + return v; + } + + /** {@code newfile}:普通文件,closef 是 {@code io_fclose} */ + private static LuaValue newFile(LuaState L, LuaFile f) { + return newFileValue(L, f, IoLib::ioFclose); + } + + /* ==================== 句柄查询 ==================== */ + + /** {@code tolstream}:必须是文件句柄,否则报错 */ + private static LuaFile toLStream(LuaState L) { + return (LuaFile) LuaAux.checkUdata(L, 1, FILE_HANDLE); + } + + /** {@code tofile}:在上面再加一条「不能是已关闭的」 */ + private static LuaFile toFile(LuaState L) { + LuaFile p = toLStream(L); + if (p.closef == null) { + throw L.libError("attempt to use a closed file"); + } + return p; + } + + /* ==================== io 库函数 ==================== */ + + /** {@code io.type} */ + private static int ioType(LuaState L) { + LuaAux.checkAny(L, 1); + LuaFile p = (LuaFile) LuaAux.testUdata(L, 1, FILE_HANDLE); + if (p == null) { + LuaAux.pushFail(L); + } else if (p.closef == null) { + L.push(LuaValue.ofString(LuaString.latin1("closed file"))); + } else { + L.push(LuaValue.ofString(LuaString.latin1("file"))); + } + return 1; + } + + /** + * {@code f_tostring}。 + *

+ * C 是 {@code "file (%p)"},地址不可能对得上,只能给个同形状的十六进制值。 + */ + private static int fTostring(LuaState L) { + LuaFile p = toLStream(L); + if (p.closef == null) { + L.push(LuaValue.ofString(LuaString.latin1("file (closed)"))); + } else { + // "%p" 不补前导零,与 BaseLib.tostring 的地址形状保持一致 + L.push(LuaValue.ofString(LuaString.latin1( + "file (0x" + Integer.toHexString(System.identityHashCode(p)) + ")"))); + } + return 1; + } + + /** + * {@code aux_close}:调句柄自己的 {@code closef}。 + *

+ * 先把 {@code closef} 清空再调,顺序照抄 C——这样 {@code __gc} 与 + * 显式 {@code close} 撞到一起时不会关两遍。 + */ + private static int auxClose(LuaState L) { + LuaFile p = toLStream(L); + LuaFunction cf = p.closef; + p.closef = null; + return cf.call(L); + } + + /** {@code f_close} */ + private static int fClose(LuaState L) { + toFile(L); // 先确认是个开着的流 + return auxClose(L); + } + + /** {@code io_close}:不给参数就关默认输出 */ + private static int ioClose(LuaState L) { + if (L.isNoneArg(1)) { + LuaValue v = L.registry.get(IO_OUTPUT); + L.push(v == null ? LuaValue.ofNil() : v); + } + return fClose(L); + } + + /** {@code f_gc}:{@code __gc} 与 {@code __close} 共用 */ + private static int fGc(LuaState L) { + LuaFile p = toLStream(L); + if (p.closef != null) { + auxClose(L); + } + return 0; + } + + /** {@code io_fclose}:普通文件的 closef */ + private static int ioFclose(LuaState L) { + LuaFile p = toLStream(L); + p.err = false; + p.closeRaw(); + LuaAux.fileResult(L, !p.err, null, p.errno); + return p.err ? 3 : 1; + } + + /** {@code io_noclose}:标准文件的 closef,关不掉 */ + private static int ioNoclose(LuaState L) { + LuaFile p = toLStream(L); + p.closef = IoLib::ioNoclose; // 保持打开 + LuaAux.pushFail(L); + L.push(LuaValue.ofString(LuaString.latin1("cannot close standard file"))); + return 2; + } + + /** {@code l_checkmode}:只认 {@code [rwa]%+?[b]*} */ + private static boolean checkMode(String mode) { + if (mode.isEmpty()) { + return false; + } + char c = mode.charAt(0); + if (c != 'r' && c != 'w' && c != 'a') { + return false; + } + int i = 1; + if (i < mode.length() && mode.charAt(i) == '+') { + i++; + } + for (; i < mode.length(); i++) { + if (mode.charAt(i) != 'b') { + return false; + } + } + return true; + } + + /** {@code l_checkmodep}:{@code popen} 只认 {@code "r"} / {@code "w"} */ + private static boolean checkModeP(String mode) { + return (mode.length() == 1) && (mode.charAt(0) == 'r' || mode.charAt(0) == 'w'); + } + + /** {@code opencheck}:打不开直接报错({@code io.lines} 用它) */ + private static void openCheck(LuaState L, LuaString fname, String mode) { + LuaFile f = new LuaFile(); + L.push(LuaValue.ofUserdata(f)); + LuaAux.setMetatableByName(L, FILE_HANDLE); + f.closef = IoLib::ioFclose; + if (!openInto(f, fname.toJavaString(), mode)) { + // 文件名是任意字节,消息得按字节拼 + throw L.libError(LuaString.latin1("cannot open file '").concat(fname) + .concat(LuaString.latin1("' (" + LuaAux.strError(f.errno) + ")"))); + } + } + + /** 按 mode 打开真实文件;失败时把 errno 记进 {@code f} 并返回 false */ + private static boolean openInto(LuaFile f, String fname, String mode) { + char base = mode.charAt(0); + boolean plus = mode.indexOf('+') >= 0; + f.readable = (base == 'r') || plus; + f.writable = (base != 'r') || plus; + f.append = (base == 'a'); + /* + * 目录:C 的 fopen 在只读模式上是开得开的(open(dir, O_RDONLY) 允许), + * 要写才给 EISDIR。Java 这边 RandomAccessFile 两种模式都开不了, + * 所以先判目录再分模式 —— 否则写目录报的是 ENOENT 而不是 EISDIR。 + */ + if (new java.io.File(fname).isDirectory()) { + if (f.writable) { + f.err = true; + f.errno = 21; // EISDIR + return false; + } + f.dir = true; + return true; + } + /* + * {@code "r+"}:C 的 {@code fopen("r+") 不创建文件——它走的是 + * {@code open(path, O_RDWR)},没带 {@code O_CREAT};而 Java 的 + * {@code RandomAccessFile(f, "rw")} 一定会建。于是 + * {@code io.open(不存在的文件, "r+")} 参考给 + * {@code nil, "...: No such file or directory", 2},我们却开出一个 + * 新文件。 + *

+ * 修法是先用只读模式探一次({@code "r"} 模式本身就不创建), + * 打不开就说明文件不在(或者没权限),errno 交给 + * {@code LuaAux.errnoOf} 从异常消息里反查——于是 ENOENT 与 EACCES + * 各归各的,不用在这儿再判一遍。直接用 {@code File.exists()} 会把 + * 「父目录不可搜索」的 EACCES 错报成 ENOENT。 + */ + if (base == 'r' && f.writable) { + try (RandomAccessFile exists = new RandomAccessFile(fname, "r")) { + // 只为确认「这个文件已经在」,句柄立刻关掉 + } catch (IOException e) { + f.err = true; + f.errno = LuaAux.errnoOf(e); + return false; + } + } + try { + f.raf = new RandomAccessFile(fname, f.writable ? "rw" : "r"); + if (base == 'w') { + f.raf.setLength(0); + } + if (f.append) { + f.raf.seek(f.raf.length()); + } + return true; + } catch (IOException e) { + f.err = true; + f.errno = LuaAux.errnoOf(e); + return false; + } + } + + /** + * {@code io.open} + *

+ * 文件名与模式都是 {@code fopen} 的 {@code const char*},在第一个 {@code \0} + * 处砍断:{@code io.open("p", "r\0w")} 的模式是 {@code "r"},不是 {@code "r\0w"} + * (后者 {@code checkMode} 会判成 invalid mode)。 + */ + private static int ioOpen(LuaState L) { + LuaString fname = LuaAux.checkString(L, 1).toCString(); + String filename = fname.toJavaString(); // 交给 Java 文件 API 的那一侧 + String mode = LuaAux.optString(L, 2, LuaString.latin1("r")).toCString().toJavaString(); + LuaFile f = new LuaFile(); + newFile(L, f); + LuaAux.argCheck(L, checkMode(mode), 2, "invalid mode"); + f.err = false; + if (!openInto(f, filename, mode)) { + LuaAux.fileResult(L, false, fname, f.errno); + return 3; + } + return 1; + } + + /** {@code io.popen} */ + private static int ioPopen(LuaState L) { + LuaString fname = LuaAux.checkString(L, 1).toCString(); + String filename = fname.toJavaString(); + String mode = LuaAux.optString(L, 2, LuaString.latin1("r")).toCString().toJavaString(); + LuaFile f = new LuaFile(); + L.push(LuaValue.ofUserdata(f)); + LuaAux.setMetatableByName(L, FILE_HANDLE); + LuaAux.argCheck(L, checkModeP(mode), 2, "invalid mode"); + f.err = false; + boolean forRead = mode.charAt(0) == 'r'; + /* + * C 的 {@code popen} 只接管一条流:读模式接管子进程的 + * {@code stdout},写模式接管它的 {@code stdin},其余一律继承 + * ——它内部就是 {@code pipe} + {@code fork} + 一次 {@code dup2} + + * {@code exec},没碰的 fd 原样传下去。 + *

+ * 实测(参考二进制): + *

+         * io.popen("echo out; echo err 1>&2", "r")  -- 读到 "out","err" 落在 lua 自己的 stderr
+         * io.popen("cat; echo WERR 1>&2", "w")      -- 写进去的 "hello" 被 cat 回显到 stdout
+         * 
+ * Java 的 {@code ProcessBuilder} 默认是三条全接成管道,于是这两 + * 边全被吞掉;更糟的是读模式下子进程的 {@code stdin} 也是管道且没人关 + * ——{@code io.popen("cat", "r")} 会一直等到天荒地老。 + *

+ * 目标流用 {@code LuaState#out} / {@code #err} 而不是 + * {@code Redirect.INHERIT}:INHERIT 走的是 JVM 进程的 fd 1 / fd 2, + * 而本实现的「标准输出 / 标准错误」就是 {@code L.out} / {@code L.err} + * (差分测试正是靠把它们换成内存缓冲区来比 {@code popen} 的输出的)。 + * C 那边的落点是 fd 1 / fd 2,两边是同一件事。 + */ + try { + ProcessBuilder pb = new ProcessBuilder("sh", "-c", filename); + /* + * 重定向必须在 {@code start()} 之前设好 —— {@code ProcessBuilder} + * 是在 {@code start} 那一刻才照这份配置去摆 fd 的,之后再改不影响已 + * 经起来的子进程。读模式要把 {@code stdin} 设为 INHERIT:子进程的 + * 输入应当来自 lua 自己的标准输入({@code cat} 这类命令才读得到东西), + * 而不是一条没人写也没人关的管道。 + */ + if (forRead) { + pb.redirectInput(ProcessBuilder.Redirect.INHERIT); + } + java.lang.Process p = pb.start(); + f.proc = p; + java.util.List ps = new java.util.ArrayList<>(); + if (forRead) { + f.in = p.getInputStream(); + f.readable = true; + ps.add(pump(p.getErrorStream(), L.err)); + } else { + f.out = p.getOutputStream(); + f.writable = true; + ps.add(pump(p.getInputStream(), L.out)); + ps.add(pump(p.getErrorStream(), L.err)); + } + f.pumps = ps.toArray(new Thread[0]); + } catch (IOException e) { + f.err = true; + f.errno = LuaAux.errnoOf(e); + } + f.closef = IoLib::ioPclose; + if (f.err) { + LuaAux.fileResult(L, false, fname, f.errno); + return 3; + } + return 1; + } + + /** + * 起一条守护线程,把子进程那条「没被接管」的流原样搬到 + * {@code LuaState} 的输出流上。 + *

+ * 搬而不是 {@code Redirect.INHERIT} 的理由见 {@link #ioPopen}: + * INHERIT 绕过了 {@code L.out} / {@code L.err}。 + *

+ * 必须一直抽干:子进程往里写而我们不读的话,管道缓冲满了它就会 + * 阻塞 —— C 那边目标是继承来的 fd,不会堵。守护线程是为了「脚本压根没 + * 关这个句柄」时也不拖住 JVM 退出。 + */ + private static Thread pump(InputStream src, OutputStream dst) { + Thread t = new Thread(() -> { + byte[] b = new byte[4096]; + try { + int n; + while ((n = src.read(b)) > 0) { + dst.write(b, 0, n); + dst.flush(); // 行/无缓冲语义:子进程写一行就看得见 + } + } catch (IOException e) { + // 子进程一退出管道就自然断在这里,正常结束 + } finally { + try { + src.close(); + } catch (IOException ignored) { + // 关不掉就算了 + } + } + }); + t.setName("lua-popen-pump"); + t.setDaemon(true); + t.start(); + return t; + } + + /** {@code pclose} 等转发线程的上限(毫秒),理由见 {@link #ioPclose} */ + private static final long PUMP_JOIN_MS = 2000; + + /** {@code io_pclose} */ + private static int ioPclose(LuaState L) { + LuaFile p = toLStream(L); + if (p.proc == null) { + // popen 就没起来过(shell 不存在之类),没有进程可等 + LuaAux.fileResult(L, false, null, (p.errno != 0) ? p.errno : 2); + return 3; + } + int stat; + try { + if (p.out != null) { + p.out.close(); + } + stat = p.proc.waitFor(); + if (p.in != null) { + p.in.close(); + } + } catch (IOException e) { + LuaAux.fileResult(L, false, null, LuaAux.errnoOf(e)); + return 3; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LuaAux.fileResult(L, false, null, 4); // EINTR + return 3; + } + /* + * 等转发线程收尾 —— 子进程已经退出了,它们读到 EOF 就会结束,所以 + * join 不会久等。不等的话 {@code popen} 的最后几行输出可能还没落到 + * {@code L.out} / {@code L.err} 上,比对就时好时坏:C 那边是 fd 直接 + * 写的,{@code pclose} 返回时早就写完了。 + *

+ * 但 join 必须有上限。C 的 {@code pclose} 只 {@code waitpid} + * 那一个子进程,因为没被接管的那两条流是继承来的 fd,孙进程 + * 拿着也无所谓;而这边是管道 —— {@code io.popen("sleep 60 & echo hi")} + * 这种会把写端留给一个活着的孙进程,{@code waitFor} 早返回了,转发线程 + * 却永远读不到 EOF,无上限的 join 就把 {@code f:close()} 卡死。 + * 设一个 2 秒的上限:正常情况下子进程一退出、管道里那点数据瞬间就抽 + * 干了;抽不干的只可能是「还有别人握着写端」,那种情况下等也没用。 + */ + if (p.pumps != null) { + for (Thread t : p.pumps) { + try { + t.join(PUMP_JOIN_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + p.pumps = null; + } + LuaAux.execResult(L, stat); + return 3; + } + + /** {@code io.tmpfile} */ + private static int ioTmpfile(LuaState L) { + LuaFile f = new LuaFile(); + newFile(L, f); + f.err = false; + try { + java.io.File tmp = java.io.File.createTempFile("lua_", ".tmp"); + tmp.deleteOnExit(); + f.raf = new RandomAccessFile(tmp, "rw"); + f.readable = true; + f.writable = true; + } catch (IOException e) { + f.err = true; + f.errno = LuaAux.errnoOf(e); + } + if (f.err) { + LuaAux.fileResult(L, false, null, f.errno); + return 3; + } + return 1; + } + + /** + * {@code getiofile}:取注册表里的默认文件,已关就报错。 + *

+ * 返回的是值而不是句柄,因为 C 的 {@code getiofile} 是 + * {@code lua_getfield}——它把文件压到了栈上,而 {@code io.read} / + * {@code io.write} 正是靠这个把句柄当成返回值带出去的。 + */ + private static LuaValue getIoFile(LuaState L, String findex) { + LuaValue v = L.registry.get(findex); + LuaFile p = (v == null || v.baseType() != Lua.TUSERDATA) + ? null : (LuaFile) LuaUserdata.dataOf(v); + if (p == null || p.closef == null) { + throw L.libError("default " + findex.substring("_IO_".length()) + + " file is closed"); + } + return v; + } + + /** {@code g_iofile}:{@code io.input} / {@code io.output} 共用 */ + private static int gIoFile(LuaState L, String key, String mode) { + if (!L.isNoneArg(1) && !L.arg1(1).isNil()) { + LuaValue a = L.arg1(1); + LuaString s = (a.isString() || a.isNumber()) ? LuaAux.asString(a) : null; + if (s != null) { + openCheck(L, s.toCString(), mode); + } else { + toFile(L); // 确认是个开着的句柄 + L.push(a); + } + L.registry.set(key, new LuaValue(L.stack[L.top - 1])); + } + LuaValue v = L.registry.get(key); + L.push(v == null ? LuaValue.ofNil() : v); + return 1; + } + + private static int ioInput(LuaState L) { + return gIoFile(L, IO_INPUT, "r"); + } + + private static int ioOutput(LuaState L) { + return gIoFile(L, IO_OUTPUT, "w"); + } + + /* ==================== lines ==================== */ + + /** + * {@code aux_lines}。 + *

+ * C 是「压三个值再 {@code lua_rotate} 再 {@code lua_pushcclosure}」,这里是 + * 直接按最终布局(文件、读格式个数、是否要关、各读格式)填 {@link CClosure} + * 的 upvalue——排布完全一样,省掉那次旋转。 + */ + private static void auxLines(LuaState L, boolean toclose) { + int n = L.getTop() - 1; // 读格式个数 + LuaAux.argCheck(L, n <= MAXARGLINE, MAXARGLINE + 2, "too many arguments"); + CClosure cl = new CClosure(IoLib::readline, 3 + n); + cl.upvalue[0].set(L.arg(0)); // 文件 + cl.upvalue[1].set(LuaValue.ofInteger(n)); + cl.upvalue[2].set(LuaValue.ofBoolean(toclose)); + for (int i = 0; i < n; i++) { + cl.upvalue[3 + i].set(L.arg(1 + i)); + } + L.push(LuaValue.ofCClosure(cl)); + } + + private static int fLines(LuaState L) { + toFile(L); + auxLines(L, false); + return 1; + } + + /** + * {@code io.lines}。 + *

+ * 给了文件名时返回的第四个数是「待关闭变量」——泛型 for 会把它当 + * {@code } 处理,迭代结束(或中途出错)时关掉文件。 + */ + private static int ioLines(LuaState L) { + boolean toclose; + if (L.isNoneArg(1)) { + L.pushNil(); + } + if (L.arg1(1).isNil()) { + LuaValue v = L.registry.get(IO_INPUT); + if (v == null) { + v = LuaValue.ofNil(); + } + if (L.isNoneArg(1)) { + L.push(v); // 一个参数都没有:直接让它当第 1 个 + } else { + L.push(v); + L.replace(1); // 显式传了 nil:覆盖掉 + } + toFile(L); // 确认默认输入还开着 + toclose = false; + } else { + LuaString filename = LuaAux.checkString(L, 1).toCString(); + openCheck(L, filename, "r"); + L.replace(1); + toclose = true; + } + auxLines(L, toclose); + if (toclose) { + L.pushNil(); // state + L.pushNil(); // control + L.pushValue(1); // 文件作为待关闭变量 + return 4; + } + return 1; + } + + /* ==================== 读 ==================== */ + + /** {@code test_eof}:{@code f:read(0)} 用它 */ + private static boolean testEof(LuaState L, LuaFile f) { + int c = f.getc(); + f.ungetc(c); + L.push(LuaValue.ofString(LuaString.latin1(""))); + return c >= 0; + } + + /** {@code read_line}:一直读到 {@code '\n'} 或 EOF;{@code chop} 决定要不要留换行 */ + private static boolean readLine(LuaState L, LuaFile f, boolean chop) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + int c; + do { + c = f.getc(); + if (c < 0 || c == '\n') { + break; + } + bos.write(c); + } while (true); + if (!chop && c == '\n') { + bos.write('\n'); + } + byte[] b = bos.toByteArray(); + L.push(LuaValue.ofString(L.newString(b, 0, b.length))); + return (c == '\n') || b.length > 0; + } + + /** {@code read_all} */ + private static void readAll(LuaState L, LuaFile f) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + for (;;) { + int n = f.read(buf, 0, buf.length); + if (n <= 0) { + break; + } + bos.write(buf, 0, n); + } + byte[] b = bos.toByteArray(); + L.push(LuaValue.ofString(L.newString(b, 0, b.length))); + } + + /** + * {@code read_chars}。 + *

+ * C 里 {@code g_read} 写的是 {@code size_t l = (size_t)luaL_checkinteger(L, n)}: + * 负数按无符号解释成一个天文数字,随后 {@code luaL_prepbuffsize} 去 + * 预分配这么大一块,{@code resizebox} 分配失败抛 {@code "not enough memory"}。 + * 实测 {@code f:read(-1)} 在参考实现上就是 {@code false not enough memory}, + * 而不是什么整数越界。 + *

+ * 所以这里不预分配 {@code n} 个字节,改成循环读到「流干」或「够数」为止 + * ——这也更贴近 {@code fread}:短读时 C 会继续读到 {@code n} 或 EOF。只有大得 + * 任何系统都分配不出来时才抛内存错。 + */ + private static boolean readChars(LuaState L, LuaFile f, long n) { + if (n < 0 || n > MAX_READ_ALLOC) { + /* + * C 那边是 {@code resizebox} 里 {@code lua_pushliteral + + * lua_error},而 {@code lua_error} 认出这是内存错误消息,于是走 + * {@code luaM_error}({@code LUA_ERRMEM})—— + * {@code xpcall} 的处理函数接不到。 + */ + LuaError e = new LuaError(LuaValue.ofString(LuaString.latin1("not enough memory"))); + e.errMem = true; + throw e; + } + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] buf = new byte[CHUNK]; + long left = n; + while (left > 0) { + int nr = f.read(buf, 0, (int) Math.min(left, CHUNK)); + if (nr <= 0) { + break; + } + bos.write(buf, 0, nr); + left -= nr; + } + byte[] b = bos.toByteArray(); + L.push(LuaValue.ofString(L.newString(b, 0, b.length))); + return b.length > 0; + } + + /** + * {@code read_number}。 + *

+ * 先把「一个合法数字前缀」读进缓冲,再交给 {@code lua_stringtonumber} 判定—— + * 这样 {@code "3x"} 会读成 {@code 3} 并把 {@code x} 留在流里,与 C 一致。 + */ + private static boolean readNumber(LuaState L, LuaFile f) { + StringBuilder b = new StringBuilder(); + int count = 0; + boolean hex = false; + int c; + do { + c = f.getc(); + } while (c >= 0 && isSpace(c)); + if (c == '-' || c == '+') { + b.append((char) c); + c = f.getc(); + } + if (c == '0') { + b.append('0'); + c = f.getc(); + if (c == 'x' || c == 'X') { + hex = true; + b.append((char) c); + c = f.getc(); + } else { + count = 1; + } + } + while (c >= 0 && (hex ? isXdigit(c) : isDigit(c)) && b.length() <= L_MAXLENNUM) { + b.append((char) c); + count++; + c = f.getc(); + } + if (c == '.') { // locale 的小数点这里只认 '.',与 C 的默认一致 + b.append('.'); + c = f.getc(); + while (c >= 0 && (hex ? isXdigit(c) : isDigit(c)) && b.length() <= L_MAXLENNUM) { + b.append((char) c); + count++; + c = f.getc(); + } + } + if (count > 0 && (c == 'e' || c == 'E' || (hex && (c == 'p' || c == 'P')))) { + b.append((char) c); + c = f.getc(); + if (c == '-' || c == '+') { + b.append((char) c); + c = f.getc(); + } + while (c >= 0 && isDigit(c) && b.length() <= L_MAXLENNUM) { + b.append((char) c); + c = f.getc(); + } + } + f.ungetc(c); + LuaValue num = (b.length() > L_MAXLENNUM) ? null : Numeral.str2num(b.toString()); + if (num != null) { + L.push(num); + return true; + } + L.pushNil(); + return false; + } + + /** + * C 的 {@code isspace}(C locale):空格、{@code \f \n \r \t \v}。 + *

+ * 竖制表符写成 {@code 11}:Java 没有 {@code '\v'} 这个转义({@code '\f'} 倒是有)。 + */ + private static boolean isSpace(int c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == 11 || c == '\f'; + } + + private static boolean isDigit(int c) { + return c >= '0' && c <= '9'; + } + + private static boolean isXdigit(int c) { + return isDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); + } + + /** + * {@code g_read}。 + * + * @param first 第一个读格式所在的绝对参数下标({@code io.read} 是 1, + * {@code f:read} 是 2) + */ + private static int gRead(LuaState L, LuaFile f, int first) { + int nargs = L.getTop() - 1; + f.err = false; + int n; + boolean success; + if (nargs == 0) { + success = readLine(L, f, true); + n = first + 1; + } else { + LuaAux.checkStack(L, nargs + LuaState.MINSTACK, "too many arguments"); + success = true; + n = first; + for (int left = nargs; left > 0 && success; left--, n++) { + LuaValue a = L.arg1(n); + if (a.baseType() == Lua.TNUMBER) { + long len = LuaAux.checkInteger(L, n); + success = (len == 0) ? testEof(L, f) : readChars(L, f, len); + } else { + LuaString p = LuaAux.checkString(L, n); + int at = (p.length() > 0 && p.byteAt(0) == '*') ? 1 : 0; + int fmt = (at < p.length()) ? p.byteAt(at) : 0; + switch (fmt) { + case 'n': + success = readNumber(L, f); + break; + case 'l': + success = readLine(L, f, true); + break; + case 'L': + success = readLine(L, f, false); + break; + case 'a': + readAll(L, f); + success = true; + break; + default: + throw L.argError(n, "invalid format"); + } + } + } + } + if (f.err) { + LuaAux.fileResult(L, false, null, f.errno); + return 3; + } + if (!success) { + L.top--; // 丢掉最后一个结果 + LuaAux.pushFail(L); + } + return n - first; + } + + private static int ioRead(LuaState L) { + LuaValue v = getIoFile(L, IO_INPUT); + L.push(v); // 压在所有读格式之后,g_read 会把它顶掉 + return gRead(L, (LuaFile) LuaUserdata.dataOf(v), 1); + } + + private static int fRead(LuaState L) { + return gRead(L, toFile(L), 2); + } + + /** + * {@code io_readline}:{@code lines} 的迭代函数。 + */ + private static int readline(LuaState L) { + LuaFile p = (LuaFile) LuaUserdata.dataOf(L.upvalue(0)); + int n = (int) L.upvalue(1).i; + if (p.closef == null) { + throw L.libError("file is already closed"); + } + L.setTop(1); + LuaAux.checkStack(L, n, "too many arguments"); + for (int i = 0; i < n; i++) { + L.push(L.upvalue(3 + i)); + } + n = gRead(L, p, 2); + if (L.stack[L.top - n].toBoolean()) { + return n; + } + if (n > 1) { + // 第二个结果是错误信息 + throw L.libError(L.stack[L.top - n + 1].asString().toLatin1String()); + } + if (L.upvalue(2).toBoolean()) { + L.setTop(0); + L.push(L.upvalue(0)); + auxClose(L); + } + return 0; + } + + /* ==================== 写 ==================== */ + + /** {@code g_write} */ + private static int gWrite(LuaState L, LuaFile f, int arg) { + int nargs = L.getTop() - arg; + boolean status = true; + f.err = false; + for (; nargs-- > 0; arg++) { + LuaValue v = L.arg1(arg); + byte[] b; + if (v.baseType() == Lua.TNUMBER) { + /* + * 浮点按 LUA_NUMBER_FMT(%.14g)写,不补 .0—— + * 那是 luaO_tostring 才做的事。实测 io.write(-0.0) 出 "-0", + * 而 print(-0.0) 出 "-0.0"。 + */ + String s = v.isInteger() ? Long.toString(v.i) : Vm.formatG(v.n, 14); + b = s.getBytes(StandardCharsets.US_ASCII); + } else { + LuaString s = LuaAux.checkString(L, arg); + b = s.bytes; + } + f.write(b, 0, b.length); + status = status && !f.err; + } + if (status) { + return 1; // 句柄已经在栈顶 + } + LuaAux.fileResult(L, false, null, f.errno); + return 3; + } + + private static int ioWrite(LuaState L) { + LuaValue v = getIoFile(L, IO_OUTPUT); + L.push(v); // 压在待写的值之后,g_write 返回的 1 个值就是它 + return gWrite(L, (LuaFile) LuaUserdata.dataOf(v), 1); + } + + private static int fWrite(LuaState L) { + LuaFile f = toFile(L); + L.pushValue(1); // 把句柄压到栈顶作为返回值 + return gWrite(L, f, 2); + } + + /* ==================== 定位与缓冲 ==================== */ + + /** {@code f_seek} */ + private static int fSeek(LuaState L) { + LuaFile f = toFile(L); + int op = LuaAux.checkOption(L, 2, "cur", new String[] { "set", "cur", "end" }); + long p3 = LuaAux.optInteger(L, 3, 0); + f.err = false; + long pos = f.seek(p3, op); + if (pos < 0) { + LuaAux.fileResult(L, false, null, f.errno); + return 3; + } + L.push(LuaValue.ofInteger(f.tell())); + return 1; + } + + /** + * {@code f_setvbuf}。 + *

+ * 参数照抄校验(模式名必须是 {@code no} / {@code full} / {@code line}), + * 但不真的改缓冲策略——见类注释第 1 条。 + */ + private static int fSetvbuf(LuaState L) { + toFile(L); + LuaAux.checkOption(L, 2, null, new String[] { "no", "full", "line" }); + LuaAux.optInteger(L, 3, LUAL_BUFFERSIZE); + LuaAux.fileResult(L, true, null, 0); + return 1; + } + + private static int ioFlush(LuaState L) { + LuaFile f = (LuaFile) LuaUserdata.dataOf(getIoFile(L, IO_OUTPUT)); + f.err = false; + f.flush(); + LuaAux.fileResult(L, !f.err, null, f.errno); + return f.err ? 3 : 1; + } + + private static int fFlush(LuaState L) { + LuaFile f = toFile(L); + f.err = false; + f.flush(); + LuaAux.fileResult(L, !f.err, null, f.errno); + return f.err ? 3 : 1; + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/LCType.java b/src/main/java/com/github/esrrhs/fakescript/lua/LCType.java new file mode 100644 index 0000000..6a7dac4 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/LCType.java @@ -0,0 +1,103 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * 字符分类 —— 对应 {@code lctype.c} 的 {@code luai_ctype_} 表。 + *

+ * 为什么照搬这张表而不是用 {@code Character.isDigit} 之类:Lua 的字符分类是 + * 与 locale 无关的 ASCII 固定表,且语义与 C 标准库并不完全一致—— + * 最明显的是 {@code lislalpha} 把 {@code '_'} 当作字母(所以 Lua 标识符可以 + * 以下划线开头),而 {@code isalpha} 不会。照搬表可以逐字节对齐上游行为。 + *

+ * 表按 {@code c + 1} 索引,是为了让 EOZ(-1)也有一个条目。 + */ +final class LCType { + + private LCType() { + } + + private static final int ALPHABIT = 0; + private static final int DIGITBIT = 1; + private static final int PRINTBIT = 2; + private static final int SPACEBIT = 3; + private static final int XDIGITBIT = 4; + + private static final int MASK_ALPHA = 1 << ALPHABIT; + private static final int MASK_DIGIT = 1 << DIGITBIT; + private static final int MASK_PRINT = 1 << PRINTBIT; + private static final int MASK_SPACE = 1 << SPACEBIT; + private static final int MASK_XDIGIT = 1 << XDIGITBIT; + + /** + * 逐字节照搬 {@code lctype.c}。NONA 取 0(未定义 LUA_UCID), + * 即所有非 ASCII 字节都不是字母——默认配置下 Lua 标识符只能是 ASCII。 + */ + private static final byte[] RAW = { + (byte) 0x00, /* EOZ */ + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, /* 0. */ + (byte) 0x00, (byte) 0x08, (byte) 0x08, (byte) 0x08, (byte) 0x08, (byte) 0x08, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, /* 1. */ + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x0c, (byte) 0x04, (byte) 0x04, (byte) 0x04, (byte) 0x04, (byte) 0x04, (byte) 0x04, (byte) 0x04, /* 2. */ + (byte) 0x04, (byte) 0x04, (byte) 0x04, (byte) 0x04, (byte) 0x04, (byte) 0x04, (byte) 0x04, (byte) 0x04, + (byte) 0x16, (byte) 0x16, (byte) 0x16, (byte) 0x16, (byte) 0x16, (byte) 0x16, (byte) 0x16, (byte) 0x16, /* 3. */ + (byte) 0x16, (byte) 0x16, (byte) 0x04, (byte) 0x04, (byte) 0x04, (byte) 0x04, (byte) 0x04, (byte) 0x04, + (byte) 0x04, (byte) 0x15, (byte) 0x15, (byte) 0x15, (byte) 0x15, (byte) 0x15, (byte) 0x15, (byte) 0x05, /* 4. */ + (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x05, + (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x05, /* 5. */ + (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x04, (byte) 0x04, (byte) 0x04, (byte) 0x04, (byte) 0x05, + (byte) 0x04, (byte) 0x15, (byte) 0x15, (byte) 0x15, (byte) 0x15, (byte) 0x15, (byte) 0x15, (byte) 0x05, /* 6. */ + (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x05, + (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x05, /* 7. */ + (byte) 0x05, (byte) 0x05, (byte) 0x05, (byte) 0x04, (byte) 0x04, (byte) 0x04, (byte) 0x04, (byte) 0x00, + /* 8. .. f. :NONA == 0,全部非 ASCII 字节都不是字母 */ + }; + + /** 补齐 128..255(NONA == 0,全为 0) */ + private static byte[] pad(byte[] raw) { + byte[] full = new byte[257]; + System.arraycopy(raw, 0, full, 0, raw.length); + return full; + } + + private static final byte[] CTYPE = pad(RAW); + + private static int prop(int c) { + // c 可能是 EOZ(-1);表按 c+1 索引 + int idx = c + 1; + if (idx < 0 || idx >= CTYPE.length) { + return 0; + } + return CTYPE[idx] & 0xFF; + } + + /** {@code lislalpha}:字母或 '_'(注意:包含下划线,与 C 的 isalpha 不同) */ + static boolean isAlpha(int c) { + return (prop(c) & MASK_ALPHA) != 0; + } + + /** {@code lislalnum} */ + static boolean isAlnum(int c) { + return (prop(c) & (MASK_ALPHA | MASK_DIGIT)) != 0; + } + + static boolean isDigit(int c) { + return (prop(c) & MASK_DIGIT) != 0; + } + + static boolean isSpace(int c) { + return (prop(c) & MASK_SPACE) != 0; + } + + static boolean isPrint(int c) { + return (prop(c) & MASK_PRINT) != 0; + } + + static boolean isXdigit(int c) { + return (prop(c) & MASK_XDIGIT) != 0; + } + + /** {@code ltolower}:只对大写字母与 '.' 有意义,这正是 Lua 需要的范围 */ + static int toLower(int c) { + return c | ('A' ^ 'a'); + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/LClosure.java b/src/main/java/com/github/esrrhs/fakescript/lua/LClosure.java new file mode 100644 index 0000000..ac16a12 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/LClosure.java @@ -0,0 +1,56 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * Lua 闭包 —— 对应 {@code lobject.h} 的 {@code LClosure}。 + *

+ * 闭包 = 原型 + upvalue 数组。同一个 {@link Proto} 可以被任意多个闭包共享 + * (循环里 {@code local f = function() end} 就会共享),可变的部分只有 + * {@code upvals},这与 C 版一致。 + *

+ * 放入 {@link LuaValue} 时用标签 {@link Lua#VLIF}。 + */ +public final class LClosure { + + public final Proto p; + + /** upvalue 数组,长度等于 {@code p.sizeupvalues}(主 chunk 例外,见下) */ + public final UpVal[] upvals; + + public LClosure(Proto p) { + this(p, p.sizeupvalues); + } + + /** + * 用显式的 upvalue 个数构造。 + *

+ * 主 chunk 必须走这条路:{@code luaY_parser} 固定按 1 个 upvalue 建闭包 + * ({@code _ENV}),之后由 {@code lua_load} 把全局表塞进去,与 + * {@code p.sizeupvalues} 是否相等无关。 + */ + public LClosure(Proto p, int nupvals) { + LuaState.chargeGc(); + this.p = p; + this.upvals = new UpVal[Math.max(nupvals, p.sizeupvalues)]; + } + + /** + * 直接替换第 {@code i} 个 upvalue —— {@code lapi.c: lua_upvaluejoin} 的落点。 + *

+ * C 版是 {@code *up1 = *up2}:把「upvalue 指针」换掉,而不是把值抄过去。 + * 换完之后两个闭包就共享同一个 {@link UpVal},此后一边写另一边可见—— + * 这正是 {@code debug.upvaluejoin} 的全部意义。所以这里必须整个替换数组元素, + * 不能 {@code upvals[i].set(...)}。 + */ + public void setUpVal(int i, UpVal uv) { + upvals[i] = uv; + } + + /** 把所有 upvalue 初始化为「关闭且值为 nil」({@code lfunc.c: luaF_initupvals}) */ + public void initUpvals() { + for (int i = 0; i < upvals.length; i++) { + if (upvals[i] == null) { + upvals[i] = UpVal.closed(LuaValue.ofNil()); + } + } + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/Lexer.java b/src/main/java/com/github/esrrhs/fakescript/lua/Lexer.java new file mode 100644 index 0000000..07b780d --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/Lexer.java @@ -0,0 +1,945 @@ +package com.github.esrrhs.fakescript.lua; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * 词法分析器 —— 逐行翻译 {@code llex.c} / {@code llex.h}。 + *

+ * 这是「直接能从 C 源码翻译」这一路线上的第一块真骨头,也是最容易在 + * 「看起来能跑」之后悄悄错的地方。翻译时守住的几条上游约定,改任何一条都会 + * 在某个真实脚本上炸掉: + *

    + *
  1. 单字符 token 的值就是字符本身的编码({@code '+' → 43}), + * 其余 token 从 {@code FIRST_RESERVED}(256+1)开始。这条决定了后面 + * 整个 parser 的 {@code switch} 长什么样;
  2. + *
  3. 数字先按整数解析、失败才按浮点({@link Numeral#str2num}), + * 所以 {@code 1} 是 TK_INT 而 {@code 1.0} 是 TK_FLT;
  4. + *
  5. 转义序列全部支持:{@code \z} 吞空白、{@code \xXX} 十六进制、 + * {@code \ddd} 十进制、以及反斜杠+u+花括号的 Unicode 码点形式, + * 且错误信息与上游逐字一致(差分测试比对的就是这些字符串);
  6. + *
  7. 长字符串/长注释开头的第一个换行被吞掉,且中间的 + * {@code \r\n} / {@code \n\r} 统一归一成一个 {@code \n}。
  8. + *
+ *

+ * 没有 {@code luaX_init}:C 版那里把保留字预先塞进字符串表并打上 + * {@code extra} 标记,这里保留字查一张静态 HashMap 即可。 + *

+ * 也没有 {@code ls->h} 那张「避免长字符串被回收」的表:C 版它同时兼作 + * 长字符串去重,但长字符串不驻留、比较一律按内容,去重与否不可观测, + * 去重只是省内存。这里只做短字符串驻留({@link StringPool}),语义一致。 + */ +public final class Lexer { + + /** + * 单字符 token 用自身编码表示,其余从这里开始({@code UCHAR_MAX + 1})。 + */ + public static final int FIRST_RESERVED = 256 + 1; + + /* ORDER RESERVED —— 顺序必须与 C 的 enum RESERVED 完全一致 */ + public static final int TK_AND = FIRST_RESERVED + 0; + public static final int TK_BREAK = FIRST_RESERVED + 1; + public static final int TK_DO = FIRST_RESERVED + 2; + public static final int TK_ELSE = FIRST_RESERVED + 3; + public static final int TK_ELSEIF = FIRST_RESERVED + 4; + public static final int TK_END = FIRST_RESERVED + 5; + public static final int TK_FALSE = FIRST_RESERVED + 6; + public static final int TK_FOR = FIRST_RESERVED + 7; + public static final int TK_FUNCTION = FIRST_RESERVED + 8; + public static final int TK_GOTO = FIRST_RESERVED + 9; + public static final int TK_IF = FIRST_RESERVED + 10; + public static final int TK_IN = FIRST_RESERVED + 11; + public static final int TK_LOCAL = FIRST_RESERVED + 12; + public static final int TK_NIL = FIRST_RESERVED + 13; + public static final int TK_NOT = FIRST_RESERVED + 14; + public static final int TK_OR = FIRST_RESERVED + 15; + public static final int TK_REPEAT = FIRST_RESERVED + 16; + public static final int TK_RETURN = FIRST_RESERVED + 17; + public static final int TK_THEN = FIRST_RESERVED + 18; + public static final int TK_TRUE = FIRST_RESERVED + 19; + public static final int TK_UNTIL = FIRST_RESERVED + 20; + public static final int TK_WHILE = FIRST_RESERVED + 21; + public static final int TK_IDIV = FIRST_RESERVED + 22; + public static final int TK_CONCAT = FIRST_RESERVED + 23; + public static final int TK_DOTS = FIRST_RESERVED + 24; + public static final int TK_EQ = FIRST_RESERVED + 25; + public static final int TK_GE = FIRST_RESERVED + 26; + public static final int TK_LE = FIRST_RESERVED + 27; + public static final int TK_NE = FIRST_RESERVED + 28; + public static final int TK_SHL = FIRST_RESERVED + 29; + public static final int TK_SHR = FIRST_RESERVED + 30; + public static final int TK_DBCOLON = FIRST_RESERVED + 31; + public static final int TK_EOS = FIRST_RESERVED + 32; + public static final int TK_FLT = FIRST_RESERVED + 33; + public static final int TK_INT = FIRST_RESERVED + 34; + public static final int TK_NAME = FIRST_RESERVED + 35; + public static final int TK_STRING = FIRST_RESERVED + 36; + + /** 保留字个数({@code NUM_RESERVED}) */ + public static final int NUM_RESERVED = TK_WHILE - FIRST_RESERVED + 1; + + /** {@code luaX_tokens} */ + private static final String[] TOKENS = { + "and", "break", "do", "else", "elseif", + "end", "false", "for", "function", "goto", "if", + "in", "local", "nil", "not", "or", "repeat", + "return", "then", "true", "until", "while", + "//", "..", "...", "==", ">=", "<=", "~=", + "<<", ">>", "::", "", + "", "", "", "" + }; + + private static final Map RESERVED = new HashMap<>(); + + static { + for (int i = 0; i < NUM_RESERVED; i++) { + RESERVED.put(TOKENS[i], FIRST_RESERVED + i); + } + } + + /** {@code SemInfo + Token}:语义信息随 token 类型不同落在不同的字段上 */ + public static final class Token { + public int token = TK_EOS; + /** TK_NAME / TK_STRING */ + public LuaString s; + /** TK_INT */ + public long i; + /** TK_FLT */ + public double r; + + void reset() { + token = TK_EOS; + s = null; + i = 0; + r = 0; + } + + /** 快照:{@code next()} 之后 {@code t} 会被原地改写,需要留存的场合必须先复制 */ + public Token copy() { + Token c = new Token(); + c.token = token; + c.s = s; + c.i = i; + c.r = r; + return c; + } + + } + + /** + * {@code luaX_token2str}。 + *

+ * 保留字要带引号({@code 'end'}),而 {@code } / {@code } / + * {@code } / {@code } / {@code } 不带——上游的分界点 + * 就是 {@code token < TK_EOS}。写反了消息会变成 {@code end expected} + * 而不是 {@code 'end' expected}。 + */ + public static String tokenName(int token) { + if (token < FIRST_RESERVED) { + return LCType.isPrint(token) ? "'" + (char) token + "'" : "'<\\" + token + ">'"; + } + String s = TOKENS[token - FIRST_RESERVED]; + return token < TK_EOS ? "'" + s + "'" : s; + } + + /* ---------- 状态(对应 LexState) ---------- */ + + private final Zio z; + private final StringPool strings; + + /** + * 源名的原始字节 —— 错误信息里用({@code luaG_addinfo} 的 {@code src})。 + *

+ * 存字节而不是 {@code String}:源名本身是 Lua 字符串(字节串),而错误信息里 + * 还会夹带 token 的原始字节(可能是非法 UTF-8)。两侧差分比对是逐字节 + * 比 stdout,任何一次「先解码成 String 再编码回去」都会在真实脚本上露出破绽。 + */ + private final byte[] sourceBytes; + + /** {@link #sourceBytes} 按 UTF-8 解码后的 Java 视图,仅用于日志与诊断 */ + public final String source; + + public int linenumber = 1; + public int lastline = 1; + + /** 当前 token */ + public Token t = new Token(); + /** 前瞻 token({@code lookahead.token == TK_EOS} 表示没有) */ + private Token lookahead = new Token(); + + private int current; + private byte[] buff = new byte[64]; + private int buffLen; + + public Lexer(byte[] data, String source) { + this(new Zio(data), source, new StringPool()); + } + + public Lexer(Zio z, String source, StringPool strings) { + this(z, (source == null ? "?" : source).getBytes(StandardCharsets.UTF_8), strings); + } + + public Lexer(byte[] data, byte[] chunkName, StringPool strings) { + this(new Zio(data), chunkName, strings); + } + + public Lexer(Zio z, byte[] chunkName, StringPool strings) { + this(z, chunkName, strings, z.getc()); + } + + /** + * {@code firstChar} 是已经从流里读出来的那个首字节。 + *

+ * 对应 {@code ldo.c: f_parser}:{@code int c = zgetc(z)} 读完首字节后 + * 直接把它交给 {@code luaY_parser(..., c)},词法器是带着它起步的。 + * 少了这一手,首字节就要再读一遍——对惰性读函数来说就是多调一次。 + * 实测 {@code load(function () return table.remove(t, 1) end)} + * ({@code t = {nil, "return ", "3"}}):首读返回 nil 即结束,参考实现 + * 只调读函数一次并编出空 chunk;再读一次就会接着吃掉 {@code "return "} + * 和 {@code "3"},编出 {@code return 3}——上游 {@code calls.lua} 断言 + * 的是前者({@code f() == nil})。 + */ + public Lexer(Zio z, byte[] chunkName, StringPool strings, int firstChar) { + this.z = z; + this.strings = strings; + this.sourceBytes = (chunkName == null) ? new byte[] { '?' } : chunkName; + this.source = new String(sourceBytes, StandardCharsets.UTF_8); + this.current = firstChar; // firstchar + } + + /* ---------- 字符与缓冲区 ---------- */ + + private void nextChar() { + current = z.getc(); + } + + private boolean currIsNewline() { + return current == '\n' || current == '\r'; + } + + /** {@code save} */ + private void save(int c) { + if (buffLen + 1 > buff.length) { + if (buff.length >= Integer.MAX_VALUE / 2) { + lexError("lexical element too long", 0); + } + buff = Arrays.copyOf(buff, buff.length * 2); + } + buff[buffLen++] = (byte) c; + } + + /** {@code save_and_next} */ + private void saveAndNext() { + save(current); + nextChar(); + } + + private void resetBuffer() { + buffLen = 0; + } + + /** {@code luaZ_buffremove} */ + private void buffRemove(int n) { + buffLen -= n; + } + + private String bufferText() { + return new String(buff, 0, buffLen, StandardCharsets.ISO_8859_1); + } + + /* ---------- 错误 ---------- */ + + /** + * {@code lexerror}:{@code source:line: msg[ near ]},逐字节拼装。 + *

+ * 源名要先过一遍 {@code luaO_chunkid}:{@code @xxx} 去掉 {@code @}、 + * {@code =xxx} 去掉 {@code =}、其余包成 {@code [string "xxx"]}—— + * C 的 {@code luaG_addinfo} 就是这么做的。漏掉这一步, + * {@code require} 一个语法错误的模块时会报 + * {@code @/tmp/m.lua:1: syntax error} 而参考实现报 {@code /tmp/m.lua:1: ...}; + * 只剥不包则会把 {@code load("x++")} 报成 {@code x++:1: ...}。 + */ + private LuaError lexError(String msg, int token) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] id = LuaState.chunkId(sourceBytes); + out.write(id, 0, id.length); + writeAscii(out, ":" + linenumber + ": " + msg); + if (token != 0) { + writeAscii(out, " near "); + // 只有 不带引号(上游对 TK_EOS 之后的「名字/字符串/数字」 + // 由 txtToken 补回引号,而 TK_EOS 本身不补) + boolean quoted = token != TK_EOS; + if (quoted) { + writeAscii(out, "'"); + } + switch (token) { + case TK_NAME: + case TK_STRING: + case TK_FLT: + case TK_INT: + out.write(buff, 0, buffLen); // token 原始字节,可能是非法 UTF-8 + break; + default: + writeAscii(out, textOf(token)); + break; + } + if (quoted) { + writeAscii(out, "'"); + } + } + LuaError e = new LuaError(LuaValue.ofString(LuaString.of(out.toByteArray()))); + /* + * C 版 {@code lexerror} 结尾是 {@code luaD_throw(L, LUA_ERRSYNTAX)}: + * 直接跳,不过 {@code L->errfunc}。所以语法错误消息永远不带 + * {@code xpcall} 处理器的痕迹(实测:{@code xpcall(function() return + * load("x++") end, H)} 给裸的 {@code [string "x++"]:1: syntax error}), + * 而读函数里 {@code error()} 出来的会给 {@code H:...}。 + */ + e.directThrow = true; + throw e; + } + + /** {@code luaX_syntaxerror} */ + public LuaError syntaxError(String msg) { + return lexError(msg, t.token); + } + + /** + * {@code luaX_token2str} 去掉两侧引号后的内容。 + *

+ * 注意 {@code txtToken} 给单字符 token 加的引号是 '<' 和 '>' 之外的普通单引号, + * 而 {@code token < FIRST_RESERVED} 且不可打印时形如 {@code '<\228>'}。 + */ + private static String textOf(int token) { + if (token < FIRST_RESERVED) { + return LCType.isPrint(token) ? String.valueOf((char) token) : "<\\" + token + ">"; + } + return TOKENS[token - FIRST_RESERVED]; + } + + private static void writeAscii(ByteArrayOutputStream out, String s) { + byte[] b = s.getBytes(StandardCharsets.US_ASCII); + out.write(b, 0, b.length); + } + + /* ---------- 对外接口 ---------- */ + + /** {@code luaX_next} */ + public void next() { + lastline = linenumber; + if (lookahead.token != TK_EOS) { + Token tmp = t; + t = lookahead; + lookahead = tmp; + lookahead.reset(); + } else { + t.token = llex(t); + } + } + + /** {@code luaX_lookahead} */ + public int lookahead() { + lookahead.token = llex(lookahead); + return lookahead.token; + } + + public int token() { + return t.token; + } + + /** + * {@code luaX_newstring}。 + *

+ * 除了驻留短串,这里还多一步:长串也要去重。C 版把 {@code ls->h} + * 当集合用(键是串本身),命中就返回表里存的那一副本;注释写得很明白 + * ——{@code "It also internalizes long strings, ensuring there is only one + * copy of each unique string"}。 + *

+ * 少了这一步,同一个 50 字符串字面量在主 chunk 与内层函数里会是两个对象, + * 于是 {@code string.format("%p", s)} 给出不同地址。上游 + * {@code literals.lua}「reuse of long strings」整段都在断言这个;而那段里 + * {@code sd = "..." .. "..."} 又要不等于字面量——运行时拼接不进这张表, + * 所以新对象,正好对上。 + */ + public LuaString newString(byte[] buf, int off, int len) { + LuaString s = strings.intern(LuaString.of(buf, off, len)); + if (s.isShort()) { + return s; // 短串已由驻留池全局唯一 + } + LuaString saved = savedStrings.get(s); + if (saved != null) { + return saved; + } + savedStrings.put(s, s); + return s; + } + + /** + * {@code ls->h} 当集合用的那一半:内容 → 首次出现的那个实例。 + * 只活到编译结束(C 版也是「锚住到编译完」),所以强引用即可。 + */ + private final Map savedStrings = new HashMap<>(); + + /* ---------- 常量缓存(lcode.c: addk 用的 ls->h) ---------- */ + + /** + * {@code ls->h}:整份源码共享的「常量 → 下标」缓存。 + *

+ * 共享(而不是每个函数一份)会带出两个反直觉的行为,都必须照搬,否则编出来的 + * 常量表与参考实现不一致: + *

    + *
  1. 跨函数会串味。内层函数从 0 开始编号,写完就把缓存里的下标改成了 + * 内层的小下标;回到外层,外层拿这个下标一查——指向的是外层自己的另一个 + * 常量——校验失败,于是同一个字符串再加一份。上游的常量表里确实会 + * 出现重复项,不是优化漏了;
  2. + *
  3. 整数与浮点在缓存里互相覆盖。Lua 表用 {@code ==} 判键相等,所以 + * {@code 1} 与 {@code 1.0} 是同一个键;命中后靠变体标签校验否决, + * 但缓存项已经被 {@code 1.0} 的下标覆盖了——下次再写 {@code 1} 又会新建。 + * 这也是上游常量表会出现「看起来该复用却没复用」的原因。
  4. + *
+ */ + private final Map constCache = new HashMap<>(); + + /** nil 不能做 Lua 表的键,上游拿表自身当键;这里直接用一个哨兵对象 */ + private static final Object NIL_KEY = new Object(); + + /** + * 数值型缓存键,相等性按 Lua 的 {@code ==}(跨整数/浮点也成立)。 + *

+ * 不能用 {@code Double}:{@code Double.equals} 认为 {@code -0.0 != 0.0}、 + * {@code NaN == NaN},而 Lua 恰恰相反({@code -0.0 == 0.0} 成立;NaN 与 + * 任何值都不等,所以每个 NaN 字面量各占一个常量槽)。 + *

+ * 整数要留着原始的 {@code long}:超过 2^53 的整数转 double 会丢精度, + * 两个不同的大整数会共用同一个哈希桶——那不要紧({@link #equals} 判得准), + * 但若只存 double 就会把两个不同的常量误判成同一个。 + */ + private static final class NumKey { + private final boolean isInt; + private final long i; + private final double d; + + NumKey(boolean isInt, long i, double d) { + this.isInt = isInt; + this.i = i; + this.d = d; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof NumKey)) { + return false; + } + NumKey k = (NumKey) o; + if (isInt && k.isInt) { + return i == k.i; + } + if (!isInt && !k.isInt) { + return d == k.d; + } + return isInt ? ((double) i) == k.d : d == ((double) k.i); + } + + @Override + public int hashCode() { + double v = isInt ? (double) i : d; + long bits = Double.doubleToLongBits(v == 0.0 ? 0.0 : v); + return (int) (bits ^ (bits >>> 32)); + } + } + + /** double 的尾数位数({@code DBL_MANT_DIG}) */ + private static final int MANT_DIG = 53; + + /** + * 常量缓存键;{@code null} 表示这个值不该进缓存。 + *

+ * 整值浮点要换一个键({@code r + r*2^-52},{@code 0} 取 {@code 2^-52}), + * 这是 {@code lcode.c: luaK_numberK} 里刻意的一步:Lua 的表用 {@code ==} + * 判键,{@code 100000} 与 {@code 100000.0} 会撞成同一个键,于是两个常量 + * 互相把对方从缓存里挤掉,每写一次都新建一份。上游因此专门给整值浮点 + * 造了个「不太可能是整数」的替代键。 + */ + static Object constKey(LuaValue v) { + if (v.isNil()) { + return NIL_KEY; + } + if (v.isBoolean()) { + return v.asBoolean(); + } + if (v.isInteger()) { + return new NumKey(true, v.i, 0); + } + if (v.isFloat()) { + double r = v.n; + Long ik = Arith.fltToInteger(r, Arith.F2I_EQ); + if (ik == null) { + return new NumKey(false, 0, r); // 非整值:用自身当键 + } + double q = Math.scalb(1.0, -MANT_DIG + 1); + return new NumKey(false, 0, (ik == 0L) ? q : r + r * q); + } + if (v.isString()) { + return v.asString(); + } + return null; + } + + /** + * {@code addk} 的查询一半:命中且校验通过才返回下标。 + *

+ * {@code k < fs->nk} 这一句不能省:缓存里的下标可能是内层函数留下的, + * 对外层的常量表来说越界了。 + */ + Integer lookupConstant(FuncState fs, LuaValue v) { + Object key = constKey(v); + if (key == null) { + return null; + } + Integer idx = constCache.get(key); + if (idx == null || idx >= fs.f.sizek) { + return null; + } + LuaValue existing = fs.f.k[idx]; + if (existing.tt != v.tt || !LuaValue.rawEqual(existing, v)) { + return null; + } + return idx; + } + + /** 追加一个常量并(覆盖)写进缓存,返回下标 */ + int addConstant(FuncState fs, LuaValue v) { + int idx = fs.f.addConstant(v); + Object key = constKey(v); + if (key != null) { + constCache.put(key, idx); + } + return idx; + } + + /* ---------- 扫描 ---------- */ + + /** {@code inclinenumber}:吃掉 {@code \n} / {@code \r} / {@code \n\r} / {@code \r\n} */ + private void inclinenumber() { + int old = current; + nextChar(); + if (currIsNewline() && current != old) { + nextChar(); + } + if (++linenumber >= Integer.MAX_VALUE) { + lexError("chunk has too many lines", 0); + } + } + + private boolean checkNext1(int c) { + if (current == c) { + nextChar(); + return true; + } + return false; + } + + private boolean checkNext2(String set) { + if (current == set.charAt(0) || current == set.charAt(1)) { + saveAndNext(); + return true; + } + return false; + } + + /** + * {@code read_numeral}。 + *

+ * 这里刻意「很宽松」,真正的格式校验交给 {@link Numeral#str2num}: + * 数字后面紧跟字母时({@code 1a}、{@code 0x})多读一个字符进来, + * 让 {@code str2num} 必然失败,从而报出上游同款 malformed number。 + */ + private int readNumeral(Token sem) { + String expo = "Ee"; + int first = current; + saveAndNext(); + if (first == '0' && checkNext2("xX")) { + expo = "Pp"; // 十六进制的指数标记是 p + } + for (;;) { + if (checkNext2(expo)) { + checkNext2("-+"); // 指数符号只在指数标记之后才合法 + } else if (LCType.isXdigit(current) || current == '.') { + saveAndNext(); + } else { + break; + } + } + if (LCType.isAlpha(current)) { + saveAndNext(); // 强制报错 + } + LuaValue obj = Numeral.str2num(bufferText()); + if (obj == null) { + lexError("malformed number", TK_FLT); + } + if (obj.isInteger()) { + sem.i = obj.i; + return TK_INT; + } + sem.r = obj.n; + return TK_FLT; + } + + /** + * {@code skip_sep}:读 {@code [=*[} 或 {@code ]=*]}。 + * + * @return '=' 个数 + 2;单个括号返回 1;{@code [==} 这种残缺返回 0 + */ + private int skipSep() { + int count = 0; + int s = current; + saveAndNext(); + while (current == '=') { + saveAndNext(); + count++; + } + return (current == s) ? count + 2 : (count == 0) ? 1 : 0; + } + + /** {@code read_long_string};{@code sem == null} 表示这是长注释 */ + private void readLongString(Token sem, int sep) { + int line = linenumber; + saveAndNext(); // 跳过第二个 '[' + if (currIsNewline()) { + inclinenumber(); // 紧跟在开头的换行被吞掉 + } + for (;;) { + if (current == Zio.EOZ) { + String what = (sem != null) ? "string" : "comment"; + lexError("unfinished long " + what + " (starting at line " + line + ")", TK_EOS); + } else if (current == ']') { + if (skipSep() == sep) { + saveAndNext(); // 跳过第二个 ']' + break; + } + } else if (current == '\n' || current == '\r') { + save('\n'); // 归一化换行 + inclinenumber(); + if (sem == null) { + resetBuffer(); // 长注释不攒内容 + } + } else { + if (sem != null) { + saveAndNext(); + } else { + nextChar(); + } + } + } + if (sem != null) { + sem.s = newString(buff, sep, buffLen - 2 * sep); + } + } + + /** {@code esccheck} */ + private void esccheck(boolean ok, String msg) { + if (!ok) { + if (current != Zio.EOZ) { + saveAndNext(); // 把出错字符放进缓冲区,好让错误信息带上它 + } + lexError(msg, TK_STRING); + } + } + + private int gethexa() { + saveAndNext(); + esccheck(LCType.isXdigit(current), "hexadecimal digit expected"); + return Numeral.hexaValue(current); + } + + private int readhexaesc() { + int r = gethexa(); + r = (r << 4) + gethexa(); + buffRemove(2); + return r; + } + + private long readutf8esc() { + long r; + int i = 4; // 要移除的字符:'\', 'u', '{', 和第一个数字 + saveAndNext(); // 跳过 'u' + esccheck(current == '{', "missing '{'"); + r = gethexa(); // 至少要有一个数字 + while (true) { + saveAndNext(); + if (!LCType.isXdigit(current)) { + break; + } + i++; + esccheck(r <= (0x7FFFFFFFL >> 4), "UTF-8 value too large"); + r = (r << 4) + Numeral.hexaValue(current); + } + esccheck(current == '}', "missing '}'"); + nextChar(); // 跳过 '}' + buffRemove(i); + return r; + } + + private void utf8esc() { + byte[] b = new byte[Numeral.UTF8BUFFSZ]; + int n = Numeral.utf8esc(readutf8esc(), b); + for (; n > 0; n--) { + save(b[Numeral.UTF8BUFFSZ - n]); + } + } + + private int readdecesc() { + int i; + int r = 0; + for (i = 0; i < 3 && LCType.isDigit(current); i++) { + r = 10 * r + current - '0'; + saveAndNext(); + } + esccheck(r <= 255, "decimal escape too large"); + buffRemove(i); + return r; + } + + /** + * {@code read_string}。 + *

+ * C 版用 {@code goto read_save / only_save / no_save} 表达「先读、只写、 + * 不写」三种收尾;Java 没有 goto,这里用一个小整数 {@code how} 表达同一件事, + * 顺序严格保持 2=先读后写、1=只写、0=不写。 + */ + private void readString(int del, Token sem) { + saveAndNext(); // 保留起始引号,错误信息里要用 + while (current != del) { + if (current == Zio.EOZ) { + lexError("unfinished string", TK_EOS); + } else if (current == '\n' || current == '\r') { + lexError("unfinished string", TK_STRING); + } else if (current == '\\') { + int c = 0; + int how = 0; + saveAndNext(); // 保留 '\\',错误信息里要用 + switch (current) { + case 'a': + c = 7; + how = 2; + break; + case 'b': + c = 8; + how = 2; + break; + case 'f': + c = 12; + how = 2; + break; + case 'n': + c = '\n'; + how = 2; + break; + case 'r': + c = '\r'; + how = 2; + break; + case 't': + c = '\t'; + how = 2; + break; + case 'v': + c = 11; + how = 2; + break; + case 'x': + c = readhexaesc(); + how = 2; + break; + case 'u': + utf8esc(); + how = 0; + break; + case '\n': + case '\r': + inclinenumber(); + c = '\n'; + how = 1; + break; + case '\\': + case '"': + case '\'': + c = current; + how = 2; + break; + case Zio.EOZ: + how = 0; // 下一轮循环会报错 + break; + case 'z': { // 吞掉后面一整段空白 + buffRemove(1); // 移除 '\\' + nextChar(); // 跳过 'z' + while (LCType.isSpace(current)) { + if (currIsNewline()) { + inclinenumber(); + } else { + nextChar(); + } + } + how = 0; + break; + } + default: + esccheck(LCType.isDigit(current), "invalid escape sequence"); + c = readdecesc(); + how = 1; + break; + } + if (how == 2) { + nextChar(); + } + if (how >= 1) { + buffRemove(1); // 移除 '\\' + save(c); + } + } else { + saveAndNext(); + } + } + saveAndNext(); // 跳过结束引号 + sem.s = newString(buff, 1, buffLen - 2); + } + + /** {@code llex} */ + private int llex(Token sem) { + resetBuffer(); + for (;;) { + switch (current) { + case '\n': + case '\r': + inclinenumber(); + break; + case ' ': + case '\f': + case '\t': + case 11: // '\v'(Java 没有 \v 字符转义) + nextChar(); + break; + case '-': { + nextChar(); + if (current != '-') { + return '-'; + } + nextChar(); // 是注释 + if (current == '[') { // 长注释? + int sep = skipSep(); + resetBuffer(); // skipSep 可能弄脏缓冲区 + if (sep >= 2) { + readLongString(null, sep); + resetBuffer(); + break; + } + } + // 短注释 + while (!currIsNewline() && current != Zio.EOZ) { + nextChar(); + } + break; + } + case '[': { + int sep = skipSep(); + if (sep >= 2) { + readLongString(sem, sep); + return TK_STRING; + } else if (sep == 0) { + lexError("invalid long string delimiter", TK_STRING); + } + return '['; + } + case '=': + nextChar(); + if (checkNext1('=')) { + return TK_EQ; + } + return '='; + case '<': + nextChar(); + if (checkNext1('=')) { + return TK_LE; + } else if (checkNext1('<')) { + return TK_SHL; + } + return '<'; + case '>': + nextChar(); + if (checkNext1('=')) { + return TK_GE; + } else if (checkNext1('>')) { + return TK_SHR; + } + return '>'; + case '/': + nextChar(); + if (checkNext1('/')) { + return TK_IDIV; + } + return '/'; + case '~': + nextChar(); + if (checkNext1('=')) { + return TK_NE; + } + return '~'; + case ':': + nextChar(); + if (checkNext1(':')) { + return TK_DBCOLON; + } + return ':'; + case '"': + case '\'': + readString(current, sem); + return TK_STRING; + case '.': + saveAndNext(); + if (checkNext1('.')) { + if (checkNext1('.')) { + return TK_DOTS; + } + return TK_CONCAT; + } else if (!LCType.isDigit(current)) { + return '.'; + } + return readNumeral(sem); + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return readNumeral(sem); + case Zio.EOZ: + return TK_EOS; + default: { + if (LCType.isAlpha(current)) { // 标识符或保留字 + do { + saveAndNext(); + } while (LCType.isAlnum(current)); + LuaString ts = newString(buff, 0, buffLen); + sem.s = ts; + Integer reserved = RESERVED.get(ts.toJavaString()); + if (reserved != null) { + return reserved; + } + return TK_NAME; + } + // 单字符 token:'+', '*', '%', '{', '}', ... + int c = current; + nextChar(); + return c; + } + } + } + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/Lua.java b/src/main/java/com/github/esrrhs/fakescript/lua/Lua.java new file mode 100644 index 0000000..0c756e0 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/Lua.java @@ -0,0 +1,159 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * 类型标签与全局常量 —— 对应 {@code lua.h} 的 {@code LUA_T*} 与 + * {@code lobject.h} 的变体标签。 + *

+ * Lua 5.4 的标签是「基础类型 + 变体」的两级结构:低 4 位是基础类型, + * 高位是变体。数字(Integer/Float)、字符串(短/长)、函数(Lua/C)都靠变体区分, + * 这一层必须保留,因为 {@code math.type(1)} 与 {@code math.type(1.0)} + * 返回不同结果,而 {@code 1 == 1.0} 又为真。 + */ +public final class Lua { + + private Lua() { + } + + /* 基础类型(低 4 位) */ + + public static final int TNONE = -1; + public static final int TNIL = 0; + public static final int TBOOLEAN = 1; + public static final int TLIGHTUSERDATA = 2; + public static final int TNUMBER = 3; + public static final int TSTRING = 4; + public static final int TTABLE = 5; + public static final int TFUNCTION = 6; + public static final int TUSERDATA = 7; + public static final int TTHREAD = 8; + + /* + * 变体:makevariant(t, v) = t | (v << 4)。 + * + * 这里写成常量表达式而不是调用 variant() 方法,是因为 switch 的 case + * 要求编译期常量,而方法调用不满足——可读性损失可以接受,注释补上算出来的值。 + */ + + public static final int VNUMINT = TNUMBER | (0 << 4); // 3:整数 + public static final int VNUMFLT = TNUMBER | (1 << 4); // 19:浮点 + + /* + * nil 与布尔的变体:这三个只有 dump / undump 用得上(二进制 chunk 的常量表 + * 按变体标签存),运行期一律用 TNIL / TBOOLEAN 加 i 载荷区分。 + */ + public static final int VNIL = TNIL | (0 << 4); // 0 + public static final int VFALSE = TBOOLEAN | (0 << 4); // 1 + public static final int VTRUE = TBOOLEAN | (1 << 4); // 17 + + public static final int VSHRSTR = TSTRING | (0 << 4); // 4:短字符串(参与驻留) + public static final int VLNGSTR = TSTRING | (1 << 4); // 20:长字符串(不驻留) + + /** + * {@code LUA_SIGNATURE}({@code "\27Lua"}})的首字节。 + *

+ * {@code ldo.c: f_parser} 就靠这一个字节决定走 {@code luaU_undump} 还是 + * {@code luaY_parser}——所以「一个 chunk 是源码还是预编译」不需要调用方 + * 声明,加载器自己看得出来。{@code load}、{@code loadfile}、 + * {@code require}、{@code dofile} 全都因此天然支持预编译 chunk。 + */ + public static final int LUA_SIGNATURE0 = 0x1B; + + /** + * {@code LUA_VERSION}:全局 {@code _VERSION} 的值。 + *

+ * 注意它不含修订号:{@code lua.h} 里 + * {@code LUA_VERSION "Lua " LUA_VERSION_MAJOR "." LUA_VERSION_MINOR}, + * 而带修订号的那个叫 {@code LUA_RELEASE}({@code "Lua 5.4.7"}),是 + * {@code lua -v} 打的东西。实测参考实现 {@code print(_VERSION)} 给 + * {@code Lua 5.4}(长度 7),不是 {@code Lua 5.4.7}。 + */ + public static final String LUA_VERSION = "Lua 5.4"; + + /** 线程 / 协程:{@code makevariant(LUA_TTHREAD, 0)} */ + public static final int VTHREAD = TTHREAD | (0 << 4); // 8 + + public static final int VLIF = TFUNCTION | (0 << 4); // 6:Lua 闭包 + public static final int VLCF = TFUNCTION | (1 << 4); // 22:轻量 C 函数 + public static final int VCCL = TFUNCTION | (2 << 4); // 38:C 闭包 + + /** + * 完整 userdata:{@code makevariant(LUA_TUSERDATA, 0)}。 + *

+ * 阶段 7 才需要它,用途是给 C 闭包绑一个宿主对象: + * {@code string.gmatch} 要在迭代之间记住「上次匹配到哪儿」, + * 而它的迭代器是 C 闭包,只能通过 upvalue 带状态。用一张表也行, + * 但那张表得暴露在 Lua 可见的地方——C 版就是 userdata,照做。 + */ + public static final int VUSERDATA = TUSERDATA | (0 << 4); // 7 + + /** 取基础类型:{@code novariant(x) = x & 0x0F} */ + public static int noVariant(int tt) { + return tt & 0x0F; + } + + /** + * 数组部分的最大规模(2 的幂)。 + *

+ * C 版是 {@code 1 << 31}(MAXABITS = 31),这里压到 {@code 1 << 20}。 + * 语义上无差别:超过这个下标的整数键一律进哈希部分,只是密集大数组会比 + * C 版多花内存与时间。取 20 是为了让数组部分最坏情况下也就是一百万个槽, + * 不至于一次分配就把堆撑爆。 + */ + public static final int MAXABITS = 20; + public static final int MAXASIZE = 1 << MAXABITS; + + /** 短字符串阈值,超过则不驻留(对应 lstring.c 的 LUAI_MAXSHORTLEN) */ + public static final int MAXSHORTLEN = 40; + + /* ==================== 调试钩子 ==================== */ + + /** {@code LUA_HOOKCALL} 等:钩子事件。顺序必须与 {@code hookf} 的名字表一致 */ + public static final int LUA_HOOKCALL = 0; + public static final int LUA_HOOKRET = 1; + public static final int LUA_HOOKLINE = 2; + public static final int LUA_HOOKCOUNT = 3; + public static final int LUA_HOOKTAILCALL = 4; + + /** {@code LUA_MASKCALL} 等:{@code sethook} 的掩码位 */ + public static final int LUA_MASKCALL = 1 << 0; + public static final int LUA_MASKRET = 1 << 1; + public static final int LUA_MASKLINE = 1 << 2; + public static final int LUA_MASKCOUNT = 1 << 3; + + /** {@code luaO_ceillog2}:不小于 log2(x) 的最小整数,x 必须 > 0 */ + public static int ceilLog2(int x) { + int l = 0; + int v = x - 1; + while (v > 0) { + v >>>= 1; + l++; + } + return l; + } + + /** 类型名,用于 {@code type()} 与错误信息 */ + public static String typeName(int tt) { + switch (noVariant(tt)) { + case TNIL: + return "nil"; + case TBOOLEAN: + return "boolean"; + case TLIGHTUSERDATA: + return "userdata"; + case TNUMBER: + return "number"; + case TSTRING: + return "string"; + case TTABLE: + return "table"; + case TFUNCTION: + return "function"; + case TUSERDATA: + return "userdata"; + case TTHREAD: + return "thread"; + default: + return "no value"; + } + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/LuaAux.java b/src/main/java/com/github/esrrhs/fakescript/lua/LuaAux.java new file mode 100644 index 0000000..44e949c --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/LuaAux.java @@ -0,0 +1,568 @@ +package com.github.esrrhs.fakescript.lua; + +import java.io.IOException; +import java.util.Arrays; + +/** + * 辅助库 —— 对应 {@code lauxlib.c} 里「库函数写起来要用的那半」。 + *

+ * 之前每个库各写一份 {@code checkInteger} / {@code "bad argument..."}, + * 拼出来的错误文本彼此不同、也都与参考实现不同。阶段 7 要一口气开六七个库, + * 再抄七遍就再也收不回来了,所以先抽出这一层。 + *

+ * 参数编号一律 1 起(与 C 的 {@code luaL_check*} 一致),因为 + * {@link LuaState#argError} 里那句「方法调用时 {@code arg--}」只有按 1 起 + * 才说得通:{@code s:rep(nil)} 报的是 {@code bad argument #1}。 + */ +public final class LuaAux { + + private LuaAux() { + } + + /* ==================== 字节缓冲区 ==================== */ + + /** + * {@code luaL_Buffer}。 + *

+ * 装的是字节不是字符。字符串库里每一处都在拼字节串 + * ({@code string.rep} 的结果、{@code gsub} 的累积、{@code format} 的输出), + * 用 Java {@code String} 或 {@code StringBuilder} 会先把字节按某种编码 + * 解释一遍——而 Lua 字符串可能根本不是合法 UTF-8。 + */ + public static final class Buf { + private byte[] data = new byte[64]; + private int len; + + private void ensure(int extra) { + if (len + extra <= data.length) { + return; + } + int cap = data.length; + while (cap < len + extra) { + cap *= 2; + } + data = Arrays.copyOf(data, cap); + } + + public void addChar(int c) { + ensure(1); + data[len++] = (byte) c; + } + + public void addBytes(byte[] b, int off, int n) { + ensure(n); + System.arraycopy(b, off, data, len, n); + len += n; + } + + public void addString(LuaString s) { + addBytes(s.bytes, 0, s.bytes.length); + } + + public void addAscii(String s) { + ensure(s.length()); + for (int i = 0; i < s.length(); i++) { + data[len++] = (byte) s.charAt(i); + } + } + + public LuaString result() { + return LuaString.of(data, 0, len); + } + } + + /** {@code luaL_addvalue}:把栈顶那个值追加进缓冲区并弹栈(值必须是字符串) */ + public static void addTop(LuaState L, Buf b) { + LuaValue v = L.stack[--L.top]; + b.addString(asString(v)); + } + + /* ==================== 取值与检查 ==================== */ + + /** {@code luaL_checklstring}:数字会转成字符串({@code string.len(42) == 2}) */ + public static LuaString checkString(LuaState L, int arg) { + LuaValue v = L.arg1(arg); + if (v.isString()) { + return v.asString(); + } + if (v.isNumber()) { + return LuaString.latin1(Vm.numberToString(v)); + } + throw L.typeArgError(arg, "string"); + } + + /** {@code luaL_optlstring}:没传或 nil 时用默认值 */ + public static LuaString optString(LuaState L, int arg, LuaString def) { + if (L.isNoneArg(arg) || L.arg1(arg).isNil()) { + return def; + } + return checkString(L, arg); + } + + /** + * {@code luaL_checkinteger}。 + *

+ * 字符串会被解释成数字({@code string.sub("abcdef","2","4")} 是成立的), + * 但解释不出整数就报「number has no integer representation」——与 + * 「number expected」是两种文本,别合并。 + *

+ * 哪种是 {@code lauxlib.c: interror} 定的,判据是 + * {@code lua_isnumber(L, arg)} 而不是 {@code lua_type(L, arg) == LUA_TNUMBER}: + * + *

+     * static void interror (lua_State *L, int arg) {
+     *   if (lua_isnumber(L, arg))
+     *     luaL_argerror(L, arg, "number has no integer representation");
+     *   else
+     *     tag_error(L, arg, LUA_TNUMBER);
+     * }
+     * 
+ * + * 而 {@code lua_isnumber} 是「是数字,或字符串能解释成数字」。所以 + * {@code string.format("%d", "3.5")} 报的是「no integer representation」 + * ({@code "3.5"} 解释得成 3.5,但 3.5 不是整数), + * {@code string.format("%d", "abc")} 报的才是「number expected, got string」。 + * 早年这里写成 {@code v.isNumber()},两条都成了后者。 + */ + public static long checkInteger(LuaState L, int arg) { + LuaValue v = L.arg1(arg); + if (L.isNoneArg(arg)) { + throw L.typeArgError(arg, "number"); + } + Long r = Vm.toInteger(v, Arith.F2I_EQ); + if (r != null) { + return r; + } + if (Vm.toNumber(v) != null) { + throw L.argError(arg, "number has no integer representation"); + } + throw L.typeArgError(arg, "number"); + } + + /** {@code luaL_optinteger} */ + public static long optInteger(LuaState L, int arg, long def) { + if (L.isNoneArg(arg) || L.arg1(arg).isNil()) { + return def; + } + return checkInteger(L, arg); + } + + /** {@code luaL_checknumber}:一律返回 double,整数也转(与 C 版一致) */ + public static double checkNumber(LuaState L, int arg) { + LuaValue v = L.arg1(arg); + if (L.isNoneArg(arg)) { + throw L.typeArgError(arg, "number"); + } + Double d = Vm.toNumber(v); + if (d != null) { + return d; + } + throw L.typeArgError(arg, "number"); + } + + /** {@code luaL_optnumber} */ + public static double optNumber(LuaState L, int arg, double def) { + if (L.isNoneArg(arg) || L.arg1(arg).isNil()) { + return def; + } + return checkNumber(L, arg); + } + + /** + * {@code luaL_checkany}:只要求「这个参数传了」,不要求类型。 + *

+ * {@code math.type} / {@code math.tointeger} 用它——两者对任何非数字都给 + * nil,唯独「一个参数也没传」要报错,报出来是 {@code (value expected)}。 + */ + public static void checkAny(LuaState L, int arg) { + if (L.isNoneArg(arg)) { + throw L.argError(arg, "value expected"); + } + } + + /** + * {@code luaL_checkoption}:在一串名字里挑,挑不到就报 + * {@code invalid option '<名字>'}。 + *

+ * {@code os.setlocale} 的第二个参数用它——就算本实现实际上不认那些 locale, + * 这条报错也得在,否则 {@code os.setlocale(nil, 'bogus')} 会静默通过。 + */ + public static int checkOption(LuaState L, int arg, String def, String[] lst) { + // C 用 strcmp 比的是 const char*:\0 之后的内容既不参与比较,也不写进报错 + String name = ((def != null) ? optString(L, arg, LuaString.latin1(def)) + : checkString(L, arg)).toCString().toJavaString(); + for (int i = 0; i < lst.length; i++) { + if (lst[i].equals(name)) { + return i; + } + } + throw L.argError(arg, "invalid option '" + name + "'"); + } + + /** + * {@code luaL_argcheck}:条件不成立就报 {@code argError(arg, msg)}。 + *

+ * 与 {@link #argExpected} 是两条不同的报错,不能混: + * {@code luaL_argexpected} 给 {@code " expected, got <实际类型>"}, + * 而 {@code luaL_argcheck} 只给裸消息。实测 + * {@code io.open("x", "bogus")} 报的是 + * {@code bad argument #2 to 'io.open' (invalid mode)}, + * 后面没有 {@code expected, got}。 + */ + public static void argCheck(LuaState L, boolean cond, int arg, String msg) { + if (!cond) { + throw L.argError(arg, msg); + } + } + + /** {@code luaL_checktype} */ + public static void checkType(LuaState L, int arg, int baseType) { + if (L.isNoneArg(arg) || L.arg1(arg).baseType() != baseType) { + throw L.typeArgError(arg, Lua.typeName(baseType)); + } + } + + /** + * {@code luaL_argexpected}:条件不成立就报「{@code tname} expected」。 + *

+ * 专供那种「接受若干种类型」的场合,比如 {@code gsub} 的第三个参数 + * 可以是 string/function/table,报出来是 + * 「string/function/table expected, got boolean」。 + */ + public static void argExpected(LuaState L, boolean cond, int arg, String tname) { + if (!cond) { + throw L.typeArgError(arg, tname); + } + } + + /** {@code luaL_checkstack} */ + /** + * {@code lauxlib.c: luaL_checkstack}。 + *

+ * 措辞是 {@code "stack overflow (%s)"}——括号里那段才是各个库自己的说明 + * ({@code too many results to unpack}、{@code string slice too long} 等)。 + * 之前这里只调 {@link LuaState#checkStack},把 {@code msg} 丢了, + * 于是大参数 {@code table.unpack} 报的是裸的 {@code stack overflow}, + * 与参考实现逐字不符。 + */ + public static void checkStack(LuaState L, int n, String msg) { + if (n > 0 && !L.checkStackOk(n)) { + throw L.libError((msg != null) ? "stack overflow (" + msg + ")" : "stack overflow"); + } + } + + /** {@code luaL_pushfail}:5.4 里「失败」就是 nil */ + public static void pushFail(LuaState L) { + L.pushNil(); + } + + /* ==================== 命名元表与 userdata ==================== */ + + /** + * {@code luaL_newmetatable}:在注册表里按名字建(或取)一张元表。 + *

+ * 名字就是 C 里那个 {@code LUA_FILEHANDLE}({@code "FILE*"})。 + * 建完被 {@code setMetatableByName} 用掉,调用方接着往里塞元方法。 + *

+ * 必须顺手设 {@code __name}——这是实测出来的: + * {@code print(#io.stdout)} 报的是 + * {@code attempt to get length of a FILE* value},那个 {@code FILE*} + * 就来自元表的 {@code __name} 字段,而 C 版正是在这里写进去的。 + */ + public static LuaTable newMetatable(LuaState L, String tname) { + LuaValue v = L.registry.get(tname); + if (v != null && !v.isNil()) { + throw L.libError("metatable '" + tname + "' already exists"); + } + LuaTable mt = new LuaTable(); + mt.set("__name", LuaValue.ofString(LuaString.latin1(tname))); + L.registry.set(tname, LuaValue.ofTable(mt)); + return mt; + } + + /** 取命名元表;没有返回 {@code null} */ + public static LuaTable getMetatableByName(LuaState L, String tname) { + LuaValue v = L.registry.get(tname); + return (v == null || !v.isTable()) ? null : v.asTable(); + } + + /** + * {@code luaL_testudata}:是 userdata 且元表正好是 {@code tname} 那张才给载荷, + * 否则给 {@code null}(不报错——{@code io.type} 靠它区分「不是文件」)。 + */ + public static Object testUdata(LuaState L, int arg, String tname) { + if (L.isNoneArg(arg)) { + return null; + } + LuaValue v = L.arg1(arg); + if (v.baseType() != Lua.TUSERDATA) { + return null; + } + LuaTable mt = ((LuaUserdata) v.o).metatable; + if (mt == null || mt != getMetatableByName(L, tname)) { + return null; + } + return ((LuaUserdata) v.o).data; + } + + /** + * {@code luaL_checkudata}:同上但失败要报错。 + *

+ * 报错文本是 {@code bad argument #1 to 'xxx' (FILEHANDLE* expected, got table)}。 + */ + public static Object checkUdata(LuaState L, int arg, String tname) { + Object p = testUdata(L, arg, tname); + if (p == null) { + throw L.typeArgError(arg, tname); + } + return p; + } + + /** + * {@code luaL_setmetatable}:把命名元表挂到栈顶那个 userdata 上。 + *

+ * 顺带跑 {@code luaC_checkfinalizer}——C 版 {@code lua_setmetatable} 里有这一步, + * 漏掉的话文件句柄的 {@code __gc} 永远不会被登记,脚本里忘了 {@code f:close()} + * 的文件就一直开着。 + */ + public static void setMetatableByName(LuaState L, String tname) { + LuaTable mt = getMetatableByName(L, tname); + LuaUserdata ud = (LuaUserdata) L.stack[L.top - 1].o; + ud.metatable = mt; + ud.markFinalizable(L); + } + + /* ==================== 文件结果三元组 ==================== */ + + /** + * {@code luaL_fileresult}:成功只给 {@code true};失败给 {@code nil, msg, errno}。 + *

+ * 失败时总是三个值——{@code io.open} 打不开文件时返回的就是 + * {@code nil, "xxx: No such file or directory", 2}。 + */ + public static void fileResult(LuaState L, boolean ok, LuaString fname, int en) { + if (ok) { + L.push(LuaValue.ofBoolean(true)); + return; + } + L.pushNil(); + String msg = strError(en); + if (fname != null) { + // 文件名可以是任意字节:消息里那一段必须原样是字节, + // 不能绕 Java String(UTF-8 解码会把 0xFF 变成 U+FFFD) + L.push(LuaValue.ofString(fname.concat(LuaString.latin1(": " + msg)))); + } else { + L.push(LuaValue.ofString(LuaString.latin1(msg))); + } + L.push(LuaValue.ofInteger(en)); + } + + /** + * {@code luaL_execresult}:{@code os.execute} 与 {@code io.popen} 的收尾。 + *

+ * 成功 {@code true, "exit", 0};非零退出 {@code nil, "exit", 码}。 + * 失败(shell 都起不来)时 {@code stat == -1},给 {@code nil, msg, errno}。 + */ + public static void execResult(LuaState L, int stat) { + if (stat == -1) { + fileResult(L, false, null, errnoOf(null)); + } else { + /* + * 失败时压的是 {@code luaL_pushfail},而 5.4 里它就是 nil + * ({@code lauxlib.h: #define luaL_pushfail(L) lua_pushnil(L)}), + * 不是 false。早先这里写的是 {@code ofBoolean(stat == 0)},于是 + * {@code io.popen("exit 3"):close()} 给的是 + * {@code false, "exit", 3} 而参考实现给 {@code nil, "exit", 3}。 + *

+ * 只有 {@code io.popen} 的收尾走这里 —— {@code os.execute} 有自己 + * 一份(那边本来就是对的,所以这个 bug 在 os 那边看不出来)。 + */ + if (stat == 0) { + L.push(LuaValue.ofBoolean(true)); + } else { + pushFail(L); + } + L.push(LuaValue.ofString(LuaString.latin1("exit"))); + L.push(LuaValue.ofInteger(stat)); + } + } + + /** + * 把 Java 的 {@code IOException} 翻译成 errno。 + *

+ * Lua 脚本里 {@code io.open} 失败后要看 {@code errno} 的数值 + * ({@code "No such file or directory" == 2}),所以不能只留消息文本。 + */ + /** + * {@link #errnoOf(IOException)} 的兜底重载:路径里带上 Java 文件系统不接受的东西时 + * ({@code InvalidPathException} 是个 {@code RuntimeException}),C 那边 + * {@code remove} / {@code rename} 拿到的是同一个被 {@code \0} 砍过的路径、 + * 报的是 {@code ENOENT}。 + */ + public static int errnoOf(Exception e) { + return (e instanceof IOException) ? errnoOf((IOException) e) : 2; // ENOENT + } + + /** + * {@code ENOTEMPTY} 的数值是操作系统定的,不是 C 库定的: + * Linux 是 {@code 39},macOS / BSD 是 {@code 66}。 + *

+ * 实测(参考二进制,本机 macOS):{@code os.remove(非空目录)} 给 + * {@code nil, "

: Directory not empty", 66} —— 之前这里写死 39, + * 消息对了、数字差 27。 + */ + private static final int ENOTEMPTY; + + static { + String os = System.getProperty("os.name", ""); + ENOTEMPTY = (os.contains("Mac") || os.contains("Darwin") || os.contains("BSD")) + ? 66 + : 39; + } + + /** + * {@code strError} 的反查表。 + *

+ * 放在这里而不是 {@link #errnoOf(IOException)} 里逐个判:errno 与 strerror + * 是一对逆函数,反查直接遍历这张表即可,不用再维护第二份 errno 清单。 + */ + private static final int[] ERRNO_BY_NAME = + { 1, 2, 13, 16, 17, 20, 21, 22, 29, ENOTEMPTY, 9 }; + + public static int errnoOf(IOException e) { + if (e instanceof java.nio.file.NoSuchFileException) { + return 2; // ENOENT + } + if (e instanceof java.nio.file.DirectoryNotEmptyException) { + return ENOTEMPTY; + } + if (e instanceof java.nio.file.FileAlreadyExistsException) { + return 17; // EEXIST + } + if (e instanceof java.nio.file.AccessDeniedException) { + return 13; // EACCES + } + /* + * 按类型判不出来时,看异常消息里那段 strerror 文本。 + *

+ * Java 的 IO 异常把 libc 的 {@code strerror} 原文带在消息里: + * {@code FileNotFoundException} 是 {@code " (Permission denied)"}, + * {@code FileSystemException} 是 {@code " -> : Not a directory"} + * 或 {@code ": Invalid argument"}({@code getReason()} 直接给)。 + * 于是 errno 可以反查出来,不用猜。 + *

+ * 不做这一步,这些形状会全部落进 {@code ENOENT} / {@code EIO} 的兜底: + * 实测(参考二进制){@code io.open("文件/y", "r")} 是 + * {@code Not a directory, 20}、{@code io.open("/etc/hosts", "w")} 是 + * {@code Permission denied, 13}、{@code os.remove(".")} 是 + * {@code Invalid argument, 22}、{@code os.remove("/")} 是 + * {@code Is a directory, 21}、{@code os.remove("/etc")} 是 + * {@code Operation not permitted, 1} —— 五个全错。 + */ + int n = errnoFromMessage(e); + if (n >= 0) { + return n; + } + if (e instanceof java.io.FileNotFoundException) { + return 2; // ENOENT + } + return 5; // EIO + } + + /** 从异常消息里那段 {@code strerror} 文本反查 errno;认不出来给 {@code -1} */ + private static int errnoFromMessage(IOException e) { + String s = (e instanceof java.nio.file.FileSystemException) + ? ((java.nio.file.FileSystemException) e).getReason() + : inParens(e.getMessage()); + if (s == null || s.isEmpty()) { + return -1; + } + for (int en : ERRNO_BY_NAME) { + if (strError(en).equals(s)) { + return en; + } + } + return -1; + } + + /** 取 {@code " (文本)"} 里括号中那一段 */ + private static String inParens(String m) { + if (m == null) { + return null; + } + int a = m.lastIndexOf('('); + int b = (a < 0) ? -1 : m.lastIndexOf(')'); + return (b > a) ? m.substring(a + 1, b) : null; + } + + /** {@code strerror}:只备了文件操作真正会碰到的那几个 errno */ + public static String strError(int en) { + switch (en) { + case 1: // EPERM + return "Operation not permitted"; + case 2: + return "No such file or directory"; + case 13: + return "Permission denied"; + /* + * EBUSY:{@code os.remove("/")} 在 macOS 上是 {@code EISDIR}(21), + * 在 Linux 上是 {@code EBUSY}(16) —— 两个平台都不一样,但两边都是 + * 从各自的 libc 推出来的,所以各自对得上。少了这一条,Linux 上就会 + * 退化成 EIO。 + */ + case 16: + return "Device or resource busy"; + case 17: + return "File exists"; + /* + * ENOTEMPTY 的两个数值都要认:Linux 39、macOS/BSD 66。 + * 只写 39 的话 macOS 上 errno 对了、消息却落进兜底的 + * "Input/output error"(实测就是这么红的:数字 66 对、消息错)。 + */ + case 39: + case 66: + return "Directory not empty"; + case 9: // EBADF + return "Bad file descriptor"; + case 22: // EINVAL + return "Invalid argument"; + case 20: // ENOTDIR + return "Not a directory"; + case 21: // EISDIR + return "Is a directory"; + case 29: // ESPIPE + return "Illegal seek"; + default: + return "Input/output error"; + } + } + + /* ==================== 小工具 ==================== */ + + /** 把一个值当成字符串读出来(数字已由调用点转换过) */ + public static LuaString asString(LuaValue v) { + return v.isString() ? v.asString() : LuaString.latin1(Vm.numberToString(v)); + } + + /** {@code luaL_tolstring}:走 {@code __tostring},再按类型格式化 */ + public static LuaString toLString(LuaState L, LuaValue v) { + return BaseLib.toLString(L, v); + } + + /** 往表里装一个函数 */ + public static void setFn(LuaTable t, String name, LuaFunction f) { + t.set(name, LuaValue.ofFunction(f)); + } + + /** + * {@code luaL_requiref} 的登记部分:库表同时进 {@code _G} 与 {@code package.loaded}。 + *

+ * 进 {@code _LOADED} 不只是为了 {@code require}:{@code luaL_argerror} + * 反查函数名时就在那张表里找,漏登记会让 + * {@code pcall(string.len)} 报出「to '?'」。 + */ + public static void registerLib(LuaState L, String name, LuaTable lib) { + L.globals.set(name, LuaValue.ofTable(lib)); + L.loadedTable().set(name, LuaValue.ofTable(lib)); + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/LuaError.java b/src/main/java/com/github/esrrhs/fakescript/lua/LuaError.java new file mode 100644 index 0000000..2817036 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/LuaError.java @@ -0,0 +1,94 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * Lua 错误 —— 携带一个 Lua 值,对应 C 版 {@code lua_error} 的 longjmp。 + *

+ * C 版用 {@code setjmp/longjmp} 做非局部跳转,{@code pcall} / {@code xpcall} / + * 协程边界各自 {@code setjmp} 一次。这里换成异常:抛出即 longjmp,捕获即 + * {@code setjmp} 命中。 + *

+ * 只有这个类型会被 {@code pcall} 捕获,这一点很关键:{@code error()} 抛出的 + * 是它,而本实现自身的 Java 异常(bug、栈溢出等)不是——后者必须继续往上冒, + * 否则会被引擎静默吞掉变成「脚本报了个错」,把实现缺陷伪装成脚本问题。 + */ +public class LuaError extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** 错误值。可以是任意 Lua 值,不一定是字符串 */ + public final LuaValue value; + + /** 是否带有位置信息(对应 {@code luaL_where} 加上的 {@code file:line:}) */ + public String where; + + /** 是否可加回溯(对应 {@code luaL_traceback})。{@code error(msg, 0)} 时不加 */ + public boolean addTraceback = true; + + /** + * 是不是 {@code luaD_throw} 直跳出来的(语法错误、{@code load} 的 + * mode 错误、二进制 chunk 的畸形)。 + *

+ * C 版有两条抛错的路,只有一条会过消息处理函数: + *

+ * 可观测的场合是 {@code xpcall} 里 {@code load(读函数)}: + * {@code luaD_protectedparser} 把 {@code L->errfunc} 原样传进内层保护区, + * 于是读函数里 {@code error("boom")} 的抛出点就被 {@code xpcall} + * 的处理器拦下了 —— 实测 {@code load} 返回 + * {@code nil, "H:case.lua:13: boom"},而语法错误返回的是裸消息。 + */ + public boolean directThrow; + + /** + * {@code LUA_ERRERR}:错误处理过程本身又出错了。 + *

+ * {@code ldo.c: luaD_seterrorobj} 对这一档不取栈顶那个错误值,而是固定写 + * {@code "error in error handling"}。看起来像句废话,但它是可观测的: + * 上游 {@code cstack.lua} 第一个用例就是 {@code xpcall(loop, loop)}—— + * 消息处理函数自己也在无限递归,最终断言 {@code msg == "error in error + * handling"}。带位置前缀就会被 {@code ==} 判否。 + */ + public boolean errErr; + + /** + * {@code LUA_ERRMEM}:内存错误,由 {@code lapi.c: lua_error} 特判出来。 + *

+ * C 版那两行看着不起眼,却是唯一一个「按错误值的内容决定错误种类」 + * 的地方: + *

+     * if (ttisshrstring(errobj) && eqshrstr(tsvalue(errobj), G(L)->memerrmsg))
+     *   luaM_error(L);      // LUA_ERRMEM:直跳
+     * else
+     *   luaG_errormsg(L);   // LUA_ERRRUN:先过 L->errfunc
+     * 
+ * 也就是错误对象是不是「{@code not enough memory} 这个驻留短串」 + * 决定了走哪条路——判的是同一个串对象(短串全局唯一),不是内容。 + * 两条路唯一的外部差别是过不过消息处理函数: + * {@code luaM_error} 不进 {@code luaG_errormsg},于是 {@code xpcall} 的 + * 处理器接不到内存错误。 + *

+ * 实测({@code H} 是处理函数): + *

+     * xpcall(function() error("not enough memory", 0) end, H)  ->  false  not enough memory
+     * xpcall(function() error("not enough memory", 1) end, H)  ->  false  H:case.lua:8: not enough memory
+     * xpcall(function() return io.open("/dev/null"):read(-1) end, H) -> false  not enough memory
+     * 
+ * 第二行带上了位置前缀,拼出来的串不再是那个驻留串,于是退化成普通 + * 错误——「位置」这个看似无关的差别把错误种类换了。 + */ + public boolean errMem; + + public LuaError(LuaValue value) { + super(value == null ? "nil" : value.toString(), null, false, false); + this.value = value; + } + + public LuaError(LuaValue value, String where) { + this(value); + this.where = where; + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/LuaFunction.java b/src/main/java/com/github/esrrhs/fakescript/lua/LuaFunction.java new file mode 100644 index 0000000..18c46a8 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/LuaFunction.java @@ -0,0 +1,24 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * C 函数 —— 对应 {@code lua.h} 的 {@code lua_CFunction}。 + *

+ * 约定与 C 版一一对应,只是把「栈」换成了显式的下标: + *

    + *
  • 调用时 {@code L.ci.func} 是函数自身的下标,参数在 + * {@code L.ci.func + 1 .. L.top - 1};
  • + *
  • 返回值必须从当前 {@code L.top} 开始连续写入,返回它们的个数;
  • + *
  • {@link LuaState} 负责按调用方期望的结果数截断或补 nil。
  • + *
+ *

+ * 抛出的 {@link LuaError} 等价于 C 版的 {@code lua_error}(longjmp), + * 会被引擎的 {@code pcall} 边界捕获;其余 Java 异常按「实现缺陷」处理,继续上冒。 + */ +public interface LuaFunction { + + /** + * @param L 当前状态。{@code L.ci} 是本次调用的帧 + * @return 返回值的个数 + */ + int call(LuaState L); +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/LuaHook.java b/src/main/java/com/github/esrrhs/fakescript/lua/LuaHook.java new file mode 100644 index 0000000..fae454c --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/LuaHook.java @@ -0,0 +1,22 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * 调试钩子 —— 对应 {@code lua.h} 的 {@code lua_Hook}。 + *

+ * C 版是一个函数指针 {@code void (*)(lua_State *L, lua_Debug *ar)},由 + * {@code luaD_hook} 在解释器的各个调用点上调用。这里用接口,语义一致。 + *

+ * 钩子是在「被跟踪的那一帧」上调用的,而不是在自己的帧上: + * {@code luaD_hook} 把 {@code L->ci}(此刻是被引发事件的函数帧)装进 + * {@code ar.i_ci},然后把钩子函数作为新的一帧压在上面。所以钩子函数里 + * {@code debug.getinfo(2)} 拿到的正是引发事件的那一帧——这是 + * {@code debug.getinfo(2, "r").ntransfer} 能读到返回值个数的全部原因。 + */ +public interface LuaHook { + + /** + * @param L 引发事件的线程 + * @param ar 事件信息;{@code ar.event} 是 {@link Lua#LUA_HOOKCALL} 等 + */ + void hook(LuaState L, DebugLib.LuaDebug ar); +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/LuaKFunction.java b/src/main/java/com/github/esrrhs/fakescript/lua/LuaKFunction.java new file mode 100644 index 0000000..a29648f --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/LuaKFunction.java @@ -0,0 +1,23 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * C 函数在 {@code yield} 之后的续体 —— 对应 {@code lua.h} 的 + * {@code lua_KFunction}。 + *

+ * Lua 5.4 里一个 C 函数想在自己调用 Lua 代码的过程中允许 {@code yield}, + * 就必须提供续体:{@code lua_callk} / {@code lua_pcallk} 在 yield 发生时把 + * 控制权交回 {@code lua_resume},等协程被再次 resume 时,不是回到 C + * 函数里被打断的那一行(C 的栈帧早没了),而是调用这个续体把剩下的活干完。 + *

+ * 这里的情况完全一样:我们的 {@code pcall} / {@code xpcall} 是 Java 方法, + * {@code yield} 用异常抛出后 Java 栈帧同样没了,所以只能把「收尾」写成续体。 + * + * @param L 发生续体的线程 + * @param status {@code LUA_OK} / {@code LUA_YIELD} / 错误码 + * @param ctx 注册续体时存下的上下文 + * @return 这个函数真正返回的结果个数(会被 {@code luaD_poscall} 搬到位) + */ +public interface LuaKFunction { + + int call(LuaState L, int status, int ctx); +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/LuaLibs.java b/src/main/java/com/github/esrrhs/fakescript/lua/LuaLibs.java new file mode 100644 index 0000000..881d117 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/LuaLibs.java @@ -0,0 +1,34 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * 标准库的入口 —— 对应 {@code linit.c} 的 {@code luaL_openlibs}。 + *

+ * 之前这份「依次开十个库」的代码是写在测试驱动里的({@code Diff}), + * 于是引擎侧一直没有 {@code luaL_openlibs} 的对应物:想跑一段脚本的宿主得自己 + * 记住哪十个库、按什么顺序开。顺序不是小事——{@code PackageLib} 要往 + * {@code package.loaded} 里登记,而 {@code lauxlib.c: pushglobalfuncname} 又靠 + * {@code package.loaded} 反查 C 函数的名字,开漏一个,{@code pcall(string.len)} + * 的报错就从 {@code 'string.len'} 退化成 {@code 'len'}。 + *

+ * 开库顺序照 {@code linit.c} 的 {@code loadedlibs}:base、package 先于其余, + * 因为后面那些库要靠 {@code require} 的登记机制。 + */ +public final class LuaLibs { + + private LuaLibs() { + } + + /** {@code luaL_openlibs} */ + public static void openAll(LuaState L) { + BaseLib.open(L); + PackageLib.open(L); + CoroutineLib.open(L); + StringLib.open(L); + Utf8Lib.open(L); + TableLib.open(L); + MathLib.open(L); + OsLib.open(L); + IoLib.open(L); + DebugLib.open(L); + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/LuaState.java b/src/main/java/com/github/esrrhs/fakescript/lua/LuaState.java new file mode 100644 index 0000000..0caad0f --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/LuaState.java @@ -0,0 +1,3464 @@ +package com.github.esrrhs.fakescript.lua; + +import java.io.ByteArrayOutputStream; +import java.lang.ref.WeakReference; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Lua 状态 —— 对应 {@code lstate.h} 的 {@code lua_State} 与 {@code ldo.c} 的 + * 调用机制。 + *

+ * 核心不变式:栈槽对象一旦创建就不再替换。C 版开放 upvalue 保存的是栈槽的 + * 地址;这里保存的是槽对象的引用。两者等价的前提是栈扩容只复制数组、不动槽对象。 + * 因此全类内部一律 {@code stack[i].set(v)},从不写 {@code stack[i] = v}, + * 新槽只在 {@link #ensureSlots(int)} 里 {@code new} 出来。 + *

+ * 几处与 C 版的差异(都是刻意的): + *

    + *
  • 指针换成栈下标。{@code StkId} 在 C 里能加减,Java 里只能这样;
  • + *
  • 没有 {@code EXTRA_STACK} 保证金:{@code luaD_call} 前需要栈空间的地方 + * 显式 {@code ensureSlots};
  • + *
  • {@code luaD_throw} 换成抛 {@link LuaError},{@code pcall} 用 + * {@code try/catch} 实现;
  • + *
  • 没有「非 Lua 线程」:协程只是另一个 {@code LuaState},靠 + * {@link #xmove} 在两条栈之间搬值,不需要任何线程/纤程去模拟第二条 + * 执行流;yield 用抛 {@link LuaYield} 实现。
  • + *
+ */ +public final class LuaState { + + /** {@code LUA_MULTRET}:调用方要全部结果 */ + public static final int MULTRET = -1; + + /** {@code LUA_MINSTACK}:C 函数帧保证的可用槽数 */ + public static final int MINSTACK = 20; + + /** {@code LUAI_MAXCCALLS}:C 调用深度上限,超过报「C stack overflow」 */ + public static final int MAXCCALLS = 200; + + /** + * {@code LUAI_MAXSTACK}:Lua 栈槽上限,超过报「stack overflow」。 + *

+ * 这个数不是安全阀的取值问题,而是语义:实测参考实现在 1,000,000 + * 槽处溢出({@code luaconf.h} 里 {@code LUAI_IS32INT} 为真时取这个分支)。 + * 没有它,无限递归在 Java 这边的表现是 {@code OutOfMemoryError} 而不是 + * 一句 Lua 错误——上游 {@code cstack.lua} 整篇都在断言这件事。 + */ + public static final int MAXSTACK = 1000000; + + /** {@code ERRORSTACKSIZE}:栈溢出之后留给错误处理用的额外空间 */ + private static final int ERRORSTACKSIZE = MAXSTACK + 200; + + /** {@code ldo.c: luaD_seterrorobj} 对 {@code LUA_ERRERR} 用的固定串 */ + private static final String ERRERR_MSG = "error in error handling"; + + /** 初始栈容量 */ + private static final int BASIC_STACK_SIZE = 2 * MINSTACK; + + /* ==================== 栈 ==================== */ + + LuaValue[] stack = new LuaValue[BASIC_STACK_SIZE]; + int top; + + /* ==================== 调用 ==================== */ + + CallInfo ci; + private final CallInfo baseCi = new CallInfo(); + int nCcalls; + + /** + * 是否处在「栈溢出之后、正在处理这个错误」的状态。 + *

+ * C 版把它编码成 {@code stacksize(L) == ERRORSTACKSIZE}:一旦溢出就把栈扩到 + * {@code MAXSTACK + 200},之后任何再增长的请求都不再报 + * {@code stack overflow},而是直接 {@code LUA_ERRERR}——那说明连错误处理 + * 自己也把栈用完了,再给一句新消息只会再溢出一次。 + */ + private boolean stackErr; + + + /** + * 当前安装的消息处理函数({@code lstate.c: L->errfunc})。 + *

+ * C 版由 {@code luaD_pcall} 在保护区期间赋值、出来时还原,而调用它的地方 + * 是 {@code luaG_errormsg}(抛出点),不是 {@code luaD_pcall}(捕获点)。 + * 这个差别只在嵌套的保护区上显形:{@code luaD_protectedparser} + * ({@code load} / {@code loadfile} / {@code require})把 {@code L->errfunc} + * 原样传进内层,于是读函数里 {@code error()} 的那一刻就被外层 + * {@code xpcall} 的处理器拦下了 —— {@code load} 于是返回 + * {@code nil, "H:..."},而语法错误走 {@code luaD_throw} 直跳、不带痕迹。 + *

+ * 协程不继承:新线程的 {@code L->errfunc} 是 0({@code preinit_thread} + * 不拷这一项),实测 {@code xpcall} 里 {@code coroutine.resume} 一个 + * {@code load(读函数)} 出错的协程,消息不带 {@code H}。 + */ + LuaValue errFunc; + + /** + * 不可 yield 的调用层数 —— C 版是 {@code nCcalls} 的高 16 位 + * ({@code nyci = 0x10000 | 1},{@code yieldable(L)} 判高位是否为 0)。 + * 这里拆成独立字段:{@code nCcalls} 只管深度,这里只管能不能 yield。 + */ + private int nNy; + + /** + * 关 tbc 变量时被 yield 打断,记下「当时正要往上抛的那个错误对象」。 + *

+ * C 版不需要这个字段:错误对象一直躺在栈上({@code luaD_seterrorobj} 每次 + * 关一个变量都会把它重新写到 {@code tbc + 1}),{@code finishpcallk} 从头 + * {@code L->top - 1} 取即可。本实现的错误对象住在异常里,yield 一抛就跟着 + * Java 栈一起没了,只能在这里暂存,等 {@link #finishPcallK} 来取。 + */ + LuaValue closeErrPending; + + /* ==================== 调试钩子 ==================== */ + + /** + * {@code L->hook}:当前钩子。{@code null} 表示没开。 + *

+ * 只有 {@code debug.sethook} 会写它,而它只被 {@code DebugLib.dHook} 读。 + */ + LuaHook hook; + + /** {@code L->hookmask}:{@link Lua#LUA_MASKCALL} 等的按位或 */ + int hookmask; + + /** {@code L->hookcount}:还有几条指令触发计数钩子 */ + int hookcount; + + /** {@code L->basehookcount}:{@code sethook} 传入的计数周期 */ + int baseHookCount; + + /** + * {@code L->allowhook}:钩子内不再触发钩子。 + *

+ * C 版在 {@code luaD_hook} 里先置 0 再调钩子,回来再置 1。缺了它, + * 钩子函数里执行的每一条 Lua 指令都会递归触发同一个钩子, + * 一瞬间就把 C 栈(这里是 Java 栈)撑爆。 + */ + boolean allowHook = true; + + /** + * {@code L->oldpc}:上一次触发行钩子的那条指令。 + *

+ * 行钩子的判据是「{@code pc} 往回跳了」或「中途换过行」,两者都要拿 + * 上一次的位置比。回到上层函数时由 {@code rethook} 更新。 + */ + int oldpc; + + /* ==================== 线程状态 ==================== */ + + /** {@code LUA_OK}:没在跑,或者跑完了 */ + public static final int LUA_OK = 0; + + /** {@code LUA_YIELD}:挂起了 */ + public static final int LUA_YIELD = 1; + + /** {@code LUA_ERRRUN}:运行期错误(本实现目前只区分这三种状态) */ + public static final int LUA_ERRRUN = 2; + + /** {@code lua_State.status} */ + int status = LUA_OK; + + /** {@code lua.h: LUA_GCGEN} —— 分代模式 */ + public static final int LUA_GCGEN = 10; + + /** {@code lua.h: LUA_GCINC} —— 增量模式(5.4 的默认) */ + public static final int LUA_GCINC = 11; + + /** + * {@code global_State.gcstp}:收集器是否在运行。 + *

+ * 只为 {@code collectgarbage("stop"/"restart"/"isrunning")} 三者自洽而存在, + * 本实现不真的收集。 + */ + boolean gcRunning = true; + + /** + * {@code global_State.gckind}:{@link #LUA_GCGEN} 或 {@link #LUA_GCINC}。 + *

+ * {@code collectgarbage("generational"/"incremental")} 返回的是切换前 + * 的模式,所以要记住当前是哪一个。 + */ + int gcKind = LUA_GCINC; + + /** + * {@code global_State.lastatomic != 0}。 + *

+ * 判据 {@code isdecGCmodegen(g)} 是 {@code gckind == KGC_GEN || + * lastatomic != 0},不是单看 {@code gckind}。而 {@code lastatomic} + * 在 头一次真正收集之后 就非 0 —— 脚本加载本身就要分配,所以 + * 用户代码刚跑起来时它已经是 0 以外的值。实测参考实现: + *

+     * 1 generational   -- 第一次 "incremental":看着像分代
+     * 2 incremental    -- 切过一次之后 lastatomic 被清 0
+     * 3 incremental    -- "generational"(旧模式是增量)
+     * 4 generational    -- "incremental"(旧模式是分代)
+     * 5 incremental
+     * 
+ * 只记 {@code gckind} 的话第一行就假。 + */ + boolean gcLastAtomic = true; + + /** {@code global_State.gcpause} / {@code gcmul}:只用于让 set* 有值可报 */ + int gcPause = 200; + + /** @see #gcPause */ + int gcStepMul = 100; + + /** + * 主线程,对应 {@code global_State.mainthread}。 + *

+ * 唯一用途是区分「跨 C 调用边界 yield」和「在协程外 yield」两条报错—— + * 两条消息的措辞不同,得知道自己在不在主线程上。 + */ + final LuaState mainThread; + + /* ==================== 全局 ==================== */ + + public final StringPool strings; + public LuaTable globals; + public final LuaTable registry; + + /** {@code lauxlib.h: LUA_LOADED_TABLE} —— 注册表里挂 {@code package.loaded} 的那个键 */ + private static final String LOADED_KEY = "_LOADED"; + + /** + * 注册表里的 {@code _LOADED} 表,也就是 {@code package.loaded}。 + *

+ * 除了阶段 8 的 {@code require} 要用它,这里立刻就要用它: + * {@code lauxlib.c: pushglobalfuncname} 靠它反查「这个 C 函数在哪个库里」, + * 于是 {@code pcall(string.len)} 报的是 + * 「bad argument #1 to 'string.len'」而不是「… to 'len'」。少的不是里子 + * 而是面子——但真实项目的日志与断言里全是这种字符串。 + *

+ * 与 {@link #registry} / {@link #globals} 一样必须与主线程共享: + * C 版 {@code _LOADED} 挂在注册表上,而注册表是 {@code global_State} + * 级别的,所有 {@code lua_State} 共用一份。早先这里是每个 state 一份, + * 于是协程里 {@code pcall(math.random, 5, 1)} 反查不到名字,报成 + * 「bad argument #1 to '?'」——参考实现给的是 'math.random'。 + */ + private final LuaTable loaded; + + /** + * 每个基础类型的默认元表,对应 {@code global_State.mt[]}。 + *

+ * 只有 {@code debug.setmetatable(nil, mt)} 这类「给整个类型设元表」的调用 + * 才会用到;表与 userdata 的元表存在对象自身。 + */ + private final LuaTable[] typeMt; + + /** 元方法名常量表,对应 {@code global_State.tmname[]} */ + final LuaString[] tmName; + + /** 开放 upvalue 列表,按 {@link UpVal#stackIdx} 升序 */ + private final List openUpvals = new ArrayList<>(); + + /** + * 当前 chunk 的名字,仅用于错误信息。 + *

+ * 存字节而不是 {@code String}:源名是 Lua 字符串,而 + * {@code luaO_chunkid} 的截断是按字节算的(见 {@link #chunkId})。 + */ + private byte[] chunkName = { '?' }; + + /** + * 标准输出。{@code print} 写这里,测试把它换成内存缓冲区即可做差分比对。 + *

+ * {@code io.stdout} 默认也写这里(不是 {@code System.out})——否则 + * {@code io.write} 的输出接不到测试装的那条缓冲区上。 + */ + public java.io.PrintStream out; + + /** + * 标准输入。{@code io.stdin} 读这里。 + *

+ * 默认是一条空流而不是 {@code System.in}:差分测试里参考实现的 stdin + * 被定向到 {@code /dev/null},两边必须一致,否则 {@code io.read()} 一边 + * 返回 nil 另一边阻塞在终端上。 + */ + public java.io.InputStream in = new java.io.ByteArrayInputStream(new byte[0]); + + /** + * 标准错误。{@code io.stderr} 写这里。 + *

+ * 单独留一个字段而不是直接用 {@code System.err}:差分测试里参考实现的 + * stdout 与 stderr 是合并捕获的,这边也得能把两条流指到同一个缓冲上。 + */ + public java.io.PrintStream err = System.err; + + /* ==================== 警告系统 ==================== */ + + /** + * 警告系统的三个状态,对应 {@code lauxlib.c} 的 warnfoff / warnfon / + * warnfcont —— C 版是「当前装着哪个函数指针」,这里用枚举值。 + */ + private static final int WARN_OFF = 0; + private static final int WARN_ON = 1; + private static final int WARN_CONT = 2; + + /** + * 当前警告状态,对应 {@code global_State.warnf}。 + *

+ * 默认关闭:{@code luaL_newstate} 里装的是 {@code warnfoff}, + * {@code lua.c} 只在看到 {@code -W} 时才 {@code lua_warning(L, "@on", 0)}。 + * 实测 {@code lua -} 直接跑脚本时 {@code warn("x")} 什么都不打。 + *

+ * 整个 Lua 状态共享一份(协程里 {@code warn("@on")} 之后主线程也该开着)。 + * 基本类型字段没法像 {@link #strings} 那样靠构造时赋同一个引用来共享, + * 所以走 {@link #mainThread} 转一道。 + */ + private int warnState = WARN_OFF; + + /** {@code lauxlib.c: warnfon} 打的消息前缀 */ + private static final byte[] WARN_PREFIX = + { 'L', 'u', 'a', ' ', 'w', 'a', 'r', 'n', 'i', 'n', 'g', ':', ' ' }; + + /** + * {@code lapi.c: lua_warning} + {@code lauxlib.c} 那三个 warnf*。 + *

+ * 形状照 C 抄,三条容易漏的规则: + *

    + *
  • 控制消息只在 {@code tocont == 0} 时认: + * {@code checkcontrol} 第一句就是 {@code if (tocont || *message++ != '@')}。 + * 所以 {@code warn("@on", "x")} 里的 {@code "@on"} 被原样打印 + * (实测参考实现输出 {@code Lua warning: @onsecond}),不是打开警告。
  • + *
  • 未知控制消息被吃掉:{@code checkcontrol} 认出 {@code '@'} 之后 + * 无论匹不匹配都返回 1,{@code warn("@junk")} 既不打印也不报错。
  • + *
  • 只有 OFF / ON 两个状态会去查控制消息,CONT 不会。 + * {@code warnfcont} 直接写,不调 {@code checkcontrol},所以 + * {@code warn("a", "@on")} 打出来的是 {@code a@on} 而不是打开警告。 + * 这一条光看 {@code warn("@on")} 那一行永远发现不了。
  • + *
+ *

+ * 参数是 {@link LuaString} 而不是 {@code String}:{@code lua_writestringerror} + * 写的是字节,{@code print(String)} 会按 UTF-8 编码,而 Lua 字符串可以是 + * 非法 UTF-8({@code warn("\xff")} 得原样输出 {@code 0xFF})。 + * + * @param msg 消息本体,这里自己按 C 字符串语义截断({@code const char*}) + * @param tocont 后面还有消息片段 + */ + public void warning(LuaString msg, boolean tocont) { + if (mainThread != this) { + mainThread.warning(msg, tocont); + return; + } + LuaString s = msg.toCString(); + switch (warnState) { + case WARN_OFF: + // warnfoff:只查控制消息,其余一律丢弃 + checkControl(s, tocont); + return; + case WARN_CONT: + break; // warnfcont:不查控制消息,直接接着写 + default: + // warnfon:控制消息被吃掉,其余打前缀 + if (checkControl(s, tocont)) { + return; + } + err.write(WARN_PREFIX, 0, WARN_PREFIX.length); + break; + } + byte[] b = s.bytes; + err.write(b, 0, b.length); + if (tocont) { + warnState = WARN_CONT; + } else { + err.write('\n'); // 消息收尾 + warnState = WARN_ON; + } + /* + * 冲一下:C 的 stderr 是无缓冲的,{@code lua.c} 的 stdout 又是每次换行的 + * {@code fflush}({@code lua_writeline}),所以警告与 print 的交错顺序 + * 与脚本里的先后一致。两边共用一条 PrintStream 时不冲就会错位。 + */ + err.flush(); + } + + /** + * {@code lauxlib.c: checkcontrol}:是不是 {@code @on} / {@code @off}。 + * + * @return 是控制消息(已被吃掉,不该打印) + */ + private boolean checkControl(LuaString s, boolean tocont) { + byte[] b = s.bytes; + if (tocont || b.length == 0 || b[0] != '@') { + return false; + } + if (asciiEq(b, 1, "off")) { + warnState = WARN_OFF; + } else if (asciiEq(b, 1, "on")) { + warnState = WARN_ON; + } + return true; // 未知的 '@' 消息也算控制消息:吃掉,不打印 + } + + /** {@code b} 从 {@code off} 起是否正好等于 {@code ascii}(且到此为止) */ + private static boolean asciiEq(byte[] b, int off, String ascii) { + if (b.length - off != ascii.length()) { + return false; + } + for (int i = 0; i < ascii.length(); i++) { + if (b[off + i] != (byte) ascii.charAt(i)) { + return false; + } + } + return true; + } + + public LuaState() { + this(null); + } + + /** + * 建一个新线程(协程)。 + *

+ * 与主线程共享的只有「全局」那部分:字符串池、全局表、注册表、类型元表、 + * 标准输出。栈、调用帧、开放 upvalue 都是每线程一份——这恰恰就是协程能 + * 各自挂起的全部原因,也就不需要任何线程、纤程去模拟「另一条执行流」。 + *

+ * 第 0 号栈槽是保留的。{@code base_ci.func} 指向它(见 + * {@code lstate.c: stack_init} 里「先占一格给函数」),协程主体函数落在 + * 下标 1。{@code lua_resume} 那句 + * {@code nresults = top - (ci->func + 1)} 只有在这个布局下才成立: + * 协程跑完、结果搬回下标 1 之后,top 减去 1 正好是结果个数。 + */ + private LuaState(LuaState main) { + mainThread = (main == null) ? this : main; + /* + * 主线程永远不可 yield——{@code lstate.c: lua_newstate} 里那句 + * {@code incnny(L); /* main thread is always non yieldable * /}。 + * 实测:主协程里 {@code coroutine.isyieldable()} 是 false,即使在 + * 最外层、一个 C 调用都没有的时候。协程起自 0,每次 resume 也重置为 0 + * (C 版 {@code L->nCcalls = getCcalls(from)} 只拷低 16 位,高位清零)。 + */ + nNy = (main == null) ? 1 : 0; + if (main == null) { + strings = new StringPool(); + globals = new LuaTable(); + registry = new LuaTable(); + loaded = new LuaTable(); + typeMt = new LuaTable[Lua.TTHREAD + 1]; + out = System.out; + } else { + strings = main.strings; + globals = main.globals; + registry = main.registry; + loaded = main.loaded; + typeMt = main.typeMt; + out = main.out; + } + tmName = new LuaString[Tm.TM_N]; + for (int i = 0; i < stack.length; i++) { + stack[i] = new LuaValue(); + } + baseCi.func = 0; + baseCi.top = MINSTACK; + baseCi.nresults = 0; + baseCi.isLua = false; + ci = baseCi; + /* + * {@code lstate.c: stack_init}:先给 {@code base_ci} 的「函数」占一格 + * (置 nil),再把 {@code top} 抬过它。 + *

+ * 主线程也要这么做,而不能停在 0:C 版主线程开库之前 + * {@code top} 就是 {@code stack + 1},{@code lua.c: main} 里 + * {@code pmain} 因此落在下标 1。差一格,所有按 {@code top} 递推出来的 + * 递归深度就整体差一。 + */ + if (main == null) { + stack[0].setNil(); + top = 1; + } + for (int i = 0; i < Tm.TM_N; i++) { + tmName[i] = LuaString.latin1(Tm.EVENT_NAMES[i]); + } + if (main == null) { + registry.set(2, LuaValue.ofTable(globals)); // LUA_RIDX_GLOBALS + globals.set("_G", LuaValue.ofTable(globals)); + /* + * 上游的 _LOADED 里也挂着 _G 本身,所以全局函数(print 等)能被 + * findfield 反查到名字(查出来是 "_G.print",再把前缀去掉)。 + * 不挂的话 pcall(print, ...) 的报错里函数名会变成 "?"。 + */ + loaded.set("_G", LuaValue.ofTable(globals)); + /* + * {@code LUA_LOADED_TABLE} 是注册表里的一个字段 + * ({@code lauxlib.h: #define LUA_LOADED_TABLE "_LOADED"}), + * 不是别处另存一份:{@code debug.getregistry()._LOADED} 必须拿得到 + * {@code package.loaded}。之前只在字段里存,于是 + * {@code type(debug.getregistry()["_LOADED"])} 给 nil 而参考实现 + * 给 table —— 靠注册表做手脚的脚本(老代码里不少)会直接失效。 + */ + registry.set(LOADED_KEY, LuaValue.ofTable(loaded)); + } + } + + /** + * {@code package.loaded}(注册表里的 {@code _LOADED})。 + *

+ * 开库时应当顺手把库表登记进来(等价于 {@code luaL_requiref} 的 + * {@code LOADED[modname] = module})——这既让 {@code require} 能用, + * 也让库函数的报错带上 {@code 'string.len'} 这样的全名。 + */ + public LuaTable loadedTable() { + /* + * 从注册表里取,而不是直接用 {@link #loaded} 字段 —— 脚本可以把 + * {@code debug.getregistry()._LOADED} 换成别的表,那时参考实现用的 + * 就是换过的那份。取不到表时(被置成 nil / 数字)退回内部那份: + * C 会在 {@code lua_getfield} 上抛 Lua 错误,这里继续用旧表只是 + * 「没那么严格」,但换成 NPE 是不行的(实现自身的 Java 异常 + * 不能被 pcall 吞成脚本报错)。 + */ + LuaValue v = registry.get(LOADED_KEY); + return (v != null && v.isTable()) ? v.asTable() : loaded; + } + + /** + * 主线程上登记的所有线程(含它自己),弱引用。 + *

+ * 唯一用途是 {@link #clearDeadStackSlices}:C 版在 GC 的 atomic 阶段会遍历 + * 每一个线程,把 {@code [top, stack_last)} 清掉;这里要清就得找得到 + * 所有协程。用弱引用是因为协程本身是 Lua 值,它死活由脚本决定,不该被这份 + * 登记表拖住。 + */ + private final List> threads = new ArrayList<>(); + + /** {@code lua_newthread} */ + public LuaState newThread() { + LuaState nl = new LuaState(mainThread); + nl.top = 1; // 第 0 格留给 base_ci.func,主体函数从下标 1 开始 + mainThread.threads.add(new java.lang.ref.WeakReference<>(nl)); + return nl; + } + + /** + * {@code lgc.c: traversethread} 的 atomic 分支:把所有栈 {@code [top, ...)} + * 那一段置 nil。 + *

+ * 为什么是「收集时才清」而不是「每次 {@code top} 下降就清」——这是照着 C 版 + * 来的,也是踩出来的:C 只在 GC 的 atomic 阶段清死区,中间那些 {@code top} + * 变动一律不清。急切清会撞上两处,而且都很难看出来: + *

    + *
  • {@code luaT_callTMres} 写完结果才弹栈,而目标寄存器可能就是弹出 + * 的那一格;清早一步就把刚写好的结果抹了({@code __lt} / {@code __eq} + * 全军覆没)。
  • + *
  • {@code LuaError.value} 直接别名了栈槽({@code error('boom', 0)} 里 + * {@code L.arg(0)} 就是活槽)。{@code pcall} 的收尾会把 {@code top} + * 降回去,一清就把错误对象清成 nil({@code pcall} 变成 {@code false nil})。
  • + *
+ * 反过来,等到收集时再清是精确的:此刻 {@code top} 之上的东西按定义 + * 就是死的,C 的 mark 阶段也只看得到 {@code [stack, top)}。 + */ + void clearDeadStackSlices() { + clearDeadSlice(this); + for (WeakReference r : mainThread.threads) { + LuaState t = r.get(); + if (t != null) { + clearDeadSlice(t); + } + } + } + + private static void clearDeadSlice(LuaState t) { + for (int i = t.top; i < t.stack.length; i++) { + t.stack[i].setNil(); + } + } + + /* ==================== __gc ==================== */ + + /** + * 所有挂过 {@code __gc} 的表,弱引用。 + *

+ * 只用于 {@link #close}:C 版 {@code lua_close} 会不管还活不活把清单上 + * 的终结器全跑一遍({@code callallpendingfinalizers}),所以退出时不能只看 + * 「已经不可达的那些」。 + */ + private final List> finalizables = new ArrayList<>(); + + /** + * 已经不可达、等着跑 {@code __gc} 的表。 + *

+ * 由 {@link GcSentinel#finalize} 从 JVM 终结线程填进来,只能在 Lua 线程 + * 上排空,所以下面的存取都要锁。 + */ + private final List pendingFinalizers = new ArrayList<>(); + + void registerFinalizable(Object t) { + synchronized (finalizables) { + finalizables.add(new WeakReference<>(t)); + } + } + + /** {@link GcSentinel#finalize} 用:把表拽回可达集合并排队 */ + void enqueueFinalizer(Object t) { + synchronized (pendingFinalizers) { + pendingFinalizers.add(t); + } + gcPending = true; + } + + /** + * 排空待终结队列,在 {@code L} 上跑它们的 {@code __gc}({@code lgc.c: GCTM})。 + *

+ * 后进先出:实测参考实现里三个对象的 {@code __gc} 顺序是 {@code 3,2,1}, + * 也就是创建顺序的逆序。Lua 没规定顺序,但差分测的是逐字节一致, + * 照它来最省事。 + */ + void drainFinalizers(LuaState L) { + while (true) { + Object t; + synchronized (pendingFinalizers) { + if (pendingFinalizers.isEmpty()) { + return; + } + t = pendingFinalizers.remove(pendingFinalizers.size() - 1); + } + runGc(L, t); + } + } + + /* ==================== 自动收集:luaC_checkGC 的等价物 ==================== */ + + /* + * 没有收集器,就得回答「什么时候去戳一下 JVM」。两个判据: + * + * 1. 分配债。C 版每 new 一个可回收对象就 {@code luaC_checkGC}, + * 债满了推进一轮收集;这里照抄,债满就 {@code System.gc()}。 + * 2. 待终结队列非空。JVM 自己也会回收,哨兵的 {@code finalize} + * 可能在没人请求的时候就把对象排进队——那时不用等债满,直接排空。 + * + * 计数做成静态的:{@link LuaTable#LuaTable()} 这类构造器拿不到 {@code L}, + * 而它只决定「什么时候戳一下」,多个 {@code LuaState} 共享也不影响正确性。 + */ + private static volatile long gcDebt; + + /** 下一次强制收集要攒到多少债。每次收集后翻倍,避免分配密集的程序被拖死 */ + private static volatile long gcThreshold = 1024; + + /** 翻倍的上限:{@code 1 << 24}。再大就基本等于「只靠 JVM 自己回收」了 */ + private static final long GC_THRESHOLD_MAX = 1L << 24; + + /** JVM 已经把对象排进队、等着在 Lua 线程上跑 {@code __gc} */ + private volatile boolean gcPending; + + /** 排空中。{@code __gc} 里也会建表,不挡住就会重进 */ + private boolean inGc; + + /** 每 new 一个可回收对象收一次债 */ + static void chargeGc() { + gcDebt++; + } + + /** 该不该看一眼收集器。放在主循环里每条指令都问一次,所以要够便宜 */ + boolean gcDue() { + return gcDebt >= gcThreshold || mainThread.gcPending; + } + + /** + * 只在指令边界上调:先要一次 GC + 终结(债满时),再把排上队的 + * {@code __gc} 跑完({@code lgc.c: GCTM})。 + *

+ * 为什么必须挑指令边界:{@link #drainFinalizers} 会在当前栈上跑 Lua + * 代码,而 {@link Vm#execute} 的 {@code ci / base / pc / k} 都是局部量。 + * 好在这些量在指令边界上都是自洽的——栈槽对象不会被替换(只有 + * {@link #ensureSlots} 会换数组,而它保持下标),{@code ci} 也不会被 + * 嵌套调用换掉。 + */ + void maybeCollect(LuaState L) { + LuaState m = mainThread; + if (m.inGc) { + return; + } + if (!m.gcPending) { + if (gcDebt < gcThreshold) { + return; + } + gcDebt = 0; + if (gcThreshold < GC_THRESHOLD_MAX) { + gcThreshold <<= 1; + } + /* + * 与 collectgarbage 同一件事:清掉各栈 {@code top} 之上的死区。 + * 少了这一步,凡是被调函数参数槽「顺便留住」的对象都收不掉—— + * 实测 {@code setmetatable({}, {__gc=f})} 里那个 {@code {}} 就 + * 卡在 {@code setmetatable} 帧的参数槽里,一直活到那个槽被别的 + * 调用覆盖为止,于是 {@code repeat until name} 永远出不来。 + */ + L.clearDeadStackSlices(); + System.gc(); + /* + * {@code gc()} 返回时终结线程未必已经跑完,不催这一下的话哨兵 + * 还没入队我们就往下走了——那样一次 {@code System.gc()} 换不来 + * 任何 {@code __gc},外层的 {@code repeat until} 就永远出不来。 + */ + System.runFinalization(); + } + m.inGc = true; + try { + m.drainFinalizers(L); + } finally { + m.inGc = false; + } + synchronized (m.pendingFinalizers) { + if (m.pendingFinalizers.isEmpty()) { + m.gcPending = false; + } + } + } + + /** + * 对一个「可终结对象」跑 {@code __gc}。 + *

+ * 表与 userdata 都可能挂 {@code __gc}({@code io} 库的文件句柄就是后者: + * 不跑它的 {@code __gc},脚本里忘了 {@code f:close()} 的文件就永远不关, + * 描述符一路泄漏到进程退出)。两套对象的收尾动作不同,所以在这里分派。 + */ + private static void runGc(LuaState L, Object o) { + if (o instanceof LuaTable) { + ((LuaTable) o).runGcMetamethod(L); + } else if (o instanceof LuaUserdata) { + ((LuaUserdata) o).runGcMetamethod(L); + } + } + + /** + * {@code lua_close}:把还没跑过的 {@code __gc} 全跑一遍。 + *

+ * 实测参考实现:脚本退出时不调 {@code collectgarbage},{@code __gc} 照样会跑。 + * 少了这一步,「不调 collectgarbage 的 {@code __gc} 用例」两边就对不上。 + *

+ * 顺序无所谓(Lua 没规定),可达的与已入队的两批都过一遍。 + */ + public void close() { + synchronized (finalizables) { + for (WeakReference r : finalizables) { + Object t = r.get(); + if (t != null) { + runGc(this, t); + } + } + finalizables.clear(); + } + drainFinalizers(this); + } + + /* ==================== 线程查询 ==================== */ + + /** {@code lua_gettop} */ + public int getTop() { + return top - (ci.func + 1); + } + + /** + * {@code lua_upvalueindex(n)} 的取值:读当前正在跑的闭包的第 + * {@code i} 个 upvalue(从 0 起)。 + *

+ * C 版用伪栈下标表达,这里直接给值。C 闭包绑的是值,Lua 闭包绑的是槽, + * 所以后者要过一层 {@link UpVal#value()}。 + */ + public LuaValue upvalue(int i) { + LuaValue f = stack[ci.func]; + if (f.tt == Lua.VCCL) { + return ((CClosure) f.o).upvalue[i]; + } + if (f.tt == Lua.VLIF) { + return ((LClosure) f.o).upvals[i].value(); + } + throw new IllegalStateException("current frame is not a closure"); + } + + /** {@code lua_getstack(L, 0, &ar)}:还有没有调用帧 */ + public boolean hasFrames() { + return ci != baseCi; + } + + /** {@code lua_status} */ + public int getStatus() { + return status; + } + + /** + * {@code lua_pushthread}:压入当前线程,返回「是不是主线程」。 + * {@code coroutine.running} 靠这个返回值区分主协程。 + */ + public int pushThread() { + push(LuaValue.ofThread(this)); + return (mainThread == this) ? 1 : 0; + } + + /** + * {@code luaS_newlstr}:运行时新建字符串。 + *

+ * 与编译期({@link Lexer})那条路的唯一区别是来源,驻留是同一件事:短字符串 + * 全局唯一。少了这一步,{@code string.rep("a", 10)} 与 + * {@code string.rep("aa", 5)} 是两个不同的对象——上游 {@code strings.lua} + * 就拿 {@code string.format("%p", s1) == string.format("%p", s2)} 来测这一点。 + * 长字符串不驻留({@link StringPool#intern} 里判),与上游一致。 + */ + public LuaString newString(byte[] bytes) { + return strings.intern(LuaString.of(bytes)); + } + + /** {@link #newString(byte[])} 的带区间形态 */ + public LuaString newString(byte[] buf, int off, int len) { + return strings.intern(LuaString.of(buf, off, len)); + } + + /** {@code lua_isyieldable} */ + public boolean isYieldable() { + return nNy == 0; + } + + /** {@code yieldable(L)}:与 {@link #isYieldable} 同义,包内用短名 */ + private boolean yieldable() { + return nNy == 0; + } + + /* ==================== 调试钩子:C API 那一层 ==================== */ + + /** + * {@code ldebug.c: lua_sethook}。 + *

+ * 末尾那句 {@code settraps} 是不能省的。主循环里的 {@code trap} 是 + * 一个只在特定点重读的局部变量,「执行到一半调用 {@code debug.sethook}」 + * 时它还是 0;{@code settraps} 把所有已经活着的 Lua 帧的 + * {@code trap} 置 1,配合 {@code OP_CALL} 之后的 {@code updatetrap} + * 才让新开的钩子立刻生效。省掉它的话 + * {@code debug.sethook(f, "l")} 之后一行钩子都不会来。 + */ + public void setHook(LuaHook f, int mask, int count) { + if (f == null || mask == 0) { + mask = 0; + f = null; + } + hook = f; + baseHookCount = count; + resetHookCount(); + hookmask = mask; + if (mask != 0) { + for (CallInfo c = ci; c != null; c = c.previous) { + if (c.isLua) { + c.trap = true; + } + } + } + } + + /** {@code lua_gethook} */ + public LuaHook getHook() { + return hook; + } + + /** {@code lua_gethookmask} */ + public int getHookMask() { + return hookmask; + } + + /** + * {@code lua_gethookcount}:注意返回的是 {@code basehookcount}, + * 不是还剩几条——{@code debug.gethook()} 的第三个结果要的是周期。 + */ + public int getHookCount() { + return baseHookCount; + } + + /** {@code resethookcount(L)} */ + void resetHookCount() { + hookcount = baseHookCount; + } + + /** + * {@code lua_xmove}:把 {@code n} 个值从一个线程搬到另一个。 + *

+ * 值本身是拷贝不是共享槽:两个协程的栈是两条独立的 {@code LuaValue[]}, + * 共享槽会让一个协程写栈时把另一个的栈也改掉。 + */ + public static void xmove(LuaState from, LuaState to, int n) { + if (n <= 0) { + return; + } + /* + * 只降 top,不顺手清槽:接下来还要从 {@code from.top + i} 把值读出来。 + * C 版也是先 {@code from->top -= n} 再拷,拷的是新 top 之上的那几格—— + * 那些格子对 GC 已经不可见,但对这段代码还得是可读的。 + * 清掉它们的工作交给 {@link #clearDeadStackSlices},时机才对。 + */ + from.top -= n; + to.checkStack(n); + for (int i = 0; i < n; i++) { + to.stack[to.top].set(from.stack[from.top + i]); + to.top++; + } + } + + public LuaTable getGlobals() { + return globals; + } + + /* ==================== 栈操作 ==================== */ + + /** + * 保证下标 {@code 0 .. n-1} 的槽都存在。 + *

+ * 只增长、只追加新槽,绝不替换已有槽对象(见类注释的不变式)。 + */ + void ensureSlots(int n) { + if (n <= stack.length) { + return; + } + int cap = stack.length; + while (cap < n) { + cap *= 2; + } + LuaValue[] bigger = new LuaValue[cap]; + System.arraycopy(stack, 0, bigger, 0, stack.length); + for (int i = stack.length; i < cap; i++) { + bigger[i] = new LuaValue(); + } + stack = bigger; + } + + /** + * {@code luaD_checkstack} → {@code luaD_growstack}:保证从 {@code top} 起 + * 还有 {@code n} 个槽,并根据 {@link #MAXSTACK} 判「stack overflow」。 + *

+ * 两档行为照抄 {@code ldo.c}:常态下超过 {@code MAXSTACK} 就把栈撑到 + * {@code ERRORSTACKSIZE}(给后面的错误处理留地方)再抛; + * 已经在 {@code stackErr} 里还要涨,就抛 {@link #errErr}。 + */ + void checkStack(int n) { + /* + * 用 {@code long} 算:{@code top + n} 用 {@code int} 会溢出。 + * {@code table.unpack(t, 1, 2^31-1)} 这种 {@code n} 一下子就把和 + * 顶成负数,「超过 MAXSTACK」这个判断反而失效,于是栈一路 push 到 + * {@code OutOfMemoryError}——上游 {@code sort.lua} 第一节正撞这里。 + */ + long need = (long) top + n; + if (need > (stackErr ? ERRORSTACKSIZE : MAXSTACK)) { + if (stackErr) { + throw errErr(); + } + stackErr = true; + ensureSlots(ERRORSTACKSIZE); + throw runError("stack overflow"); + } + ensureSlots((int) need); + } + + /** + * {@code lapi.c: lua_checkstack}:只判「还容不容得下 {@code n} 个槽」, + * 不抛错。 + *

+ * 与 {@link #checkStack} 的差别不只是返回值:措辞不同的错误各有各的出处—— + * {@code table.unpack} 报 {@code too many results to unpack}、 + * {@code coroutine.resume} 报 {@code too many results to resume}, + * 都不是笼统的 {@code stack overflow}。让 {@link #checkStack} 代抛就把 + * 这两句都吞成一句,上游 {@code coroutine.lua} 末尾那个 + * 「{@code table.unpack({}, 1, lim)}」的用例正是对着它们断言的。 + */ + boolean checkStackOk(int n) { + /* 同上:{@code top + n} 溢出会让这个判断整个失效 */ + long need = (long) top + n; + if (need > (stackErr ? ERRORSTACKSIZE : MAXSTACK)) { + return false; + } + ensureSlots((int) need); + /* 与 C 版一致:顺带把当前帧的 top 抬上去,后面的压栈才落在帧内 */ + if (ci.top < need) { + ci.top = (int) need; + } + return true; + } + + /** + * {@code luaD_shrinkstack}。 + *

+ * C 版在这里把 {@code ERRORSTACKSIZE} 的栈收回正常大小,顺带也就退出了 + * 「正在处理栈溢出」的状态——这正是 {@code stacksize(L) == ERRORSTACKSIZE} + * 那个判据能复位的原因。这里只需要把标志清掉:栈数组缩不缩纯粹是内存问题, + * 交给 JVM 更省事,但缩一下能让跑过 {@code heavy.lua} 这类用例之后的内存 + * 回到正常量级。 + */ + void shrinkStack() { + int inuse = stackInUse(); + int max = (inuse > MAXSTACK / 3) ? MAXSTACK : inuse * 3; + if (inuse <= MAXSTACK && stack.length > max) { + int nsize = (inuse > MAXSTACK / 2) ? MAXSTACK : inuse * 2; + trimStack(Math.max(nsize, BASIC_STACK_SIZE)); + /* + * 「正在处理栈溢出」这个状态,C 版的判据是 + * {@code stacksize(L) == ERRORSTACKSIZE} —— 也就是说它是被 + * 收回常规尺寸这个动作结束的,不是被 {@code luaD_shrinkstack} + * 这个调用结束的。 + *

+ * 无条件在这里清标志会出这种事:消息处理函数内部再来一次 + * {@code pcall},那个内层 {@code pcall} 收尾时调了 + * {@code shrinkStack},而它自己的 {@code inuse} 很大(外层的帧 + * 还在),于是没有真的收缩,标志却被清成 false;等处理函数 + * 接着调用 {@code tostring}/{@code math.sin},{@code checkStack} + * 就按 {@code MAXSTACK} 判,立刻又报一次 + * {@code stack overflow}——本该拿到 {@code error in error handling} + * 或正常结果的。 + */ + stackErr = false; + } + } + + /** + * {@code ldo.c: stackinuse}:栈上真正还在用的格数。 + *

+ * 不是 {@code top},而是 {@code top} 与每一帧 {@code ci.top} 的 + * 最大值——帧的寄存器区一直延伸到 {@code ci.top},哪怕 {@code top} 已经 + * 落回帧内很浅的位置。 + *

+ * 只按 {@code top} 算会在这种情形下把栈锯短:{@code xpcall} 的消息处理函数 + * 是在栈已经撑到 {@code ERRORSTACKSIZE} 的时候跑的,它内部再来一次 + * {@code pcall},那个内层 {@code pcall} 收尾时 {@code top} 才一千出头 + * (是处理函数自己的局部栈顶),于是栈被锯回 {@code MAXSTACK},而外层 + * 那几百万格还在用的帧瞬间越界——表现是 + * {@code ArrayIndexOutOfBoundsException}。上游 {@code errors.lua} 的 + * 「error in error handling」一段正踩在这个坑上。 + */ + private int stackInUse() { + int lim = top; + for (CallInfo c = ci; c != null; c = c.previous) { + if (lim < c.top) { + lim = c.top; + } + } + return (lim + 1 < MINSTACK) ? MINSTACK : lim + 1; + } + + /** 只缩不扩,且绝不缩掉 {@code top} 以下的活槽 */ + private void trimStack(int nsize) { + if (nsize >= stack.length || nsize < top) { + return; + } + LuaValue[] smaller = new LuaValue[nsize]; + System.arraycopy(stack, 0, smaller, 0, nsize); + stack = smaller; + } + + /** + * {@code ldo.c: luaD_seterrorobj} 里 {@code LUA_ERRERR} 那一档: + * 不取栈顶的错误值,固定写 {@code "error in error handling"}。 + */ + private LuaValue errObjOf(LuaError e) { + return e.errErr ? LuaValue.ofString(LuaString.latin1(ERRERR_MSG)) : e.value; + } + + /** {@code luaD_throw(L, LUA_ERRERR)} */ + private LuaError errErr() { + LuaError e = new LuaError(LuaValue.ofString(LuaString.latin1(ERRERR_MSG)), ""); + e.errErr = true; + e.addTraceback = false; + return e; + } + + /** 取栈槽。等价于 C 的 {@code s2v(L->top.p + i)} */ + public LuaValue at(int idx) { + return stack[idx]; + } + + /** 压入一个值(拷贝,不是共享槽) */ + public void push(LuaValue v) { + ensureSlots(top + 1); + stack[top++].set(v); + } + + public void pushNil() { + ensureSlots(top + 1); + stack[top++].setNil(); + } + + void pushFunction(LuaFunction f) { + ensureSlots(top + 1); + stack[top++].setObject(Lua.VLCF, f); + } + + void pushClosure(LClosure c) { + ensureSlots(top + 1); + stack[top++].setObject(Lua.VLIF, c); + } + + /** 当前帧的参数个数 */ + public int argCount() { + return top - ci.func - 1; + } + + /** 第 i 个参数(0 起) */ + public LuaValue arg(int i) { + int idx = ci.func + 1 + i; + /* + * 越界必须给 nil,不能把栈槽里的残值交出去。 + * C 里 lua_type 对越界下标返回 LUA_TNONE,取值拿到的是 nilvalue; + * 而我们的栈是复用的数组,越界读出来的是上一帧留下的东西—— + * 实测 pcall(setmetatable) 有时报 "got function"、有时 "got nil", + * 取决于那一刻槽里恰好是什么。这类 bug 的难处在于它随机: + * 同一个用例这次绿下次红。 + */ + return (idx >= top) ? LuaValue.ofNil() : stack[idx]; + } + + /** {@code lua_settop}(正数下标版:1 = 第一个参数,0 = 清空) */ + void setTop(int idx) { + int t = ci.func + 1 + idx; + if (t > top) { + ensureSlots(t); + while (top < t) { + stack[top++].setNil(); + } + } else { + while (top > t) { + stack[--top].setNil(); + } + } + } + + /** {@code lua_pop}:从栈顶扔掉 {@code n} 个 */ + void pop(int n) { + while (n-- > 0 && top > 0) { + stack[--top].setNil(); + } + } + + /** {@code lua_pushvalue}:把第 {@code idx} 个(1 起)再压一份 */ + void pushValue(int idx) { + push(stack[ci.func + idx]); + } + + /** {@code lua_replace}:用栈顶覆盖第 {@code idx} 个(1 起)并弹栈 */ + void replace(int idx) { + stack[ci.func + idx].set(stack[--top]); + } + + /* ==================== upvalue ==================== */ + + /** + * {@code luaF_findupval}:找到指向 {@code stackIdx} 的开放 upvalue,没有就建一个。 + *

+ * 列表按 {@code stackIdx} 升序,所以从后往前扫能尽早命中「最近创建」的那批。 + */ + UpVal findUpval(int stackIdx) { + for (int i = openUpvals.size() - 1; i >= 0; i--) { + UpVal uv = openUpvals.get(i); + if (uv.stackIdx == stackIdx) { + return uv; + } + if (uv.stackIdx < stackIdx) { + break; + } + } + UpVal uv = UpVal.open(stack[stackIdx], stackIdx); + int pos = 0; + while (pos < openUpvals.size() && openUpvals.get(pos).stackIdx < stackIdx) { + pos++; + } + openUpvals.add(pos, uv); + return uv; + } + + /** + * {@code luaF_newtbcupval}:为 {@code } 变量新建一个 to-be-closed 的 + * 开放 upvalue,并当场检查它真的有 {@code __close}。 + *

+ * 两个容易漏掉的细节,都是实测出来的: + *

    + *
  • {@code nil} 与 {@code false} 直接跳过,不需要 {@code __close} + * (C 版是 {@code if (l_isfalse(s2v(level))) return;})。所以 + * {@code local x = nil} 完全合法。
  • + *
  • 检查在赋值的当下做,不在作用域结束时。所以 + * {@code local x = 5 print(x)} 在 {@code OP_TBC} 上就抛错, + * {@code print} 根本不会执行——把它挪到关闭时检查,输出会先多出一行。
  • + *
+ *

+ * 与 {@link #findUpval} 不同,这里不去重:C 版也是每次新建一个, + * 因为 tbc 变量是「槽」而不是「变量绑定」,同一个槽可以被标记多次。 + */ + UpVal newTbcUpval(int stackIdx) { + LuaValue v = stack[stackIdx]; + if (v.isNil() || (v.tt == Lua.TBOOLEAN && !v.toBoolean())) { + // false / nil 不需要关闭,也不建 upvalue(C 版直接 return) + return null; + } + LuaValue tm = LuaTm.getTmByObj(this, v, Tm.TM_CLOSE); + if (tm == null || tm.isNil()) { + LuaString vn = localNameAt(stackIdx - ci.func); + // "%s" 是 C 字符串:局部名(只有二进制 chunk 能带 NUL)在这里被砍断 + throw runError("variable '" + + ((vn == null) ? "?" : LuaString.cutAtNul(vn.toLatin1String())) + + "' got a non-closable value"); + } + UpVal uv = UpVal.open(stack[stackIdx], stackIdx); + uv.toBeClosed = true; + int pos = 0; + while (pos < openUpvals.size() && openUpvals.get(pos).stackIdx < stackIdx) { + pos++; + } + openUpvals.add(pos, uv); + return uv; + } + + /** + * {@code luaF_close}:关闭 {@code stackIdx >= level} 的全部开放 upvalue。 + *

+ * 从栈顶往下关(与 C 版一致),因为 {@code __close} 元方法可能改动栈、 + * 也可能再建 upvalue。tbc 变量在关闭前先跑 {@code __close}。 + * + * @param errObj 传给 {@code __close} 的错误对象;{@code null} 表示正常关闭 + */ + void closeUpvals(int level, LuaValue errObj) { + closeUpvals(level, errObj, false); + } + + /** + * @param forceYy {@code lfunc.c: luaF_close} 的 {@code yy} 参数取 1: + * 不看当前帧是不是 Lua 帧,一律允许 {@code __close} + * 里 yield + */ + void closeUpvals(int level, LuaValue errObj, boolean forceYy) { + /* + * 下面会动栈({@code __close} 的调用帧就摆在变量之上),而传进来的 + * {@code errObj} 常常是某个栈槽的别名——先拷一份,否则写完调用帧它就 + * 变成了 {@code __close} 自己。 + */ + LuaValue err = (errObj == null) ? null : new LuaValue(errObj); + while (!openUpvals.isEmpty()) { + UpVal uv = openUpvals.get(openUpvals.size() - 1); + if (uv.stackIdx < level) { + break; + } + openUpvals.remove(openUpvals.size() - 1); + LuaValue obj = uv.value(); + boolean isTbc = uv.toBeClosed; + int absIdx = uv.stackIdx; // 绝对下标,close() 之后就取不到了 + int localIdx = absIdx - ci.func; // luaG_findlocal 的序号 + uv.close(); + if (isTbc && !(obj.isNil())) { + LuaTm.callCloseMethod(this, obj, err, absIdx, localIdx, forceYy); + } + } + } + + /* ==================== 类型元表 ==================== */ + + LuaTable typeMetatable(int baseType) { + return typeMt[baseType]; + } + + /** {@code lapi.c: lua_setmetatable} 里「给整个类型设元表」的分支 */ + public void setTypeMetatable(int baseType, LuaTable mt) { + typeMt[baseType] = mt; + } + + /* ==================== 调用 ==================== */ + + private CallInfo prepCallInfo(int funcIdx, int nresults, boolean isLua, int topIdx) { + CallInfo c = new CallInfo(); + c.func = funcIdx; + c.nresults = nresults; + c.isLua = isLua; + c.top = topIdx; + c.previous = ci; + ci = c; + return c; + } + + /** {@code ldo.c: precallC} —— C 函数在这里就被真正调用掉 */ + private int precallC(int funcIdx, int nresults, LuaFunction f) { + checkStack(MINSTACK); + CallInfo c = prepCallInfo(funcIdx, nresults, false, top + MINSTACK); + /* + * 调用钩子在「真正调 C 函数」之前。这个先后是能观测到的: + * 实测 {@code debug.sethook(function() error('x') end, 'c')} 之后 + * {@code pcall(function() ... end)},钩子在 pcall 的 C 帧上就炸了, + * 于是 pcall 抓不到这个错误——它的恢复点要到下面那一行才建起来。 + * 挪到 {@code f.call} 之后就会让 pcall 把它吞掉,与参考实现不符。 + */ + if ((hookmask & Lua.LUA_MASKCALL) != 0) { + DebugLib.dHook(this, Lua.LUA_HOOKCALL, -1, 1, top - funcIdx - 1); + } + int n = f.call(this); + poscall(c, n); + return n; + } + + /** + * {@code luaD_precall}:准备一次调用。 + * + * @return Lua 函数返回新帧(交由 {@link Vm} 执行);C 函数返回 {@code null} + * (函数已经跑完,结果在 {@code funcIdx} 处) + */ + CallInfo precall(int funcIdx, int nresults) { + while (true) { + LuaValue f = stack[funcIdx]; + if (f.tt == Lua.VLIF) { + LClosure cl = (LClosure) f.o; + Proto p = cl.p; + int narg = top - funcIdx - 1; + int nfixparams = p.numparams; + int fsize = p.maxstacksize; + checkStack(fsize); + CallInfo c = prepCallInfo(funcIdx, nresults, true, funcIdx + 1 + fsize); + c.savedpc = 0; + for (; narg < nfixparams; narg++) { + stack[top++].setNil(); + } + return c; + } else if (f.tt == Lua.VLCF) { + precallC(funcIdx, nresults, (LuaFunction) f.o); + return null; + } else if (f.tt == Lua.VCCL) { + precallC(funcIdx, nresults, ((CClosure) f.o).f); + return null; + } else { + funcIdx = tryFuncTM(funcIdx); + } + } + } + + /** + * {@code luaD_pretailcall}:为尾调用准备帧。 + *

+ * Lua 函数复用调用方的 CallInfo(这正是尾调用不涨栈的原因), + * C 函数则照常新建一帧跑完后销毁。 + * + * @return Lua 函数返回 {@code -1}(帧已就位,交给 {@link Vm} 跑); + * C 函数返回结果个数 + */ + int pretailcall(CallInfo c, int funcIdx, int narg1, int delta) { + while (true) { + LuaValue f = stack[funcIdx]; + if (f.tt == Lua.VCCL) { + /* + * C 参考实现里 {@code luaD_pretailcall} 有独立的 {@code LUA_VCCL} + * 分支,漏掉它的代价很隐蔽:{@code return wrapfn()} 这种尾调用 + * 会掉进 {@code default} 去试 {@code __call},于是报 + * 「attempt to call a function value」——一个明明是函数的函数 + * 说自己不可调用。{@code coroutine.wrap} 返回的就是 C closure。 + */ + return precallC(funcIdx, MULTRET, ((CClosure) f.o).f); + } else if (f.tt == Lua.VLIF) { + LClosure cl = (LClosure) f.o; + Proto p = cl.p; + int fsize = p.maxstacksize; + int nfixparams = p.numparams; + checkStack(Math.max(fsize - delta, 0)); + c.func -= delta; + for (int i = 0; i < narg1; i++) { + stack[c.func + i].set(stack[funcIdx + i]); + } + funcIdx = c.func; // 函数与参数已下移 + for (; narg1 <= nfixparams; narg1++) { + stack[funcIdx + narg1].setNil(); + } + c.top = funcIdx + 1 + fsize; + c.savedpc = 0; + c.tail = true; + top = funcIdx + narg1; + return -1; + } else if (f.tt == Lua.VLCF) { + return precallC(funcIdx, MULTRET, (LuaFunction) f.o); + } else { + funcIdx = tryFuncTM(funcIdx); + narg1++; // 元方法本身也是参数 + } + } + } + + /** {@code tryfuncTM}:不可调用的值,找 {@code __call} 元方法顶上 */ + private int tryFuncTM(int funcIdx) { + LuaValue tm = LuaTm.getTmByObj(this, stack[funcIdx], Tm.TM_CALL); + if (tm == null || tm.isNil()) { + throw callError(stack[funcIdx]); + } + // 把元方法插到 func 位置,其余整体右移一格 + checkStack(1); + for (int p = top; p > funcIdx; p--) { + stack[p].set(stack[p - 1]); + } + top++; + stack[funcIdx].set(tm); + return funcIdx; + } + + /** {@code ldo.c: moveresults} */ + private void moveResults(int res, int nres, int wanted) { + if (wanted == 0) { + top = res; + return; + } + if (wanted == 1) { + if (nres == 0) { + stack[res].setNil(); + } else { + stack[res].set(stack[top - nres]); + } + top = res + 1; + return; + } + if (wanted == MULTRET) { + wanted = nres; + } + int firstresult = top - nres; + if (nres > wanted) { + nres = wanted; + } + for (int i = 0; i < nres; i++) { + stack[res + i].set(stack[firstresult + i]); + } + for (int i = nres; i < wanted; i++) { + stack[res + i].setNil(); + } + top = res + wanted; + } + + /** {@code luaD_poscall} */ + void poscall(CallInfo c, int nres) { + int wanted = c.nresults; + /* + * 返回钩子。C 版的条件是 + * {@code L->hookmask && !hastocloseCfunc(wanted)}:带 to-be-closed + * 变量的 C 帧要先关变量、再报钩子(那是 {@code moveresults} 里的分支)。 + * 「有 tbc」在 C 里编码在 {@code nresults} 的高位,本实现不这么做, + * 只在 {@link #finishCcall} 那条 yield 恢复路径上用 + * {@link CallInfo#clsret} 记——所以走到这里时 {@code clsret} 恒为假, + * 与 C 的判据在可达路径上一致。 + */ + if (hookmask != 0 && !c.clsret) { + DebugLib.rethook(this, c, nres); + } + moveResults(c.func, nres, wanted); + ci = c.previous; + } + + /** {@code luaD_call}:从宿主调进 Lua */ + public void call(int funcIdx, int nresults) { + nCcalls++; + if (nCcalls >= MAXCCALLS) { + checkStack(0); // 先把 EXTRA_STACK 的临时占用清掉,再判 + checkCStack(); + } + try { + CallInfo c = precall(funcIdx, nresults); + if (c != null) { + c.fresh = true; + Vm.execute(this, c); + } + } finally { + nCcalls--; + } + } + + /** + * {@code lstate.c: luaE_checkcstack}。 + *

+ * 两档都留着是对的,而且中间那段「什么都不做」的区间正是设计: + * 第 200 层抛 {@code C stack overflow} 之后,消息处理函数还要在 201… + * 219 层里跑({@code luaG_errormsg} 会 {@code callnoyield} 调它), + * 直到 220 层({@code MAXCCALLS / 10 * 11})才认定「连错误处理都溢出了」, + * 转成 {@code LUA_ERRERR}。少了这段余量,{@code xpcall(f, f)} 之类的 + * 处理函数一进去就被判死,拿不到 {@code "error in error handling"}。 + */ + private void checkCStack() { + if (nCcalls == MAXCCALLS) { + throw runError("C stack overflow"); + } + if (nCcalls >= MAXCCALLS / 10 * 11) { + throw errErr(); + } + } + + /** + * {@code lstate.c: luaE_incCstack}。 + *

+ * 调用方是语法分析器的 {@code enterlevel}({@code #define enterlevel(ls) + * luaE_incCstack(ls->L)})。它与 {@link #call} 共用 {@code nCcalls},所以 + * 调用深度会吃掉表达式嵌套深度的配额——这正是参考实现的行为。 + */ + public void incCstack() { + nCcalls++; + if (nCcalls >= MAXCCALLS) { + checkCStack(); + } + } + + /** {@code leavelevel}:{@code L->nCcalls--} */ + public void decCstack() { + nCcalls--; + } + + /** + * {@code lua_callk}:带续体的调用。 + *

+ * 续体只在「给了 {@code k} 且当前可 yield」时才登记,否则走 + * {@link #callNoYield}——那时在被调用的函数里 yield 会报「attempt to yield + * across a C-call boundary」。这正是 C 版 {@code lua_call}({@code k} 为 + * {@code NULL})与 {@code lua_callk} 的分界。 + *

+ * 标准库里只有两处需要它:{@code pairs} 调 {@code __pairs} + * ({@code lbaselib.c: pairscont})与 {@code dofile} 调加载出来的 chunk + * ({@code dofilecont})。少了它,{@code __pairs} 里 yield 就会在恢复时 + * 撞上 {@code c.k == null}(上游 {@code nextvar.lua} 专门测这一条)。 + */ + /** + * {@code ltm.c} 里反复出现的那句 + * 「metamethod may yield only when called from Lua code」 + * ({@code luaT_callTM} / {@code luaT_callTMres})。 + *

+ * 判据是当前帧是不是 Lua 帧:从 Lua 代码里触发的元方法可以 yield, + * 从 C 函数里触发的不行(那时报「attempt to yield across a C-call + * boundary」)。一律放行会让 {@code table.sort} 之类的回调真的 yield 出去, + * 而恢复时 {@link CallInfo#k} 是 {@code null}——直接 NPE。 + */ + void callTmMightYield(int funcIdx, int nresults) { + if (ci.isLua) { + call(funcIdx, nresults); + } else { + callNoYield(funcIdx, nresults); + } + } + + void callK(int funcIdx, int nresults, int ctx, LuaKFunction k) { + if (k != null && yieldable()) { + ci.k = k; + ci.ctx = ctx; + call(funcIdx, nresults); + } else { + callNoYield(funcIdx, nresults); + } + } + + /** + * {@code lua_pcall}:带保护地调用。 + *

+ * 只捕获 {@link LuaError}。实现自身的 Java 异常必须继续上冒,否则缺陷会被 + * 引擎吞成「脚本报错」。 + * + * @return 出错时返回错误对象,否则 {@code null} + */ + public LuaValue pcall(int funcIdx, int nresults) { + return pcall(funcIdx, nresults, null); + } + + /** + * {@code lua_pcall} 带消息处理函数的形态({@code xpcall} 用它)。 + *

+ * 处理函数必须在调用链还没退的时候跑。C 版是在抛出点 + * ({@code ldebug.c: luaG_errormsg})直接 {@code luaD_callnoyield} 调它, + * 那会儿整条调用链都还在——这正是 {@code xpcall(f, debug.traceback)} 能打出 + * 完整回溯的原因。放在「先恢复调用链再调处理函数」的位置上是错的: + * 回溯会只剩最外面两层,而这是 Lua 项目打错误日志的标准写法。 + *

+ * Java 这边没有 longjmp,{@code catch} 到的时候 Java 栈已经退了,但 + * Lua 栈没有({@link #stack} / {@link #top} / {@link #ci} 都还是出错 + * 那一刻的样子,恢复动作就在下面几行)。所以处理函数放在 {@code catch} 的最 + * 前面跑,等价于 C 的在抛出点跑。 + * + * @param handler 消息处理函数;{@code null} 表示没有(与 {@code msgh = 0} 等价) + * @return 出错时返回(可能被处理函数改写过的)错误对象,否则 {@code null} + */ + public LuaValue pcall(int funcIdx, int nresults, LuaValue handler) { + int oldTop = top; + CallInfo saved = ci; + // luaD_pcall:L->errfunc = ef(ef 为 0 即「没有」,与 handler == null 等价) + LuaValue oldErrFunc = errFunc; + errFunc = handler; + /* + * {@code luaD_pcall} 里的 {@code old_allowhooks}。 + *

+ * 恢复的时机很讲究:钩子内抛错时 {@code luaD_hook} 的收尾 + * (把 {@code allowhook} 置回 1)被跳过了,此时 {@code allowhook} 还是 0。 + * 而消息处理函数在 C 版是 {@code luaG_errormsg} 在抛出点调的, + * 那会儿 {@code allowhook} 依然是 0——所以处理函数(也就是 + * {@code luaL_traceback})跑的时候不会再触发钩子。 + * 提前恢复成 1 会让回溯过程中又进一次钩子、再抛一次错,最后被 + * {@code applyErrMsg} 兜成 {@code "error in error handling"}。 + * 实测:{@code debug.sethook(function() error("x") end, "c")} 之后 + * 未捕获的错误,参考实现给的是完整回溯,不是那句话。 + */ + boolean oldAllowHook = allowHook; + /* + * {@code luaD_rawrunprotected} 最后那句 {@code L->nCcalls = oldnCcalls} + * ——无论成功失败都要回滚。 + *

+ * 少了这一句会漏:{@code luaD_call} 是「先 {@code nCcalls++},再判 + * {@code luaE_checkcstack}」,判不过就直接抛,自增那一步落在 + * {@code try} 外面,{@code finally} 管不到。C 那边靠 longjmp 回到 + * 这里的恢复点兜住,Java 没有 longjmp,只能自己记。 + *

+ * 实测:脚本里每来一次 C 栈溢出就永久少一层配额, + * {@code pcall} 递归探针从 197 一路掉到 193,上游 {@code errors.lua} + * 的「error in error handling」段因此提前触发。 + */ + int oldNCcalls = nCcalls; + try { + try { + call(funcIdx, nresults); + return null; + } catch (LuaError e) { + /* + * 位置信息在这里补,而不是在抛出的地方补:像「除以零」这类错误 + * 来自 Arith 这种没有 LuaState 的纯算术层,它抛的是裸错误值, + * 只有走到保护边界才知道当前行号。这正是 C 版 msghandler 里 + * luaL_traceback 干的活。 + */ + LuaValue v; + if (e.errErr) { + /* + * {@code luaD_throw(L, LUA_ERRERR)} 与 {@code luaG_errormsg} + * 是两条路:后者才会调 {@code L->errfunc},前者直接跳。所以 + * {@code ERRERR} 不再过消息处理函数——它本来就是因为 + * 处理函数把栈跑穿才产生的。 + */ + v = LuaValue.ofString(LuaString.latin1(ERRERR_MSG)); + } else if (e.errMem) { + /* + * {@code LUA_ERRMEM} 直跳,不进 {@code luaG_errormsg}: + * 消息处理函数接不到,位置前缀也不补(内存错误的值恒是那个 + * 驻留短串,C 版 {@code luaD_seterrorobj} 写的就是它)。 + */ + v = e.value; + } else { + v = e.value; + if (e.where == null && v != null && v.isString()) { + LuaString prefix = where(); // 必须用出错时的帧,先于恢复调用链 + v = LuaValue.ofString(prefix.concat(v.asString())); + } + if (handler != null) { + /* + * C 版在抛出点还留着保护调用自己那一次 {@code ccall} + * 加的层:{@code f_call → luaD_callnoyield → ccall} 的 + * {@code L->nCcalls -= inc} 被 {@code longjmp} 跳过了。 + * 本实现的 {@link #call} 用 {@code finally} 把它减回去, + * 于是处理函数整体浅一格 —— 递归到 C 栈上限的层数随之差 1。 + *

+ * 实测(处理函数连续出错 {@code K} 次后才成功): + * {@code K=196} 参考给 {@code done:C stack overflow}、 + * 本实现给 {@code done:...h};{@code K=215} 参考给 + * {@code error in error handling}、本实现还能成功。 + * 补上这一格之后两处临界点都对齐。 + */ + nCcalls = oldNCcalls + 1; + v = applyErrMsg(v, handler); + } + } + /* + * 回滚点就在这里,不能早也不能晚: + *

    + *
  • 不能早:消息处理函数是 C 版在抛出点 + * ({@code luaG_errormsg})调的,那会儿 {@code nCcalls} 还 + * 带着溢出时的高位(201…219 那段「给错误处理留的余量」正是 + * 为它准备的)。提前回滚,{@code debug.traceback} 就跑在 + * 一个比参考实现更浅的位置上;
  • + *
  • 不能晚:{@code __close} 是回滚之后才跑的 + * ({@code luaD_closeprotected} 在 rawrunprotected 之外), + * 带着高位跑会让收尾时的调用白白撞上 + * {@code C stack overflow}。
  • + *
+ */ + nCcalls = oldNCcalls; + // 到这一步才算走到 C 版 luaD_pcall 的恢复点:__close 是带钩子的 + allowHook = oldAllowHook; + /* + * C 版 {@code luaD_pcall} 是先 {@code L->ci = old_ci} 再 + * {@code luaD_closeprotected}。顺序反了,{@code __close} 里 + * {@code debug.getinfo(2)} 看到的就是出错的那个 Lua 帧而不是 + * {@code pcall}——实测上游 {@code locals.lua} 断言 + * {@code debug.getinfo(2).name == "pcall"},照旧写法拿到的是 + * {@code "error"}({@code foo} 里最后调的是 {@code error})。 + */ + ci = saved; + /* + * {@code forceYy} 取 {@code yieldable()},对应 C 版 + * {@code lua_pcallk} 那个分支判据 {@code k != NULL && yieldable(L)}: + * 带续体的 {@code pcall} 在可 yield 的线程里不建本地恢复点, + * 错误一路冒到 {@code lua_resume},由 {@code precover} → + * {@code finishpcallk} → {@code luaF_close(..., yy = 1)} 收尾。 + *

+ * 本实现用 Java 异常代替长跳转,恢复点就在这里的 {@code catch}, + * 于是只能把「要不要允许 {@code __close} yield」显式传下去。取 + * {@code false} 时 {@code __close} 里的 yield 会报 + * 「attempt to yield across a C-call boundary」——上游 + * {@code locals.lua}「yielding inside closing metamethods」 + * 第三段(函数体里 {@code error(10)})正测这条:三个 {@code } + * 变量要能依次 yield 出去。 + */ + v = closeUpvalsOnUnwind(saved, v, yieldable(), handler); // 返回值可能已被 __close 换掉 + top = oldTop; + shrinkStack(); // 与 C 版 luaD_pcall 末尾那句同位置 + return v; + } + } finally { + errFunc = oldErrFunc; + } + } + + /** + * {@code ldebug.c: luaG_errormsg} 里跑消息处理函数的那一段。 + *

+ * 处理函数是在出错的那一帧之上起新帧调用的(C 版把 {@code msgh} 压在 + * 当时的 {@code L->top} 上),所以它的 {@code previous} 就是出错的那一帧—— + * 回溯能从这里一路走到底。 + *

+ * 重入保护:处理函数自己又出错时,C 版会再次进入 {@code luaG_errormsg}, + * 而 {@code L->errfunc} 还在,于是一路递归到 C 栈溢出、被 + * {@code luaE_checkcstack} 兜成 {@code LUA_ERRERR},最后 + * {@code luaD_seterrorobj} 把它写成 {@code "error in error handling"}。 + * 这里直接认这个结果,中间那几十层递归没有意义。 + */ + /** + * {@code ldebug.c: luaG_errormsg} 里那句「调 {@code L->errfunc}」。 + *

+ * 给被保护区包住的嵌套加载用({@code load} / {@code loadfile} / + * {@code require}):C 那边是 {@code luaD_protectedparser} 把 + * {@code L->errfunc} 原样传进 {@code luaD_pcall},于是读函数里 + * {@code error()} 的抛出点就被外层 {@code xpcall} 的处理器接住了, + * {@code load} 返回的是 {@code nil, "H:..."}。 + *

+ * 只有非{@link LuaError#directThrow} 的错才过这一道——语法错误在 C 里 + * 走 {@code luaD_throw} 直跳,压根不进 {@code luaG_errormsg}。 + */ + LuaValue applyErrFunc(LuaValue v) { + return (errFunc == null) ? v : applyErrMsg(v, errFunc); + } + + /** + * 处理函数自身出错时再进一次 {@code luaG_errormsg}。 + *

+ * 曾经这里是个重入开关:第二层直接判死,返回 + * {@code "error in error handling"}。那把「第二次调用其实会成功」的情形也 + * 一起吞了。实测分歧(错误对象是个 {@code __tostring} 返回非串的表, + * 处理函数写 {@code type(m) .. ":" .. tostring(m)}): + *

+     * 参考:false  string:case.lua:6: '__tostring' must return a string
+     * 本实现:false  error in error handling
+     * 
+ * 道理是:C 版 {@code luaG_errormsg} 是在抛出点无保护地 + * {@code luaD_callnoyield} 处理函数,而那时 {@code L->errfunc} 还设着 + * ({@code luaD_pcall} 要到保护区出口才还原)。所以处理函数里 + * {@code error(...)} 会再次走进 {@code luaG_errormsg},把新的错误对象 + * 交给同一个处理函数——递归,而不是失败。 + *

+ * 判死的条件不是「重入」,是深度:{@code luaD_callnoyield} 每次 + * {@code incXCcalls} 加一层,而错误是 longjmp 出去的,对应的 + * {@code decXCcalls} 不会执行,于是层数一层层累上去。第 200 层 + * {@code luaE_checkcstack} 抛 {@code C stack overflow}(这个消息又会被交给 + * 处理函数),一直累到 220 层({@code MAXCCALLS / 10 * 11})才 + * {@code luaD_throw(L, LUA_ERRERR)},最终落成 + * {@code "error in error handling"}。 + *

+ * Java 这边 {@link #call} 的 {@code nCcalls--} 在 {@code finally} 里,longjmp + * 那一步被 {@code finally} 抵消了,所以要在 {@code catch} 里手工把那一层加回去。 + */ + private LuaValue applyErrMsg(LuaValue v, LuaValue handler) { + int saved = nCcalls; + try { + return applyErrMsgRec(v, handler); + } finally { + /* + * 递归累上去的层数必须在本函数出口还回去。 + *

+ * C 版是 {@code luaD_throw} 跳到 {@code luaD_rawrunprotected} 的恢复点, + * 那里无条件 {@code L->nCcalls = oldnCcalls}。本实现没有 longjmp, + * 而且 {@link #closeUpvalsOnUnwind} 里那次调用发生在 {@code pcall} + * 的恢复点之后,不在这里还,层数就永久漏在外面——实测 + * {@code xpcall} + 永远抛错的处理器 + {@code } 之后,同一次 + * 运行里后续所有 {@code coroutine.resume} 都变成 + * {@code false, "C stack overflow"}。 + */ + nCcalls = saved; + } + } + + private LuaValue applyErrMsgRec(LuaValue v, LuaValue handler) { + int hf = top; + int depth = nCcalls; + try { + checkStack(2); + stack[hf].set(handler); + stack[hf + 1].set(v); + top = hf + 2; + /* + * 处理函数必须在不可 yield 的上下文里调。 + *

+ * C 版这里是 {@code luaD_callnoyield}。上一版用的是 {@code pcall}, + * 于是 {@code xpcall} 的消息处理函数里 {@code coroutine.yield} + * 竟然成功了:实测参考实现给的是 + * {@code false, "error in error handling"},本实现给的是 + * {@code true, "in msgh"}(协程停在 suspended)。 + *

+ * 道理是:能在错误处理的续体里 yield,就意味着「错误已经交出去一半」 + * 的中间态可以被挂起,而 C 版没有这种状态。 + */ + callNoYield(hf, 1); + } catch (LuaError e) { + top = hf; + // 补上 incXCcalls 加的那一层(C 版靠 longjmp 跳过 decXCcalls 保留它) + nCcalls = depth + 1; + if (e.errErr) { + /* + * 累到 220 层:{@code luaD_throw(L, LUA_ERRERR)} 直跳到最内层 + * 的保护区,由 {@code luaD_seterrorobj} 写成那句固定串。 + * 这里必须返回而不是继续上抛 —— this 帧就在 {@code pcall} + * 的 {@code catch} 块里,再抛就越过那道边界了(实测: + * {@code xpcall(f, function() error('h') end)} 外层再套 + * {@code pcall},本实现给 {@code false, "error in error handling"}, + * 参考给 {@code true, false, "error in error handling"})。 + */ + return LuaValue.ofString(LuaString.latin1(ERRERR_MSG)); + } + return applyErrMsgRec(e.value, (errFunc != null) ? errFunc : handler); + } + LuaValue res = new LuaValue(stack[top - 1]); + top = hf; + return res; + } + + /** + * 出错展开时关掉被丢弃帧上的 upvalue,带错误对象跑 {@code __close}。 + *

+ * 对应 {@code ldo.c: luaD_closeprotected}。正常返回时 {@code OP_CLOSE} 与 + * {@code OP_RETURN} 自己会关,出错路径没有人关——不在这里补, + * {@code local f = ... error('x')} 里的 {@code __close} 就永远不跑。 + *

+ * {@code __close} 自己又出错时,新错误顶掉原错误(实测: + * {@code error('orig')} 之后 {@code __close} 里 {@code error('in close')}, + * {@code pcall} 拿到的是 {@code in close})。所以这里返回的是可能已被换掉 + * 的错误对象,调用方必须拿返回值覆盖原来的。 + *

+ * 另一个细节是循环:C 版用 {@code luaD_rawrunprotected} 一次只关一批, + * 出错就记下新错误再接着关剩下的,直到关完。这里同样循环到没有可关的为止, + * 但加了「一轮下来一个都没关掉就停」的兜底,避免某个 {@code __close} + * 一进入就抛错时转不出来。 + * + * @param forceYy 同 {@link #closeUpvals(int, LuaValue, boolean)} 的 + * {@code forceYy};由调用方按「这个 {@code pcall} 是不是 + * 可 yield 的 {@code pcall}」给 + * @param handler 消息处理函数({@code null} 表示没有)。关闭期间抛出的错 + * 也要过它——见下面 {@code catch} 里的注释 + * @return 最终的错误对象(可能与传入的 {@code errObj} 不同) + */ + private LuaValue closeUpvalsOnUnwind(CallInfo target, LuaValue errObj, boolean forceYy, + LuaValue handler) { + if (target == null || openUpvals.isEmpty()) { + return errObj; + } + int level = target.func + 1; + CallInfo saved = ci; + /* 同上:{@code errObj} 可能是栈槽别名,而这一趟下来栈要被反复改写 */ + if (errObj != null) { + errObj = new LuaValue(errObj); + } + while (true) { + int before = openUpvals.size(); + try { + closeUpvals(level, errObj, forceYy); + return errObj; + } catch (LuaYield y) { + /* + * {@code __close} 里 yield 出去了,本帧的收尾只能等 resume 之后 + * 由 {@link #finishPcallK} 接着做。错误对象要留给它——见 + * {@link #closeErrPending} 的说明。 + */ + closeErrPending = errObj; + throw y; + } catch (LuaError e2) { + LuaValue v = e2.value; + if (e2.errMem) { + // 同上:内存错误不过消息处理函数,位置也不补 + } else if (e2.where == null && v != null && v.isString()) { + LuaString prefix = where(); // 出错那刻的帧,先于恢复 + v = LuaValue.ofString(prefix.concat(v.asString())); + } + /* + * {@code __close} 自己抛的错也要过消息处理函数。 + *

+ * C 版 {@code luaD_pcall} 是到最后一句才 + * {@code L->errfunc = old_errfunc} 恢复的,而 + * {@code luaD_closeprotected} 就在那句之前——所以跑 + * {@code __close} 的整个过程中 {@code L->errfunc} 仍然设着, + * {@code __close} 里 {@code error(...)} 走到 + * {@code luaG_errormsg} 时处理函数照样被调。 + *

+ * 漏掉这一步的实测分歧({@code xpcall} + 带 {@code } 的函数 + * 体里 {@code error},且 {@code __close} 也 {@code error}): + *

+                 * 参考:msgh orig | close a err=H | msgh E:a | false  H
+                 * 本实现:msgh orig | close a err=H |            false  E:a
+                 * 
+ * 也就是最终结果是处理函数的返回值,不是 {@code __close} + * 那个原始错。{@code error in error handling} 那一条也靠这里: + * 处理函数自己抛错时,{@code __close} 的错再过一次它被兜成那句话。 + *

+ * 内存错误例外:{@code __close} 里 + * {@code error("not enough memory", 0)} 走的是 + * {@code luaM_error},压根不进 {@code luaG_errormsg}, + * 处理函数接不到。 + */ + if (handler != null && !e2.errMem) { + v = applyErrMsg(v, handler); + } + ci = saved; + errObj = new LuaValue(v); + if (openUpvals.size() >= before) { + return errObj; // 一个也没关掉,再试也不会有进展 + } + } + } + } + + /** 恢复调用链到初始状态。宿主在捕获错误后必须调用,否则下一次调用从脏帧开始 */ + public void reset() { + ci = baseCi; + top = 0; + openUpvals.clear(); + closeErrPending = null; + } + + /* ==================== 协程:yield / resume / unroll ==================== */ + + /** + * {@code lua_yieldk}。 + *

+ * 抛 {@link LuaYield} 而不是返回值——C 版这里是 + * {@code luaD_throw(L, LUA_YIELD)},一跳到底,中间所有帧都不做收尾。 + * 这正是 yield 能穿过 {@code pcall} 的原因:{@code pcall} 只捕 + * {@link LuaError},这个异常它看不见,于是一路冒到 {@link #resume}。 + *

+ * 顺带一提,两条不能 yield 的报错措辞不同,得靠 {@link #mainThread} 区分。 + * 实测参考实现:{@code table.sort} 的比较函数里 yield 报 + * {@code attempt to yield across a C-call boundary};主协程里直接 yield 报 + * {@code attempt to yield from outside a coroutine}。两条都不带位置前缀, + * 因为当时 {@code L->ci} 是 C 帧({@link #where} 对 C 帧返回空串)。 + */ + public void yieldk(int nresults) { + if (!yieldable()) { + throw runError((mainThread == this) + ? "attempt to yield from outside a coroutine" + : "attempt to yield across a C-call boundary"); + } + status = LUA_YIELD; + ci.nyield = nresults; + throw LuaYield.INSTANCE; + } + + /** {@code lua_yield}:不带续体的形态,{@code coroutine.yield} 用它 */ + public void yield(int nresults) { + yieldk(nresults); + } + + /** + * {@code luaD_callnoyield}:从宿主调进 Lua,但期间不许 yield。 + *

+ * {@code table.sort} 的比较函数、{@code string.gsub} 的替换函数这类回调都 + * 走这里——C 版同样用 {@code nyci} 把「不可 yield」这一层记上,所以回调里 + * yield 会报跨 C 调用边界,与参考实现一致。 + */ + public void callNoYield(int funcIdx, int nresults) { + nNy++; + try { + call(funcIdx, nresults); + } finally { + nNy--; + } + } + + /** + * {@code lua_resume}。 + *

+ * 调用前,给协程的参数必须已经由 {@link #xmove} 压到本线程栈上。 + * + * @param nargs 压上来的参数个数 + * @param nresultsOut 长度 1 的数组,用来带出「该从协程栈上搬走几个值」 + * @return {@link #LUA_OK} / {@link #LUA_YIELD} / 错误码 + */ + public int resume(LuaState from, int nargs, int[] nresultsOut) { + if (status == LUA_OK) { + if (ci != baseCi) { + return resumeError("cannot resume non-suspended coroutine", nargs); + } else if (top - (ci.func + 1) == nargs) { + // base_ci.func 之上只剩参数 —— 说明连函数都没了,协程已经跑完 + return resumeError("cannot resume dead coroutine", nargs); + } + } else if (status != LUA_YIELD) { + return resumeError("cannot resume dead coroutine", nargs); + } + /* + * C 版这里写 {@code L->nCcalls = getCcalls(from)}。 + *

+ * 必须继承调用深度,不能清零。清了之后一层套一层的协程就永远 + * 到不了 {@code LUAI_MAXCCALLS}:{@code coroutine.wrap(f)()} 的 + * {@code f} 里再 wrap 再调,深度每进一层就被重置回 0,于是无限递归。 + * 上游 {@code cstack.lua}「nesting of resuming yielded coroutines」 + * 与 {@code coroutine.lua} 里那段 wrap 递归专测这一条。 + *

+ * 「不可 yield」的 {@code nNy} 则不继承:C 版 {@code getCcalls} + * 只取低 16 位,高位清零。所以每个协程能不能 yield 是独立算的—— + * 在 {@code table.sort} 的回调里 resume 另一个协程,那个协程照样能 + * yield(实测如此)。 + */ + nNy = 0; + nCcalls = (from == null) ? 0 : from.nCcalls; + if (nCcalls >= MAXCCALLS) { + return resumeError("C stack overflow", nargs); + } + nCcalls++; + int st; + try { + doResume(nargs); + st = LUA_OK; + } catch (LuaYield y) { + st = LUA_YIELD; + } catch (LuaError e) { + /* + * luaD_seterrorobj:错误对象必须落到栈顶,后面 + * finishPcallK / auxresume 都是从 top-1 取它的。 + *

+ * 顺带把 {@link #closeErrPending} 清掉:这个错误是刚抛出来的, + * 比「上一趟关变量时被 yield 打断时暂存的那个」新。C 版不存在这个 + * 取舍——它永远取 {@code L->top - 1},而 {@code luaG_errormsg} 早把 + * 新错误写到那儿了。上游 {@code locals.lua} 里 {@code y} 的 + * {@code __close} 先 yield 再 {@code error(err + 20)},下一个变量 + * 看到的必须是 30 而不是 10。 + */ + push(errObjOf(e)); + closeErrPending = null; + st = LUA_ERRRUN; + } + st = precover(st); + if (st != LUA_OK && st != LUA_YIELD) { + status = st; // 标记线程死亡,之后 resume 一律「cannot resume dead coroutine」 + } + nresultsOut[0] = (st == LUA_YIELD) ? ci.nyield + : (top - (ci.func + 1)); + return st; + } + + private int resumeError(String msg, int narg) { + top -= narg; // 把参数从栈上摘掉 + push(LuaValue.ofString(LuaString.latin1(msg))); + return LUA_ERRRUN; + } + + /** {@code ldo.c: resume} */ + private void doResume(int nargs) { + if (status == LUA_OK) { + /* + * 刚起一个协程:直接调它的主体。 + *

+ * C 版是 {@code ccall(L, firstArg - 1, LUA_MULTRET, 0)}—— + * {@code inc} 传的是 0,因为 {@code lua_resume} 进来时已经 + * 做过 {@code L->nCcalls++}。这里再用 {@link #call} 会再加一层, + * 于是嵌套 resume 的深度变成每级 +2,{@code LUAI_MAXCCALLS} 撑得住 + * 的层数正好减半:上游 {@code cstack.lua} 那几处「final count」 + * 会从 197 掉到 98。 + */ + CallInfo c = precall(top - nargs - 1, MULTRET); + if (c != null) { + c.fresh = true; + Vm.execute(this, c); + } + return; + } + status = LUA_OK; // 标记「又在跑了」 + CallInfo c = ci; + if (c.isLua) { + // 只可能发生在 hook 里 yield,我们没有 hook + throw new IllegalStateException("yield inside a hook is not supported"); + } + /* + * 关键一步:resume 传进来的参数,就是 yield 的返回值。 + * 所以这里用 nargs(而不是 yield 出去的个数)去做 poscall—— + * 实测 {@code coroutine.yield(1)} 之后 {@code resume(co, 99)}, + * 协程里拿到的就是 99。 + */ + int n = nargs; + if (c.k != null) { + n = c.k.call(this, LUA_YIELD, c.ctx); + } + poscall(c, n); + unroll(); + } + + /** {@code ldo.c: unroll}:把被 yield 打断的调用链一路跑到底 */ + private void unroll() { + while (ci != baseCi) { + CallInfo c = ci; + if (!c.isLua) { + finishCcall(c); + } else { + Vm.finishOp(this); // 补完被打断的那条指令 + Vm.execute(this, c); // 一直跑到「fresh 帧返回」为止 + } + } + } + + /** {@code ldo.c: finishCcall}:补完一个被 yield 打断的 C 帧 */ + private void finishCcall(CallInfo c) { + int n; + if (c.clsret) { + n = c.nres; // 只是在关 tbc 变量时被中断,重做 poscall 即可 + } else { + int st = LUA_YIELD; // 没有别的错误时就是「被 yield 打断」 + if (c.yieldPcall) { + st = finishPcallK(c); + } + // adjustresults(L, LUA_MULTRET):只抬高 ci.top,不动 L.top + if (c.top < top) { + c.top = top; + } + n = c.k.call(this, st, c.ctx); + } + poscall(c, n); + } + + /** + * {@code ldo.c: finishpcallk}:补完一个被 yield 打断的 {@code pcall}。 + *

+ * 有两种「被打断」:真的 yield 了({@code recst == LUA_OK}),或者恢复之后 + * 被保护的函数自己出错({@code recst} 记着那个错误状态)。后者要在这里把 + * tbc 变量关掉、把错误对象放到位——和 {@code luaD_pcall} 出错时做的一样。 + */ + private int finishPcallK(CallInfo c) { + int st = c.recst; + if (st != LUA_OK) { + int fIdx = c.funcIdx; // 被保护的那个函数的栈下标 + /* + * 正常情况同 C 版 {@code luaD_seterrorobj} 从 {@code top - 1} 取。 + * 但如果上一趟是在 {@code __close} 里 yield 出去的,栈上的 {@code top} + * 已经被「搬走 yield 值」动过,那里不再是错误对象——取 + * {@link #closeErrPending} 里暂存的那个。 + */ + LuaValue errObj = (closeErrPending != null) + ? closeErrPending + : new LuaValue(stack[top - 1]); + /* + * 消息处理函数在关 {@code __close} 之前跑,不是之后。 + *

+ * C 版是在抛出点({@code luaG_errormsg})调的,那会儿 + * {@code __close} 还没跑;{@code finishpcallk} 里那句 + * {@code L->errfunc = old_errfunc} 也是在 {@code luaF_close} + * 之后才恢复的。放在关完之后跑的话,{@code __close} 收到的 + * 就是没处理过的原始错,而且顺序也反了。 + *

+ * 实测(协程里 {@code xpcall},函数体先 yield 再 {@code error}, + * 且带一个 {@code }): + *

+             * 参考:msgh h2 after | close a err=H-h2
+             * 本实现(旧):close a err=after | msgh h2 after
+             * 
+ */ + boolean alreadyHandled = closeErrPending != null; + closeErrPending = null; + /* + * {@link #closeErrPending} 不为 null 意味着这一趟是「{@code __close} + * 里 yield 出去、现在 resume 回来接着关」,而那个错误对象在 yield + * 之前就已经过过处理函数了({@code closeUpvalsOnUnwind} 的调用方 + * 先 {@code applyErrMsg} 再关)。这里再过一次就会多调一次处理函数—— + * 实测参考实现在那条路径上一次都不调。 + *

+ * 处理函数必须在把 {@code top} 压回 {@code fIdx + 1} 之前跑。 + * {@code applyHandler} 是照 {@code luaG_errormsg} 在出错那一刻的 + * 栈顶压调用帧的,那会儿被保护函数的局部变量都还在。先把 + * {@code top} 收到 {@code fIdx + 1} 再跑,它就从 {@code fIdx + 2} + * 开始写——而那正是第一个局部变量(也就是那个 {@code } 变量) + * 的槽,于是 {@code __close} 被覆盖成消息处理函数自己,报 + * 「attempt to call a nil value」。 + */ + if (c.errHandler != null && !alreadyHandled) { + errObj = applyHandler(c.errHandler, errObj, fIdx); + } + top = fIdx + 1; + errObj = closeUpvalsFrom(fIdx, errObj, c.errHandler); + stack[fIdx].set(errObj); + top = fIdx + 1; + c.recst = LUA_OK; + shrinkStack(); // 与 ldo.c: finishpcallk 末尾那句同位置 + } else { + st = LUA_YIELD; // 不是出错,是 yield + } + c.yieldPcall = false; + return st; + } + + /** + * 关掉 {@code level} 以上的 upvalue 并跑 {@code __close}。 + *

+ * 与 {@link #closeUpvalsOnUnwind} 同构,同样要处理「{@code __close} 自己又 + * 出错,新错误顶掉旧错误」——C 版 {@code luaF_close} 一次只关一批, + * 出错就抛,由 {@code luaD_closeprotected} 循环接着关剩下的。 + * + * @param handler 消息处理函数;{@code __close} 抛的错要过它(理由同 + * {@link #closeUpvalsOnUnwind} 里那段:{@code finishpcallk} + * 也是在 {@code luaF_close} 之后才恢复 + * {@code L->errfunc},所以关闭期间它仍然生效) + */ + private LuaValue closeUpvalsFrom(int level, LuaValue errObj, LuaValue handler) { + while (true) { + if (openUpvals.isEmpty()) { + return errObj; + } + int before = openUpvals.size(); + try { + /* + * {@code ldo.c: finishpcallk} 里那句是 + * {@code luaF_close(L, func, status, 1)} —— {@code yy} 恒为 1。 + *

+ * 不能按「当前帧是不是 Lua 帧」判:走到这里时 {@code ci} 是被打断的 + * {@code pcall} 那个 C 帧,照 {@code luaT_callTM} 那套判据 + * 会退化成 {@code luaD_callnoyield},于是 {@code __close} 里 + * {@code coroutine.yield} 报「attempt to yield across a C-call + * boundary」。上游 {@code locals.lua}「yielding inside closing + * metamethods」那一段正测这个:三个 {@code } 变量, + * 中间那个在 {@code __close} 里 yield 完再 {@code error}, + * 剩下那个要能接着 yield。 + */ + closeUpvals(level, errObj, true); + return errObj; + } catch (LuaYield y) { + /* + * 还有没关完的:yield 出去之后 {@link #finishPcallK} 会再进来 + * 一趟({@code recst} 还是错误状态),错误对象得留着。 + */ + closeErrPending = errObj; + throw y; + } catch (LuaError e2) { + LuaValue v = e2.value; + if (e2.errMem) { + // 内存错误:不过消息处理函数,也不补位置(见 closeUpvalsOnUnwind) + } else if (e2.where == null && v != null && v.isString()) { + LuaString prefix = where(); // 与 closeUpvalsOnUnwind 同形 + v = LuaValue.ofString(prefix.concat(v.asString())); + } + if (handler != null && !e2.errMem) { + v = applyErrMsg(v, handler); + } + errObj = v; + if (openUpvals.size() >= before) { + return errObj; // 一个也没关掉,再试也不会有进展 + } + } + } + } + + /** + * 跑 {@code xpcall} 的消息处理函数,返回值顶掉原来的错误对象。 + *

+ * 与 {@link #pcall(int, int, LuaValue)} 里那段同形,只是错误对象来自 + * 协程恢复路径而不是 {@code catch}。 + */ + private LuaValue applyHandler(LuaValue handler, LuaValue err, int fIdx) { + int hf = top; + checkStack(2); + stack[hf].set(handler); + stack[hf + 1].set(err); + top = hf + 2; + LuaValue bad = pcall(hf, 1); // 处理函数自身出错不再兜底 + if (bad != null) { + top = fIdx + 1; + return bad; + } + LuaValue res = new LuaValue(stack[top - 1]); + top = fIdx + 1; + return res; + } + + /** + * {@code ldo.c: precover}:出错时一层层往回找恢复点。 + *

+ * 只要还能找到一个被打断的 {@code pcall},就把调用链退到那一层, + * 记下错误状态,再跑一次 {@link #unroll}——于是错误在这层 {@code pcall} + * 上被「捕获」,协程继续跑。找不到恢复点才是真的错误,线程就此死亡。 + */ + private int precover(int st) { + while (st != LUA_OK && st != LUA_YIELD) { + CallInfo c = findPcall(); + if (c == null) { + return st; + } + ci = c; + c.recst = st; + try { + unroll(); + st = LUA_OK; + } catch (LuaYield y) { + st = LUA_YIELD; + } catch (LuaError e) { + push(errObjOf(e)); + closeErrPending = null; // 同上:新错误顶掉 yield 时暂存的那个 + st = LUA_ERRRUN; + } + } + return st; + } + + /** {@code ldo.c: findpcall} */ + private CallInfo findPcall() { + for (CallInfo c = ci; c != null; c = c.previous) { + if (c.yieldPcall) { + return c; + } + } + return null; + } + + /** + * {@code lua_closethread}:关掉协程里所有待关闭({@code })的变量。 + *

+ * {@code coroutine.close} 用它。已死亡的协程没有可关的东西,直接报原状态。 + */ + public int closeThread(LuaState from) { + return (closeThreadValue(from) == null) ? LUA_OK : LUA_ERRRUN; + } + + /** + * {@code lua_closethread}:关掉协程里所有待关闭({@code })的变量, + * 并把协程置为死亡。 + *

+ * 已经因为出错而死的协程没有可关的东西,直接返回 {@code null}——这一点很 + * 要紧:{@code coroutine.wrap} 在错误分支里调它,如果这里还「搬一次错误 + * 对象」,搬走的会是栈上的陈值,把真正的错误消息顶掉。 + * + * @return {@code __close} 抛出的错误对象(已压在本线程栈顶,供 + * {@link #xmove} 搬走);没有错误返回 {@code null} + */ + public LuaValue closeThreadValue(LuaState from) { + return closeThreadValue(from, + (top > 0) ? new LuaValue(stack[top - 1]) : LuaValue.ofNil()); + } + + /** + * 带「原错误对象」的重载。 + *

+ * {@code coroutine.wrap} 必须走这一个:它在调 {@code lua_closethread} + * 之前已经用 {@code lua_xmove(co, L, 1)} 把错误对象搬走了,协程栈上 + * 的 {@code top} 已经降了一格,{@code stack[top - 1]} 不再是错误对象—— + * 拿到的是排序比较用的 {@code 1} 之类的陈值。 + *

+ * C 版不会踩到这个坑,因为 {@code luaD_seterrorobj} 的 {@code default} + * 分支是 {@code setobjs2s(L, oldtop, L->top - 1)}:错误对象在栈上是 + * 连着两格的,搬走上面那格之后下面那格还是它。本实现的错误对象 + * 住在异常里、不在栈上,所以只有一格,只能由调用方把它带进来。 + */ + public LuaValue closeThreadValue(LuaState from, LuaValue prevErr) { + /* + * {@code lua_closethread} 的第一句就是 + * {@code L->nCcalls = (from) ? getCcalls(from) : 0}。 + *

+ * 这行是链式 close 唯一的深度记账:{@code __close} 里再 + * {@code coroutine.close} 上一个协程时,深度靠它往上传一层。漏了, + * 一千个互相 close 的协程就会真的递归一千层——上游 + * {@code cstack.lua}「chain of 'coroutine.close'」断言的正是 + * {@code C stack overflow},不是 Unlimited Recursion。 + */ + nCcalls = (from == null) ? 0 : from.nCcalls; + /* + * {@code lstate.c: luaE_resetthread}。 + *

+ * 两个反直觉的地方,都实测于参考实现(上游 {@code coroutine.lua} + * 「closing a coroutine after an error」那一段): + *

    + *
  • 已经因错而死的协程也要走这一步,而且要把原来的错误对象 + * 原样带回来。{@code coroutine.close(co)} 在协程因 + * {@code error(100)} 死亡之后返回 {@code false, 100}—— + * 之前这里直接返回 null(「已死亡,没有可关的」),于是返回 + * {@code true},断言就炸了。原来的错误对象在栈顶,是 + * {@code lua_resume} 出错时用 {@code luaD_seterrorobj} 放上去的。
  • + *
  • {@code status} 要先置成 {@code LUA_OK} 再关,因为 + * {@code __close} 本身要在「线程还活着」的前提下跑;而且关完之后 + * 不恢复成原来的错误码。所以紧接着第二次 + * {@code coroutine.close(co)} 返回的是 {@code true}。
  • + *
+ */ + int st = status; + if (st == LUA_YIELD) { + st = LUA_OK; + } + LuaValue errObj = (st == LUA_OK) ? LuaValue.ofNil() : new LuaValue(prevErr); + status = LUA_OK; // 让 __close 能跑 + LuaValue err = null; + /* + * {@code luaD_closeprotected}:一个 {@code __close} 抛了错,那个错误对象 + * 要接着传给下一个 {@code __close},而不是就地放弃。实测参考实现: + * 两个 tbc 变量,内层用 {@code error(111)}、外层看到 + * {@code err == 111} 后再 {@code error(200)},最终 + * {@code coroutine.close} 返回 {@code false, 200}——只关一个就返回的话 + * 拿到的是 {@code false, 111},而且外层那个 {@code __close} 要等第二次 + * {@code close} 才跑(上游 {@code coroutine.lua} 就测这一条)。 + */ + while (true) { + try { + closeUpvals(1, errObj); // 从第 1 格起全部关掉 + break; + } catch (LuaError e) { + errObj = e.value; // 下一个 __close 看到的 err + err = e.value; // 也是最终要交出去的那个 + st = LUA_ERRRUN; + ci = baseCi; // 与 C 版「恢复 old_ci 再重来」一致 + } + } + ci = baseCi; + ensureSlots(2); + top = 1; + if (st != LUA_OK) { + stack[1].set((err != null) ? err : errObj); // luaD_seterrorobj + top = 2; + } + baseCi.top = top + MINSTACK; + status = LUA_OK; // 关完之后一律「正常」——第二次 close 才会返回 true + return (st == LUA_OK) ? null : new LuaValue(stack[1]); + } + + /* ==================== 加载与执行 ==================== */ + + /** + * 编译一段源码,返回主 chunk 的闭包。 + *

+ * 对应 {@code lapi.c: lua_load}:闭包的第 0 号 upvalue 是关闭的,里面放全局表。 + */ + public LClosure load(byte[] source, String chunkName) { + return load(new Zio(source), LuaString.utf8(chunkName).bytes); + } + + /** + * 从一条(可能是惰性的)输入流编译。 + * + * @see Zio#Source + */ + public LClosure load(Zio z, String chunkName) { + return load(z, LuaString.utf8(chunkName).bytes); + } + + /** 源名是 Lua 字符串(字节串)时的重载,避免 UTF-8 往返带来的失真 */ + public LClosure load(Zio z, byte[] chunkName) { + return loadDispatch(z, chunkName, z.getc()); + } + + /** + * {@code ldo.c: f_parser} 那一步分派:首字节是 {@code LUA_SIGNATURE} 的 + * 第一个字节就走 {@code luaU_undump},否则才进词法/语法分析。 + *

+ * 这一层之前只存在于 {@link BaseLib} 的 {@code load} / {@code loadfile} 里, + * 而 {@code lua_load} 本身(也就是 {@link #load})不看签名——于是 + * {@code require} 一个预编译模块会失败:{@code PackageLib} 走的是 + * {@link #loadFile},报的是 + * {@code unexpected symbol near '<\27>'}(把签名当源码读了)。 + * 参考实现里 {@code require} 预编译 chunk 是正常路径—— + * {@code luac -o mod.luac} 之后直接 {@code require "mod"} 是常见分发方式。 + *

+ * 二进制分支不回退首字节:C 的 {@code luaU_undump} 注释写得很直白, + * 「skip 1st char (already read and checked)」,{@code checkHeader} 从第二个 + * 字节开始比。 + */ + private LClosure loadDispatch(Zio z, byte[] chunkName, int firstChar) { + /* + * chunkname 在 C 里是 const char*,存进 Proto 之前经 luaS_new(strlen)-- + * 于是第一个 \0 之后的内容根本不会成为源名。实测 + * load("error('E')", "a\0b") 报的是 [string "a"]:1: E。 + */ + byte[] name = nulTruncate(chunkName); + if (firstChar == Lua.LUA_SIGNATURE0) { + return undump(z, name); + } + return load(z, name, firstChar); + } + + /** 截到第一个 {@code \0}({@code const char*} 语义),没有 {@code \0} 就原样返回 */ + private static byte[] nulTruncate(byte[] b) { + for (int i = 0; i < b.length; i++) { + if (b[i] == 0) { + byte[] r = new byte[i]; + System.arraycopy(b, 0, r, 0, i); + return r; + } + } + return b; + } + + /** + * {@code firstChar} 是已经读出来的首字节({@code f_parser} 的形态)。 + * + * @see Lexer#Lexer(Zio, byte[], StringPool, int) + */ + public LClosure load(Zio z, byte[] chunkName, int firstChar) { + chunkName = nulTruncate(chunkName); + this.chunkName = chunkName; + /* + * luaD_protectedparser 那层保护:出错时 nCcalls 要整体回滚 + * (C 是 luaD_rawrunprotected 里的 {@code L->nCcalls = oldnCcalls})。 + *

+ * 它必须落在这一层而不是 loadDispatch:解析器的 enterlevel 借的 + * 是 nCcalls,语法错误在 leavelevel 之前就抛出来,不平衡的层数会永久 + * 留在计数器上。实测:{@code load("x = =")} 失败一次漏两层,上游 + * {@code errors.lua} 里几十次失败解析之后,深度配额从 197 掉到 95, + * 于是「error in error handling」那段提前触发,整个文件假红。 + */ + int saved = nCcalls; + try { + Proto p = Parser.parse(z, chunkName, strings, firstChar, this); + LClosure cl = new LClosure(p, 1); + cl.initUpvals(); + cl.upvals[0] = UpVal.closed(LuaValue.ofTable(globals)); + return cl; + } finally { + nCcalls = saved; + } + } + + /** 二进制 chunk:走 {@code luaU_undump} 而不是词法/语法分析 */ + public LClosure undump(Zio z, byte[] name) { + this.chunkName = name; + return Undump.undump(this, z, name); + } + + /** + * {@code luaL_loadfilex}:编译一个脚本文件。 + *

+ * 与 {@link #load} 的差别是会跳过 BOM 与 {@code #!} 行(那属于加载层, + * 不属于词法层)。行号必须靠补一个 {@code \n} 保住。 + */ + public LClosure loadFile(byte[] source, String chunkName) { + byte[] name = LuaString.utf8(chunkName).bytes; + this.chunkName = name; + /* + * 先过 lauxlib 那一步(BOM / #!),再按签名分派。 + * 顺序不能反:预编译 chunk 的第一个字节是签名,不是 '#', + * skipFilePreamble 不会动它;反过来若先分派就吃不到 BOM 了。 + */ + Zio z = new Zio(Parser.skipFilePreamble(source)); + return loadDispatch(z, name, z.getc()); + } + + /** 编译并执行一段源码 */ + public void run(String source) { + run(LuaString.latin1(source).bytes, "=chunk"); + } + + /** 编译并执行,等价于 {@code luaL_dostring} */ + public void run(byte[] source, String chunkName) { + int base = top; + pushClosure(load(source, chunkName)); + call(base, 0); + } + + /** 带保护地执行;返回错误对象或 {@code null} */ + public LuaValue runProtected(byte[] source, String chunkName) { + int base = top; + pushClosure(load(source, chunkName)); + return pcall(base, 0); + } + + /* ==================== 错误 ==================== */ + + /** + * {@code luaG_runerror}。 + *

+ * 位置前缀直接拼进错误值,不是另存一份元数据——C 版就是这样: + * {@code luaG_addinfo} 改写的是栈顶那个消息串。{@link LuaError#where} + * 只当「已经加过位置」的标记用,避免 {@link #pcall} 再加一次。 + */ + public LuaError runError(String msg) { + LuaString prefix = where(); + LuaString full = prefix.concat(LuaString.latin1(msg)); + return new LuaError(LuaValue.ofString(full), prefix.toJavaString()); + } + + /** {@code luaG_typeerror}(含 {@code varinfo} 的变量名后缀) */ + public LuaError typeError(LuaValue v, String op) { + return runError("attempt to " + op + " a " + LuaTm.objTypeName(this, v) + + " value" + varInfo(v)); + } + + /** + * {@code luaG_callerror}:名字先从「怎么调的」找,找不到才退回 {@link #varInfo}。 + *

+ * 两条路都试是必要的:{@code t:nope()} 走 {@code funcnamefromcall} + * ({@code OP_CALL} → {@code OP_SELF} → "method 'nope'"),而 + * {@code local f = nil; f()} 这类只能靠 {@code varinfo} 认出局部变量。 + * 只用其中一条就会在另一类场景丢掉 {@code (method 'xxx')} 后缀。 + */ + public LuaError callError(LuaValue v) { + String[] name = new String[1]; + String kind = funcNameFromCall(name); + String extra = (kind != null) ? formatVarInfo(kind, name[0]) : varInfo(v); + return runError("attempt to call a " + LuaTm.objTypeName(this, v) + " value" + extra); + } + + /** {@code ldebug.c: funcnamefromcall} */ + private String funcNameFromCall(String[] name) { + CallInfo c = ci; + if (c == null || c.func >= stack.length) { + return null; + } + // CIST_FIN:正在跑 __gc,报成「metamethod '__gc'」 + if (c.fin) { + name[0] = "__gc"; + return "metamethod"; + } + if (!c.isLua) { + return null; + } + LuaValue f = stack[c.func]; + if (f.tt != Lua.VLIF) { + return null; + } + return funcNameFromCode(((LClosure) f.o).p, + (c.savedpc > 0) ? c.savedpc - 1 : 0, name); + } + + /** + * {@code ldebug.c: funcnamefromcode}:按调用点那条指令反推它在调什么。 + *

+ * 元方法那一批共用一个名字来源:{@code tmname[tm]} 去掉开头的 {@code __} + * (C 版就是 {@code getshrstr(tmname[tm]) + 2})。 + */ + static String funcNameFromCode(Proto p, int pc, String[] name) { + if (pc < 0 || pc >= p.code.length) { + return null; + } + int i = p.code[pc]; + int tm = -1; + switch (OpCode.getOpCode(i)) { + case OpCode.OP_CALL: + case OpCode.OP_TAILCALL: + return getObjName(p, pc, OpCode.getArgA(i), name); + case OpCode.OP_TFORCALL: + name[0] = "for iterator"; + return "for iterator"; + case OpCode.OP_SELF: + case OpCode.OP_GETTABUP: + case OpCode.OP_GETTABLE: + case OpCode.OP_GETI: + case OpCode.OP_GETFIELD: + tm = Tm.TM_INDEX; + break; + case OpCode.OP_SETTABUP: + case OpCode.OP_SETTABLE: + case OpCode.OP_SETI: + case OpCode.OP_SETFIELD: + tm = Tm.TM_NEWINDEX; + break; + case OpCode.OP_MMBIN: + case OpCode.OP_MMBINI: + case OpCode.OP_MMBINK: + tm = OpCode.getArgC(i); + break; + case OpCode.OP_UNM: + tm = Tm.TM_UNM; + break; + case OpCode.OP_BNOT: + tm = Tm.TM_BNOT; + break; + case OpCode.OP_LEN: + tm = Tm.TM_LEN; + break; + case OpCode.OP_CONCAT: + tm = Tm.TM_CONCAT; + break; + case OpCode.OP_EQ: + tm = Tm.TM_EQ; + break; + case OpCode.OP_LT: + case OpCode.OP_LTI: + case OpCode.OP_GTI: + tm = Tm.TM_LT; + break; + case OpCode.OP_LE: + case OpCode.OP_LEI: + case OpCode.OP_GEI: + tm = Tm.TM_LE; + break; + case OpCode.OP_CLOSE: + case OpCode.OP_RETURN: + case OpCode.OP_RETURN0: + case OpCode.OP_RETURN1: + tm = Tm.TM_CLOSE; + break; + default: + return null; + } + String ev = (tm >= 0 && tm < Tm.EVENT_NAMES.length) ? Tm.EVENT_NAMES[tm] : null; + name[0] = (ev != null && ev.startsWith("__")) ? ev.substring(2) : "?"; + return "metamethod"; + } + + /** + * {@code luaG_opinterror}:算术/位运算的类型错误报坏掉的那一个。 + *

+ * 判据是「第一个操作数不是数字就报它」——所以 {@code 1 + nil} 与 + * {@code nil + 1} 都得到「attempt to perform arithmetic on a nil value」, + * 而不是把两个类型都打进去。 + */ + public LuaError opInterror(LuaValue p1, LuaValue p2, String op) { + return typeError(p1.isNumber() ? p2 : p1, op); + } + + /** + * {@code luaG_ordererror}。 + *

+ * 两个类型名都取自 {@link LuaTm#objTypeName}(C 版的 + * {@code luaT_objtypename}),也就是先看 {@code __name}。 + * 用 {@code v.typeName()} 会让 {@code XX < io.stdin} 报成 + * {@code nil with userdata},而参考实现给 {@code nil with FILE*}。 + */ + public LuaError orderError(LuaValue p1, LuaValue p2) { + String t1 = LuaTm.objTypeName(this, p1); + String t2 = LuaTm.objTypeName(this, p2); + if (t1.equals(t2)) { + return runError("attempt to compare two " + t1 + " values"); + } + return runError("attempt to compare " + t1 + " with " + t2); + } + + /** + * 抛出一个带位置信息的错误,对应 {@code error(msg, level)} 与 + * {@code assert} 的失败路径。 + *

+ * 返回值写成异常是为了调用点能直接 {@code throw L.errorAt(...)}。 + * {@link #where} 非空是这个错误「已经带位置」的标记,{@link #pcall} 据此 + * 决定要不要再补一次——补两次会得到 {@code file:1: file:1: msg} 这种叠前缀。 + * + * @param level 位置取自第几层调用者({@code 1} = 直接调用者); + * {@code <= 0} 表示不加位置 + */ + public LuaError errorAt(LuaValue msg, int level) { + /* + * C 版 {@code lua_error} 的内存错误特判就在这条路上: + * {@code error()} 与 {@code luaL_error} 最终都是 {@code lua_error(L)}, + * 而它判的是栈顶那个值是不是"not enough memory"这个驻留短串。 + * 位置前缀拼好之后再判,所以 {@code error("not enough memory")} + * (默认 level 1)拼上了前缀、反而退化成普通错误。 + */ + if (level > 0 && msg.isString()) { + LuaString prefix = whereAt(level); + LuaValue full = LuaValue.ofString(prefix.concat(msg.asString())); + LuaError e = new LuaError(full, prefix.toJavaString()); + e.errMem = isMemErr(full); + return e; + } + LuaError e = new LuaError(msg, ""); + e.errMem = isMemErr(msg); + return e; + } + + /** + * {@code lapi.c: lua_error} 那句 + * {@code eqshrstr(tsvalue(errobj), G(L)->memerrmsg)}。 + *

+ * C 比的是短串指针——短串全局驻留,所以「内容相等」与「同一个对象」 + * 等价。这里按内容比:{@code "not enough memory"} 只有 17 字节,恒为短串, + * 不会有「内容相同却是长串」的偏差;而按内容比还能接住那些没过驻留池就 + * 造出来的消息串。 + */ + static boolean isMemErr(LuaValue v) { + return v != null && v.isString() + && Arrays.equals(v.asString().bytes, MEM_ERR_MSG); + } + + /** {@code lstring.h: MEMERRMSG} */ + private static final byte[] MEM_ERR_MSG = "not enough memory".getBytes( + java.nio.charset.StandardCharsets.ISO_8859_1); + + /** + * {@code luaL_error} / {@code luaL_argerror}:库函数抛的错误。 + *

+ * 位置取自 {@code luaL_where(L, 1)}(回跳一层到调用者),不是 + * {@link #where}:库函数执行时 {@code ci} 是 C 帧,{@link #where} 只会 + * 给出空串,报出来的消息就没有 {@code src:line:} 前缀了。两类错误的取位 + * 方式不同,是 {@code luaG_runerror} 与 {@code luaL_error} 本身就不同—— + * 合成一个会在某一边丢掉行号。 + */ + public LuaError libError(String msg) { + return errorAt(LuaValue.ofString(LuaString.latin1(msg)), 1); + } + + /** + * 与 {@link #libError(String)} 的差别:消息已经是字节串。 + *

+ * 消息里嵌了 Lua 数据(文件名、模块名、{@code __name})时走这条——那些是 + * 任意字节,绕一圈 Java String 再 {@code latin1} 编码回去不是内容被改写 + * 就是直接抛 {@code IllegalArgumentException}。 + */ + public LuaError libError(LuaString msg) { + return errorAt(LuaValue.ofString(msg), 1); + } + + /* ==================== lauxlib.c 那套「参数检查」 ==================== */ + + /** + * 取第 {@code arg} 个参数(1 起,与 {@code luaL_*} 一致)。 + *

+ * 越界时返回一个 {@code LUA_TNONE} 的值而不是栈槽里的残值:C 里 + * {@code lua_type} 对越界下标返回 {@code LUA_TNONE},报出来是 + * 「no value」,而栈槽里往往还是 nil,报出来就成了「nil」—— + * 两者是不同的错误文本,混掉就会在 {@code pcall(f)} 这类用例上对不上。 + */ + LuaValue arg1(int arg) { + int idx = ci.func + arg; // 1 起:arg=1 → func+1 是第一个参数 + if (idx >= top) { + return LuaValue.ofNil(); // 仅用于取值,配合下面的 isNone 判断 + } + return stack[idx]; + } + + /** {@code lua_isnone}:第 {@code arg} 个参数(1 起)根本没传? */ + boolean isNoneArg(int arg) { + return ci.func + arg >= top; + } + + /** {@code luaL_typename}:越界是 {@code no value} */ + String argTypeName(int arg) { + return isNoneArg(arg) ? "no value" : stack[ci.func + arg].typeName(); + } + + /** + * {@code lua_type}(1 起参数下标):越界是 {@link Lua#TNONE} 而不是 nil。 + *

+ * 专供 {@code luaL_argexpected} 那类「条件是若干类型之一」的判断: + * 缺参必须落进不满足那一支,否则 {@code setmetatable({})} 会因为 + * 「nil 也算合法」而静默通过 —— 参考实现报 + * {@code bad argument #2 to 'setmetatable' (nil or table expected, got no value)}。 + */ + int argBaseType(int arg) { + return isNoneArg(arg) ? Lua.TNONE : stack[ci.func + arg].baseType(); + } + + /** + * {@code lauxlib.c: luaL_typeerror}:{@code " expected, got "}。 + *

+ * {@code __name} 元字段优先于真实类型名——这是 userdata 报 + * 「MyType expected」的机制,照抄。 + */ + public LuaError typeArgError(int arg, String tname) { + String typeArg; + if (!isNoneArg(arg)) { + LuaValue v = stack[ci.func + arg]; + /* + * 走 LuaTm.objTypeName,不要在这里自己取 __name:C 这边 + * luaL_typeerror 用的就是 luaT_objtypename,于是「只有表与 userdata + * 认 __name」和「__name 是 C 字符串」两条规则在这里同样成立。 + * 自己取一遍就是同一份逻辑的第二处拷贝 —— 而第二处拷贝已经漏过一次了。 + */ + if (v.tt == Lua.TLIGHTUSERDATA) { + /* + * {@code lauxlib.c: luaL_typeerror} 对轻量 userdata 有 + * 专门的措辞。{@code luaT_typenames_} 里它和完整 userdata + * 共用 {@code "userdata"},只有报错走的这一条会区分—— + * 实测 {@code debug.setuservalue(lightud, {})} 报 + * {@code got light userdata},上游 {@code errors.lua} 断言这句话。 + */ + typeArg = "light userdata"; + } else { + typeArg = LuaTm.objTypeName(this, v); + } + } else { + typeArg = "no value"; + } + return argError(arg, tname + " expected, got " + typeArg); + } + + /** + * {@code lauxlib.c: luaL_argerror}。 + *

+ * 函数名有三个来源,顺序不能改: + *

    + *
  1. 调用点({@code ldebug.c: getfuncname}):从调用者那条 + * {@code OP_CALL} 反推。所以 {@code local f = function() string.len(nil) end} + * 报的是 {@code 'len'},与 {@code pcall(string.len)} 报的 + * {@code 'string.len'} 不一样——后者拿不到调用点(调用者是 C 帧)。
  2. + *
  3. {@code _LOADED} 反查({@code pushglobalfuncname}):在 + * {@code package.loaded} 里找两层,找到 {@code string.len}。
  4. + *
  5. 都没有就给 {@code ?}。
  6. + *
+ *

+ * 还有一处「减一」:以方法形式调用({@code s:rep(nil)})时 {@code self} + * 不算在参数编号里,所以 {@code arg--};减完是 0 说明错的就是 self 本身, + * 那时报 {@code "calling 'x' on bad self"} 而不是 {@code bad argument}。 + */ + public LuaError argError(int arg, String extra) { + String[] name = new String[1]; + String what = null; + if (ci != null && !ci.tail) { + what = callerFuncName(name); + } + if ("method".equals(what)) { + arg--; + if (arg == 0) { + return libError("calling '" + name[0] + "' on bad self (" + extra + ")"); + } + } + if (name[0] == null) { + name[0] = globalFuncName(); + if (name[0] == null) { + name[0] = "?"; + } + } + return libError("bad argument #" + arg + " to '" + name[0] + "' (" + extra + ")"); + } + + /** {@code ldebug.c: getfuncname}:从调用者那帧反推被调函数的名字 */ + private String callerFuncName(String[] name) { + CallInfo prev = (ci == null) ? null : ci.previous; + if (prev == null || !prev.isLua || prev.func >= stack.length) { + return null; + } + LuaValue f = stack[prev.func]; + if (f.tt != Lua.VLIF) { + return null; + } + return funcNameFromCode(((LClosure) f.o).p, (prev.savedpc > 0) ? prev.savedpc - 1 : 0, + name); + } + + /** + * {@code luaL_getmetafield}:直接读元表字段,不做元表的元表递归。 + *

+ * 与 {@link LuaTm#getTmByObj} 分得开是必要的:元方法查找要走 {@code __index} + * 链,而 {@code __name} 这种「描述性字段」只认自己那张表—— + * 递归进去会拿到别处的 {@code __name}。 + */ + LuaValue metafieldOf(LuaValue v, String name) { + LuaTable mt = LuaTm.metatableOf(this, v); + if (mt == null) { + return null; + } + LuaValue r = mt.get(LuaString.latin1(name)); + return (r == null || r.isNil()) ? null : r; + } + + /** {@code lauxlib.c: pushglobalfuncname} 的前半段:在 {@code _LOADED} 里找两层 */ + private String globalFuncName() { + if (ci == null || ci.func >= stack.length) { + return null; + } + String s = findFieldName(loaded, stack[ci.func], 2); + if (s == null) { + return null; + } + return s.startsWith("_G.") ? s.substring(3) : s; + } + + /** + * {@code lauxlib.c: findfield}。 + *

+ * 只认字符串键(C 版 {@code lua_type(L,-2) == LUA_TSTRING}),且只下钻两层: + * 这正是 {@code string.len} 能查到、{@code a.b.c} 查不到的原因。 + */ + static String findFieldName(LuaTable t, LuaValue obj, int level) { + if (level == 0) { + return null; + } + LuaValue key = LuaValue.ofNil(); + while (true) { + LuaValue[] r = t.next(key); + if (r == null) { + return null; + } + key = r[0]; + LuaValue k = r[0]; + LuaValue v = r[1]; + if (k.isString()) { + if (LuaValue.rawEqual(obj, v)) { + return LuaString.cutAtNul(k.asString().toLatin1String()); + } + if (v.isTable()) { + String sub = findFieldName(v.asTable(), obj, level - 1); + if (sub != null) { + return LuaString + .cutAtNul(k.asString().toLatin1String() + "." + sub); + } + } + } + } + } + + /** + * {@code luaG_concaterror}:拼接失败时报坏掉的那一个操作数。 + *

+ * 判据是「第一个操作数本身能当字符串用吗」——能,说明坏的是第二个; + * 不能,就报第一个。所以 {@code nil .. 'x'} 与 {@code 'x' .. nil} + * 都得到「attempt to concatenate a nil value」。 + */ + public LuaError concatError(LuaValue p1, LuaValue p2) { + LuaValue bad = (p1.isString() || p1.isNumber()) ? p2 : p1; + return typeError(bad, "concatenate"); + } + + /** + * {@code luaG_forerror}:消息里要带出错值的类型名。 + *

+ * 用 {@link LuaTm#objTypeName} 而不是 {@code v.typeName()}:C 版 + * {@code luaG_forerror} 调的是 {@code luaT_objtypename}, + * 会先看元表的 {@code __name}。实测 {@code for i = io.stdin, 10 do end} + * 在参考实现里报 {@code got FILE*},不是 {@code got userdata} + * ——上游 {@code errors.lua} 有一整组这样的断言。 + */ + public LuaError forError(LuaValue v, String what) { + return runError("bad 'for' " + what + " (number expected, got " + + LuaTm.objTypeName(this, v) + ")"); + } + + /** + * {@code ldebug.c: varinfo}:给出错误值所在变量的名字,形如 + * {@code " (local 'b')"}、{@code " (constant '10')"}、{@code " (method 'nope')"}。 + *

+ * 三段来源,顺序与 C 版一致:先认 upvalue,再认寄存器,寄存器还要反推指令流 + * ({@code findsetreg} + {@code getobjname})才能知道它是常量、字段还是方法。 + * 少了反推那一段,{@code '10' | 1} 就只能报「attempt to perform bitwise + * operation on a string value」,而参考实现报的是 + * 「… on a string value (constant '10')」——这类后缀在真实项目里天天出现。 + * + * @return {@code " (kind 'name')"} 或空串 + */ + public String varInfo(LuaValue o) { + CallInfo c = ci; + if (c == null || !c.isLua || c.func >= stack.length) { + return ""; + } + LuaValue f = stack[c.func]; + if (f.tt != Lua.VLIF) { + return ""; + } + LClosure cl = (LClosure) f.o; + Proto p = cl.p; + String[] name = new String[1]; + String kind = getUpvalName(cl, o, name); + if (kind == null) { + int reg = inStack(c, o); + if (reg >= 0) { + kind = getObjName(p, (c.savedpc > 0) ? c.savedpc - 1 : 0, reg, name); + } + } + return formatVarInfo(kind, name[0]); + } + + /** + * {@code luaG_findlocal}:当前 Lua 帧里第 {@code n} 号局部量的名字。 + *

+ * {@code checkclosemth} 用它给「non-closable value」报错带上变量名。 + * + * @param n 从 1 起(与 {@code luaF_getlocalname} 一致) + */ + public LuaString localNameAt(int n) { + CallInfo c = ci; + if (c == null || !c.isLua || c.func >= stack.length) { + return null; + } + LuaValue f = stack[c.func]; + if (f.tt != Lua.VLIF) { + return null; + } + return ((LClosure) f.o).p.getLocalName(n, (c.savedpc > 0) ? c.savedpc - 1 : 0); + } + + private static String formatVarInfo(String kind, String name) { + if (kind == null) { + return ""; + } + return " (" + kind + " '" + ((name == null) ? "?" : name) + "')"; + } + + /** {@code ldebug.c: instack}:值是第几个寄存器?不在栈上返回 {@code -1} */ + private int inStack(CallInfo c, LuaValue o) { + int base = c.func + 1; + for (int pos = 0; base + pos < c.top && base + pos < stack.length; pos++) { + if (stack[base + pos] == o) { + return pos; + } + } + return -1; + } + + /** {@code ldebug.c: getupvalname} */ + private static String getUpvalName(LClosure cl, LuaValue o, String[] name) { + for (int i = 0; i < cl.upvals.length; i++) { + UpVal uv = cl.upvals[i]; + if (uv != null && uv.value() == o) { + name[0] = upvalName(cl.p, i); + return "upvalue"; + } + } + return null; + } + + /** {@code ldebug.c: upvalname}:没有名字时给 {@code "?"},与 C 版一致 */ + static String upvalName(Proto p, int uv) { + LuaString s = (uv < p.sizeupvalues && p.upvalues[uv] != null) + ? p.upvalues[uv].name : null; + return (s == null) ? "?" : s.toJavaString(); + } + + /** + * {@code ldebug.c: kname}:只有字符串常量才有名字。 + *

+ * 两处 C 字符串语义都要照搬: + *

    + *
  • 名字最终经 {@code formatvarinfo} 的 {@code "%s"} 拼进消息,而 C 的 + * {@code %s} 在第一个 {@code \0} 处断 —— {@code toCString()}。实测 + * {@code load("local t={} return t['a\0b'].x")()}:参考实现报 + * {@code (field 'a')},不是 {@code (field 'a\0b')}。
  • + *
  • 常量可以是任意字节({@code t['\xff']} 的键),Latin-1 往返才不丢 —— 用 + * UTF-8 解会把 {@code 0xFF} 变成 {@code U+FFFD},回头 {@code latin1} 还要抛 + * {@code IllegalArgumentException}(那是 Java 异常,{@code pcall} 接不住)。
  • + *
+ * 高字节不截断:实测 {@code t['\255\254']} 参考实现原样报 + * {@code (field '')}。 + */ + static String kName(Proto p, int index, String[] name) { + if (index < 0 || index >= p.k.length) { + name[0] = "?"; + return null; + } + LuaValue kv = p.k[index]; + if (kv.isString()) { + name[0] = kv.asString().toCString().toLatin1String(); + return "constant"; + } + name[0] = "?"; + return null; + } + + /** {@code ldebug.c: filterpc}:在跳转目标之前的赋值不算数(可能没执行到) */ + private static int filterPc(int pc, int jmptarget) { + return (pc < jmptarget) ? -1 : pc; + } + + /** + * {@code ldebug.c: findsetreg}:反推「最后一次改写 {@code reg} 的指令」。 + *

+ * 只扫 {@code lastpc} 之前的指令,且一旦被 {@code jmptarget} 之后的条件跳转 + * 越过就作废——这正是它叫「符号执行」的原因:要顺着控制流判断哪次赋值 + * 真的发生过。 + */ + static int findSetReg(Proto p, int lastpc, int reg) { + int setreg = -1; + int jmptarget = 0; + int[] code = p.code; + if (lastpc >= 0 && lastpc < code.length && OpCode.testMMMode(OpCode.getOpCode(code[lastpc]))) { + lastpc--; // 前一条指令其实没执行(那是落空的 MMBIN) + } + for (int pc = 0; pc < lastpc && pc < code.length; pc++) { + int i = code[pc]; + int op = OpCode.getOpCode(i); + int a = OpCode.getArgA(i); + boolean change; + switch (op) { + case OpCode.OP_LOADNIL: { + int b = OpCode.getArgB(i); + change = (a <= reg && reg <= a + b); + break; + } + case OpCode.OP_TFORCALL: { + change = (reg >= a + 2); + break; + } + case OpCode.OP_CALL: + case OpCode.OP_TAILCALL: { + change = (reg >= a); + break; + } + case OpCode.OP_JMP: { + int dest = pc + 1 + OpCode.getArgSJ(i); + if (dest <= lastpc && dest > jmptarget) { + jmptarget = dest; + } + change = false; + break; + } + default: + change = (OpCode.testAMode(op) && reg == a); + break; + } + if (change) { + setreg = filterPc(pc, jmptarget); + } + } + return setreg; + } + + /** {@code ldebug.c: basicgetobjname}:局部名优先,其次反推指令流 */ + static String basicGetObjName(Proto p, int[] ppc, int reg, String[] name) { + int pc = ppc[0]; + LuaString local = p.getLocalName(reg + 1, pc); + if (local != null) { + name[0] = local.toJavaString(); + return "local"; + } + pc = findSetReg(p, pc, reg); + ppc[0] = pc; + if (pc != -1 && pc < p.code.length) { + int i = p.code[pc]; + switch (OpCode.getOpCode(i)) { + case OpCode.OP_MOVE: { + int b = OpCode.getArgB(i); + if (b < OpCode.getArgA(i)) { + return basicGetObjName(p, ppc, b, name); + } + break; + } + case OpCode.OP_GETUPVAL: { + name[0] = upvalName(p, OpCode.getArgB(i)); + return "upvalue"; + } + case OpCode.OP_LOADK: + return kName(p, OpCode.getArgBx(i), name); + case OpCode.OP_LOADKX: + return kName(p, OpCode.getArgAx(p.code[pc + 1]), name); + default: + break; + } + } + return null; + } + + /** {@code ldebug.c: rname}:只要常量名,别的都写成 {@code "?"} */ + static void rName(Proto p, int pc, int c, String[] name) { + int[] ppc = { pc }; + String what = basicGetObjName(p, ppc, c, name); + if (what == null || what.charAt(0) != 'c') { + name[0] = "?"; + } + } + + /** {@code ldebug.c: rkname}:RK 操作数,可能是常量也可能是寄存器 */ + static void rkName(Proto p, int pc, int i, String[] name) { + int c = OpCode.getArgC(i); + if (OpCode.getArgK(i) != 0) { + kName(p, c, name); + } else { + rName(p, pc, c, name); + } + } + + /** {@code ldebug.c: isenv}:被索引的表是不是 {@code _ENV} */ + static String isEnv(Proto p, int pc, int i, boolean isUp) { + int t = OpCode.getArgB(i); + String[] name = new String[1]; + if (isUp) { + name[0] = upvalName(p, t); + } else { + int[] ppc = { pc }; + basicGetObjName(p, ppc, t, name); + } + return ("_ENV".equals(name[0])) ? "global" : "field"; + } + + /** {@code ldebug.c: getobjname}:在 {@code basicgetobjname} 之上再认表访问 */ + static String getObjName(Proto p, int lastpc, int reg, String[] name) { + int[] ppc = { lastpc }; + String kind = basicGetObjName(p, ppc, reg, name); + if (kind != null) { + return kind; + } + int pc = ppc[0]; + if (pc != -1 && pc < p.code.length) { + int i = p.code[pc]; + switch (OpCode.getOpCode(i)) { + case OpCode.OP_GETTABUP: + kName(p, OpCode.getArgC(i), name); + return isEnv(p, pc, i, true); + case OpCode.OP_GETTABLE: + rName(p, pc, OpCode.getArgC(i), name); + return isEnv(p, pc, i, false); + case OpCode.OP_GETI: + name[0] = "integer index"; + return "field"; + case OpCode.OP_GETFIELD: + kName(p, OpCode.getArgC(i), name); + return isEnv(p, pc, i, false); + case OpCode.OP_SELF: + rkName(p, pc, i, name); + return "method"; + default: + break; + } + } + return null; + } + + /** + * {@code luaG_runerror} 的位置前缀:只在当前帧是 Lua 帧时才加。 + *

+ * 与 {@link #whereAt} 的差别是刻意的:{@code luaG_runerror} 用的是 + * {@code isLua(ci)} 这一个判断,而 {@code error()} 走的是 + * {@code luaL_where(L, 1)},会往回跳一层到调用者。混用会让 + * {@code error("x")} 报出行号偏移一格。 + *

+ * 返回的是字节串,不是 Java {@code String}。chunk 名与错误消息都是 + * Lua 字节串,可以是任意字节;走 Java String 会被 UTF-8 解码替换成 + * {@code U+FFFD},再拼回去内容就变了({@code assert(false, "\xff")} + * 一度直接抛 {@code IllegalArgumentException: not latin-1})。 + * 拼接一律用 {@link LuaString#concat}。 + */ + public LuaString where() { + CallInfo c = ci; + if (c == null || !c.isLua) { + return LuaString.EMPTY; + } + return lineInfo(c, true); + } + + /** + * {@code luaL_where(L, level)}:回跳 {@code level} 个调用帧后的位置。 + *

+ * {@code level} 只数调用层数、不跳过 C 帧——这一点与 {@code lua_getstack} + * 一致:若那一层恰好是 C 函数,就没有行号可给,返回空串。 + */ + public LuaString whereAt(int level) { + CallInfo c = ci; + for (int i = 0; i < level && c != null; i++) { + c = c.previous; + } + if (c == null || !c.isLua) { + return LuaString.EMPTY; + } + return lineInfo(c, false); + } + + /** + * 取某一帧的 {@code 源名:行号: } 前缀。 + *

+ * {@code addInfo} 区分的是 C 里两个不同的函数,它们对「没有调试信息」 + * 的处理完全相反,合成一个必然在一边错: + *

    + *
  • {@code luaG_addinfo}:源名为 {@code NULL} 时用 {@code "?"},行号照印 + * (剥离调试信息后是 {@code -1}),所以报出来是 {@code ?:-1: msg}。 + * 实测参考实现:{@code load(string.dump(f, true))} 之后 + * {@code f({})} 报 {@code ?:-1: attempt to perform arithmetic...}, + * 上游 {@code errors.lua} 断言的 {@code "^%?:%-1:"} 就是这一条。
  • + *
  • {@code luaL_where}:{@code ar.currentline > 0} 才给位置,否则空串。 + * 同一个剥离过的函数里 {@code error("x")} 报的是裸的 {@code "x"}。
  • + *
+ * 只写「源名为 null 就整段不给」会让前者丢掉 {@code ?:-1:};而退化到「用 + * chunkName 兜底」更糟——它会把那段二进制 chunk 名塞进消息里,那串是非法 + * UTF-8,解码出来就是替换字符。 + */ + private LuaString lineInfo(CallInfo c, boolean addInfo) { + if (c.func >= stack.length) { + return LuaString.EMPTY; + } + LuaValue f = stack[c.func]; + if (f.tt != Lua.VLIF) { + return LuaString.EMPTY; + } + LClosure cl = (LClosure) f.o; + int pc = (c.savedpc > 0) ? c.savedpc - 1 : 0; + int line = cl.p.getFuncline(pc); + if (!addInfo && line <= 0) { + return LuaString.EMPTY; // luaL_where:没有行号就不给位置 + } + // 源名是字节串(load(f, "\xff") 是合法的),不能解码成 Java String 再拼 + LuaString src = (cl.p.source == null) ? LuaString.latin1("?") + : LuaString.of(chunkId(cl.p.source.bytes)); + return src.concat(LuaString.latin1(":" + line + ": ")); + } + + /** + * {@code luaO_chunkid}:把源名整理成给人看的样子。 + *

+ * 三种写法,逐条照 C: + *

    + *
  • {@code =xxx}:字面源名,原样(过长则截到 {@code LUA_IDSIZE-1});
  • + *
  • {@code @xxx}:文件名,去掉 {@code @}(过长则在前面加 + * {@code ...}——保留的是末尾那一段);
  • + *
  • 其余:包成 {@code [string "xxx"]},过长则在后面加 + * {@code ...},且只取第一行(遇到 {@code \n} 就停)。
  • + *
+ * 最后一条最容易被漏:{@code load("x++")} 报的是 + * {@code [string "x++"]:1: syntax error},不是 {@code x++:1: ...}。 + */ + static String chunkId(String source) { + return new String(chunkId(source.getBytes(StandardCharsets.UTF_8)), + StandardCharsets.UTF_8); + } + + /** + * 字节版的 {@code chunkId}:源名是 Lua 字符串(字节串),截取是按字节 + * 算的,不能先解码成 {@code String} 再截,否则遇到非法 UTF-8 时长度就对不上了。 + *

+ * 结果还要按 C 字符串再截一次(道理同 {@link LuaString#cutAtNul}): + * C 版写进 {@code char buff[LUA_IDSIZE]},之后无论 {@code luaG_addinfo} 的 + * {@code "%s"} 还是 {@code short_src} 的 {@code lua_pushstring},看到的 + * 都只有第一个 NUL 之前那一段。 + *

+ * 源名里带 NUL 只有二进制 chunk 做得到——{@code lua_load} 的 + * {@code chunkname} 是 {@code const char*},进 {@code Proto.source} 之前 + * 就被 {@code strlen} 截过了,所以 {@code load(s, "a\0b")} 测不出这一层。 + * 实测:把 {@code string.dump} 出来的源名 {@code "local zzzz = 1..."} + * 改成 {@code "local a\0b\0 ..."} 再 {@code load},参考实现的报错是 + * {@code [string "local a:1: variable 'a' got a non-closable value} + * —— 连收尾的 {@code "]"} 都没有。 + */ + static byte[] chunkId(byte[] source) { + return nulTruncate(chunkIdRaw(source)); + } + + /** {@link #chunkId} 的实现,不含最后的 C 字符串截断。 */ + private static byte[] chunkIdRaw(byte[] source) { + final int IDSIZE = 60; // LUA_IDSIZE + final byte[] RETS = { '.', '.', '.' }; + final byte[] PRE = "[string \"".getBytes(StandardCharsets.US_ASCII); + final byte[] POS = "\"]".getBytes(StandardCharsets.US_ASCII); + int srclen = source.length; + /* + * 空源名不是「原样返回」。C 版读的是 {@code *source},Lua 字符串 + * 结尾有 {@code '\0'},所以走的是最后那条 {@code [string "..."]} 分支, + * 产出 {@code [string ""]}。实测参考实现: + * {@code load(s, "")} 之后 {@code debug.getinfo(f).short_src} 是 + * {@code [string ""]}(上游 {@code db.lua} 里就钉着这一条)。 + */ + byte c = (srclen > 0) ? source[0] : 0; + if (c == '=') { + // 字面源名:去掉 '=',过长则从右边截(保留开头那一段) + return Arrays.copyOfRange(source, 1, Math.min(srclen, IDSIZE)); + } + if (c == '@') { + // 文件名:去掉 '@',过长则在前头加 '...'(保留末尾那一段) + if (srclen <= IDSIZE) { + return Arrays.copyOfRange(source, 1, srclen); + } + /* + * {@code srclen} 是含那个 {@code '@'} 的总长。C 版在这里 + * {@code memcpy(out, source + 1 + srclen - bufflen, bufflen)}—— + * 末位读到的其实是字符串结尾的 {@code '\0'},所以真正搬过去的内容只有 + * {@code bufflen - 1} 个字节,可见长度是 + * {@code RETS + bufflen - 1 = IDSIZE - 1}。 + * 照抄那个 {@code bufflen} 会在 Java 里越界一字节(Lua 字符串没有 + * 结尾的 {@code '\0'}),而且是只在长于 59 字节的路径上才炸—— + * 本机跑上游测试套件时第一次暴露,因为用例目录的绝对路径够长。 + * 实测参考实现:66 字节的 {@code @路径} 打出来是 59 字节的 + * {@code ...dddddd/.../x.lua}。 + */ + int bufflen = IDSIZE - RETS.length; + ByteArrayOutputStream out = new ByteArrayOutputStream(IDSIZE); + out.write(RETS, 0, RETS.length); + out.write(source, 1 + srclen - bufflen, bufflen - 1); + return out.toByteArray(); + } + // 其余:包成 [string "xxx"],过长则从右边截;只取第一行 + int bufflen = IDSIZE - (PRE.length + RETS.length + POS.length) - 1; + int nl = srclen; + for (int i = 0; i < srclen; i++) { + if (source[i] == '\n') { + nl = i; + break; + } + } + boolean whole = (srclen < bufflen && nl == srclen); + int len = whole ? srclen : Math.min(nl, bufflen); + ByteArrayOutputStream out = new ByteArrayOutputStream(IDSIZE); + out.write(PRE, 0, PRE.length); + out.write(source, 0, len); + if (!whole) { + out.write(RETS, 0, RETS.length); + } + out.write(POS, 0, POS.length); + return out.toByteArray(); + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/LuaString.java b/src/main/java/com/github/esrrhs/fakescript/lua/LuaString.java new file mode 100644 index 0000000..b814a18 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/LuaString.java @@ -0,0 +1,196 @@ +package com.github.esrrhs.fakescript.lua; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +/** + * Lua 字符串 —— 对应 {@code lobject.h} 的 {@code TString}。 + *

+ * 底层是 {@code byte[]} 而不是 Java {@code String}。这是本实现最重要的一处 + * 「不能照搬 Java 习惯」的地方:Lua 字符串是字节串,可以包含 {@code \0}, + * {@code #s} 数的是字节数,{@code s:byte(i)} 返回字节值,{@code \x80} 这类 + * 非法 UTF-8 序列也是合法字符串。Java 的 {@code String} 是 UTF-16 且拒绝孤立 + * 代理项,用它当底层必然在处理二进制数据或 UTF-8 文本的真实脚本上出错。 + *

+ * 代价是所有字符串库函数都要按字节实现,不能顺手调 Java 的 String 方法—— + * 这正是 {@code lstrlib.c} 原本的做法,照搬即可。 + */ +public final class LuaString { + + public static final LuaString EMPTY = new LuaString(new byte[0]); + + /** 内容,构造后不再变化(与 C 版 TString 一样不可变) */ + public final byte[] bytes; + + /** 缓存的哈希值,对应 {@code lstring.c} 的 {@code luaS_hash} 结果 */ + private final int hash; + + private LuaString(byte[] bytes) { + this.bytes = bytes; + this.hash = luaSHash(bytes); + } + + /** + * {@code lstring.c: luaS_hash}:从尾部按步长跳跃的混合哈希。 + *

+ * 哈希值只影响哈希表内部分布,不影响任何可观测语义,但照搬上游实现可以让 + * 未来的对比调试更好做(同一个字符串在两边落到同一个桶)。 + */ + private static int luaSHash(byte[] b) { + int h = b.length; + int step = (b.length >> 5) + 1; + for (int i = b.length; i >= step; i -= step) { + h ^= (h << 5) + (h >> 2) + (b[i - 1] & 0xFF); + } + return h & 0x7FFFFFFF; + } + + public static LuaString of(byte[] bytes) { + return new LuaString(bytes); + } + + public static LuaString of(byte[] buf, int off, int len) { + return new LuaString(Arrays.copyOfRange(buf, off, off + len)); + } + + /** + * 内部使用:把 Java {@code String} 按 Latin-1 逐字节映射。 + *

+ * 只在元方法名、错误信息这类由本实现自己产生的 ASCII 文本上使用。 + * 之所以用 Latin-1 而非 UTF-8,是因为它保证 {@code byte<->char} 在 0..255 + * 上无损往返,不会因为编码选择意外改写内容。 + */ + public static LuaString latin1(String s) { + byte[] b = new byte[s.length()]; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c > 0xFF) { + throw new IllegalArgumentException("not latin-1: " + c); + } + b[i] = (byte) c; + } + return new LuaString(b); + } + + /** 按 UTF-8 编码。用于需要真实 UTF-8 字节的场合(如 os.getenv 返回值) */ + public static LuaString utf8(String s) { + return new LuaString(s.getBytes(StandardCharsets.UTF_8)); + } + + /** 字节长度,对应 {@code tsslen} —— 也就是 {@code #s} 的结果 */ + public int length() { + return bytes.length; + } + + /** 取第 i 个字节(0 起),返回 0..255 */ + public int byteAt(int i) { + return bytes[i] & 0xFF; + } + + /** 是否是短字符串(参与驻留) */ + public boolean isShort() { + return bytes.length <= Lua.MAXSHORTLEN; + } + + public int hash() { + return hash; + } + + /** 拼接 */ + public LuaString concat(LuaString other) { + byte[] b = new byte[bytes.length + other.bytes.length]; + System.arraycopy(bytes, 0, b, 0, bytes.length); + System.arraycopy(other.bytes, 0, b, bytes.length, other.bytes.length); + return new LuaString(b); + } + + public LuaString substring(int from, int to) { + return new LuaString(Arrays.copyOfRange(bytes, from, to)); + } + + /** 按 UTF-8 解码成 Java String —— 仅用于调试输出与宿主交互,非法序列会被替换 */ + public String toJavaString() { + return new String(bytes, StandardCharsets.UTF_8); + } + + /** + * 按 Latin-1 逐字节解码,与 {@link #latin1} 互为逆运算。 + *

+ * 用在「名字要绕一圈 Java String 再拼回 Lua 字符串」的场合:varinfo 的字段名 + * ({@code t["\xff"]} 里的常量)、元表的 {@code __name}、{@code require} 的 + * 模块名、{@code argError} 反查出来的函数名。这些地方最终都要 + * {@code LuaString.latin1} 编码回去,而用 UTF-8 解码会把 {@code 0xFF} 变成 + * {@code U+FFFD} —— 于是要么内容被改写(编码成 3 字节 {@code EF BF BD}), + * 要么 {@code latin1} 直接抛 {@code IllegalArgumentException} + * ({@code U+FFFD > 0xFF})。Latin-1 保证 {@code byte<->char} 在 0..255 上 + * 无损往返,是这条链上唯一安全的中间表示。 + */ + public String toLatin1String() { + char[] c = new char[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + c[i] = (char) (bytes[i] & 0xFF); + } + return new String(c); + } + + /** + * 按 C 字符串语义截断到第一个 {@code \0}。 + *

+ * 移植时凡是 C 版把 {@code LuaString} 当作 {@code const char *} 用的地方 + * (格式化进 {@code "%s"}、拿去当类型名、拼进报错文案),内容都会在第一个 + * NUL 处被悄悄砍断 —— 因为 C 字符串以 NUL 结尾,而 Lua 字符串不以 NUL 结尾。 + * 实测 {@code tostring(setmetatable({}, {__name = "A\0B"}))}:参考实现打印 + * {@code A: 0x...},只有 {@code A}。 + */ + public LuaString toCString() { + for (int i = 0; i < bytes.length; i++) { + if (bytes[i] == 0) { + return new LuaString(Arrays.copyOf(bytes, i)); + } + } + return this; + } + + /** + * {@link #toCString} 的 Java String 版:把 Latin-1 中间表示的名字 + * 按 C 字符串语义截断到第一个 {@code \0}。 + *

+ * 名字从字节串出发、经 {@link #toLatin1String} 变成 Java String、最后被拼进 + * {@code "%s"} 一路的那些出口({@code findfield} 反查出来的函数名、 + * {@code luaO_chunkid} 的结果)都卡在这一步:C 那边 {@code %s} 读的是 + * {@code const char *},第一个 NUL 之后的内容根本看不见。实测 + * {@code package.loaded["M\0"] = {["\0"] = f}} 之后 {@code debug.traceback()} + * 打印 {@code in function 'M'},不是 {@code 'M<00>.<00>'}。 + *

+ * 高字节不动:只有 NUL 是 C 字符串的终止符。 + */ + public static String cutAtNul(String latin1) { + if (latin1 == null) { + return null; + } + int i = latin1.indexOf('\0'); + return (i < 0) ? latin1 : latin1.substring(0, i); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof LuaString)) { + return false; + } + LuaString s = (LuaString) o; + return hash == s.hash && Arrays.equals(bytes, s.bytes); + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public String toString() { + return toJavaString(); + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/LuaTable.java b/src/main/java/com/github/esrrhs/fakescript/lua/LuaTable.java new file mode 100644 index 0000000..6a74c32 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/LuaTable.java @@ -0,0 +1,1249 @@ +package com.github.esrrhs.fakescript.lua; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +/** + * Lua 表 —— 对应 {@code ltable.c} 的 {@code Table}。 + *

+ * 与 C 版一样分成两段: + *

    + *
  • 数组部分 {@code array}:密集的正整数键,下标 {@code k} 落在 + * {@code array[k-1]}。这是 {@code for i = 1, #t} 能跑快的原因,也是 + * {@code #t} 能二分查找边界的原因。
  • + *
  • 哈希部分 {@code node}:其余所有键,链地址法,容量取 2 的幂。
  • + *
+ *

+ * 键的相等性用 Lua 的 {@code rawequal} 而不是 Java 的 {@code equals}: + * {@code 1} 与 {@code 1.0} 是同一个键(Lua 里 {@code 1 == 1.0} 为真), + * 但 {@code 1} 与 {@code "1"} 不是(整数键与字符串键是不同的项)。 + *

+ * 一处刻意的简化:C 版 5.4 用「假 alimit」把数组部分的实际边界藏在一个标志位里 + * ({@code isfakealimit}),以省掉部分二分查找。这里直接用精确的 + * {@code alimit == array.length},语义等价、代码好读,代价是 {@code #t} + * 偶尔多一次二分。 + */ +public final class LuaTable { + + private static final LuaValue[] EMPTY_ARRAY = new LuaValue[0]; + + /** 数组部分。下标 k(1 起)对应 array[k-1],槽永不为空(nil 用 isNil() 表示) */ + LuaValue[] array = EMPTY_ARRAY; + + /** 哈希部分,长度为 2 的幂;为 null 表示为空(对应 C 的 dummy node) */ + Node[] node = null; + + /** 哈希部分长度 */ + int nodeSize = 0; + + /** 下一个待探测的空闲位置,从 {@link #nodeSize} 往下走(对应 {@code lastfree}) */ + int lastFree = 0; + + /** 元表 */ + LuaTable metatable; + + /** 挂了 {@code __gc} 时创建的哨兵;没挂则为 {@code null}。见 {@link GcSentinel} */ + private GcSentinel gcSentinel; + + /** {@code __gc} 是否已经跑过。只跑一次,与参考实现一致(实测确认) */ + private boolean gcDone; + + /** 哈希节点:键 + 值 + 同链下一跳(下标,-1 表示链尾) */ + static final class Node { + final LuaValue key = new LuaValue(); + final LuaValue val = new LuaValue(); + int next = -1; + } + + /* ==================== 弱表 ==================== */ + + /** + * 弱引用包装。弱表里的键(或值)把这个放进 {@code LuaValue.o}, + * 而不是直接放对象本身。 + *

+ * 为什么必须换掉载荷:如果槽里直接放对象,那就是一条强引用,JVM 永远 + * 不会回收它,弱表也就永远不会释放任何东西——弱引用必须是存储形式, + * 不能只是旁边挂一个 {@code WeakReference} 做样子。 + */ + static final class WeakRef extends WeakReference { + + /** + * 建引用那一刻的身份哈希。 + *

+ * 不能等到用的时候再 {@code System.identityHashCode(get())}:引用失效后 + * {@code get()} 是 {@code null},而 {@code identityHashCode(null)} 恒为 0, + * 于是所有失效键的主位置一起塌到 0 号桶。链地址法靠主位置走链,主位置一变 + * 就找不到前驱——表现是 {@code newKey} 里 {@code node[-1]} 越界。 + */ + final int hash; + + WeakRef(Object referent) { + super(referent); + hash = System.identityHashCode(referent); + } + } + + /** {@code __mode} 里的 {@code 'k'} / {@code 'v'} */ + private boolean weakKey; + + private boolean weakValue; + + /** 上次清扫时的收集轮次;用来保证一轮里只清一次,避免 {@code pairs} 变 O(n²) */ + private int purgeGen = -1; + + /** + * 全局收集轮次,{@code collectgarbage} 时递增。 + *

+ * 弱条目只在「一轮收集结束」时被摘掉,这一点与 C 版一致:Lua 是在 GC 周期的 + * 末尾清弱表,不是键一变成不可达就立刻消失。所以自发发生的 JVM 收集不会 + * 让条目提前消失——这与参考实现的行为更接近。 + */ + static int gcGeneration = 0; + + /** 取槽里真正的对象:弱引用解一层,引用已失效返回 {@code null} */ + static Object payload(LuaValue v) { + Object o = v.o; + return (o instanceof WeakRef) ? ((WeakRef) o).get() : o; + } + + /** 这一项是否已经失效(弱引用被回收) */ + static boolean isDead(LuaValue v) { + return (v.o instanceof WeakRef) && ((WeakRef) v.o).get() == null; + } + + /** + * 会不会被弱表清掉。 + *

+ * 只有「有显式构造」的对象才会被清:表、函数、线程、userdata。 + * 字符串不算——它虽然可收集,但没有显式构造,所以 + * {@code __mode='kv'} 的表里 {@code t['a']='b'} 永远不会被摘掉(实测确认)。 + * 数字、布尔同理。 + */ + private static boolean isCollectable(LuaValue v) { + switch (Lua.noVariant(v.tt)) { + case Lua.TTABLE: + case Lua.TFUNCTION: + case Lua.TTHREAD: + case Lua.TUSERDATA: + return true; + default: + return false; + } + } + + /** + * 写入时把可收集的对象包成弱引用。 + * + * @param doWrap 这一侧(键或值)是不是弱的 + */ + private static LuaValue weakWrap(LuaValue v, boolean doWrap) { + /* + * 已经包过的不能再包。{@code purge} 与 {@code resize} 搬的是槽里现有的 + * LuaValue,它的 {@code o} 已经是 {@code WeakRef};再包一层就成了 + * WeakRef 套 WeakRef,解引用拿到的是内层引用对象而不是表,所有按身份 + * 比较的查找就此失效。 + */ + if (!doWrap || !isCollectable(v) || (v.o instanceof WeakRef)) { + return v; + } + LuaValue r = new LuaValue(); + r.set(v); + r.o = new WeakRef(v.o); + return r; + } + + /** 本表是不是弱表 */ + boolean isWeak() { + return weakKey || weakValue; + } + + /** + * 需要清扫时清掉失效条目。 + *

+ * 放在每个公开方法的入口,于是下面的算法(查找、rehash、遍历)都只需要处理 + * 「活条目」——失效项不会在算法中途冒出来。用轮次挡一层是为了让 + * {@code for k in pairs(t)} 是 O(n) 而不是 O(n²):一轮收集里只清一次。 + */ + private void purgeIfNeeded() { + if (!isWeak() || purgeGen == gcGeneration) { + return; + } + purgeGen = gcGeneration; + purge(); + } + + /** 摘掉所有已经失效的条目。弱表不用数组部分,所以只看哈希部分 */ + private void purge() { + if (nodeSize == 0) { + return; + } + List live = new ArrayList<>(); + for (int i = 0; i < nodeSize; i++) { + Node n = node[i]; + if (n.val.isNil()) { + continue; + } + if (isDead(n.key) || isDead(n.val)) { + continue; + } + live.add(new LuaValue[] { n.key, n.val }); + } + if (live.size() == countLiveNodes()) { + return; // 一项都没失效,不用重建 + } + int size = nodeSize; + node = null; + nodeSize = 0; + lastFree = 0; + setNodeVector(size); + for (LuaValue[] kv : live) { + insertNoRehash(kv[0], kv[1]); + } + } + + private int countLiveNodes() { + int n = 0; + for (int i = 0; i < nodeSize; i++) { + if (!node[i].val.isNil()) { + n++; + } + } + return n; + } + + /** + * 读出时把弱引用解成普通值。 + *

+ * 这一层是必须的:槽里的 {@code WeakRef} 绝不能泄漏到 VM 里,否则 + * {@code (LClosure) v.o} 这类取载荷的地方会直接抛 {@code ClassCastException}。 + */ + private static LuaValue weakUnwrap(LuaValue v) { + if (!(v.o instanceof WeakRef)) { + return v; + } + Object o = ((WeakRef) v.o).get(); + LuaValue r = new LuaValue(); + if (o == null) { + // 引用已经失效:还成一个 nil,绝不能带着 tt=TABLE 而载荷是 null + r.setNil(); + return r; + } + r.set(v); + r.o = o; + return r; + } + + /** + * 弱键比较:两边至少有一边是弱引用时按「解引用后同一对象」判等。 + *

+ * 两边都失效时必须判不等:否则任意两个死条目会互相匹配(解出来都是 + * {@code null}),一次查找就会命中一个和它毫无关系的槽。 + */ + private static boolean slotKeyEquals(LuaValue a, LuaValue b) { + if (a.o instanceof WeakRef || b.o instanceof WeakRef) { + if (a.tt != b.tt) { + return false; + } + Object pa = payload(a); + Object pb = payload(b); + return pa != null && pa == pb; + } + return LuaValue.rawEqual(a, b); + } + + public LuaTable() { + LuaState.chargeGc(); + } + + /** 预分配两段。仅供需要预置大小的场合(如构造 registry) */ + public LuaTable(int arraySize, int hashSize) { + LuaState.chargeGc(); + if (arraySize > 0) { + setArrayVector(arraySize); + } + if (hashSize > 0) { + setNodeVector(hashSize); + } + } + + /* ==================== 哈希与主位置 ==================== */ + + /** {@code mainposition}:键的主桶下标 */ + private int mainPosition(LuaValue key) { + switch (key.tt) { + case Lua.VNUMINT: + return (int) (key.i & 0x7FFFFFFF) & (nodeSize - 1); + case Lua.VNUMFLT: + return hashFloat(key.n) & (nodeSize - 1); + case Lua.VSHRSTR: + case Lua.VLNGSTR: + return key.asString().hash() & (nodeSize - 1); + case Lua.TBOOLEAN: + return (int) key.i & (nodeSize - 1); + default: + return objectHash(key.o) & (nodeSize - 1); + } + } + + /** + * 对象键的哈希。 + *

+ * 弱键走 {@link WeakRef#hash}——建引用时记下的那份;直接用 + * {@code identityHashCode(payload(key))} 会在引用失效后算出 0(见 + * {@link WeakRef#hash} 的注释)。 + */ + private static int objectHash(Object o) { + return (o instanceof WeakRef) ? ((WeakRef) o).hash : System.identityHashCode(o); + } + + /** + * {@code ltable.c: l_hashfloat}:把浮点拆成尾数×2^指数再映射到整数。 + *

+ * {@code -0.0} 与 {@code 0.0} 必须哈希到同一处——Lua 里二者相等 + * ({@code luai_numeq}),因此必须是同一个键。 + */ + private static int hashFloat(double n) { + if (Double.isNaN(n) || Double.isInfinite(n)) { + return 0; + } + if (n == 0.0) { + return 0; // 归一化 -0.0 + } + int e = Math.getExponent(n) + 1; + double m = Math.scalb(n, -e); // 0.5 <= |m| < 1 + long ni = (long) (m * (Integer.MAX_VALUE - Double.MAX_EXPONENT)); + ni += e; + if (ni < 0) { + ni = -ni; + } + return (int) (ni & 0x7FFFFFFF); + } + + /* ==================== 取 ==================== */ + + /** + * {@code luaH_get}:返回值的槽;键不存在时返回 {@code null}(调用方按 nil 处理)。 + *

+ * 返回的是活的槽而不是副本:调用方可以直接往里写。这与 C 版返回 + * {@code TValue*} 一致,也是「数组部分优先于哈希部分」这个优化能成立的前提。 + */ + public LuaValue get(LuaValue key) { + purgeIfNeeded(); + LuaValue slot = rawGet(key); + if (slot == null) { + return null; + } + /* + * 弱表必须返回副本而不是活槽:槽里放的是 WeakRef,直接交出去 + * 会让取载荷的代码炸掉。强表照旧返回活槽——那是写入路径依赖的优化, + * 不能为了弱表把它改掉。 + */ + return isWeak() ? weakUnwrap(slot) : slot; + } + + /** {@code luaH_get} 的原实现,返回活槽(含弱引用载荷),供内部使用 */ + private LuaValue rawGet(LuaValue key) { + if (key.tt == Lua.VNUMINT) { + return getInt(key.i); + } + if (key.tt == Lua.VNUMFLT) { + // 整值浮点与整数是同一个键:Lua 5.4.7 实测 t[1.0] 与 t[1] 同一项 + long[] k = new long[1]; + if (floatToIntExact(key.n, k)) { + return getInt(k[0]); + } + } + return getFromHash(key); + } + + /** 整数键查找:先数组部分,再哈希部分 */ + private LuaValue getInt(long k) { + if (k > 0 && k <= array.length) { + return array[(int) k - 1]; + } + return getFromHash(LuaValue.ofInteger(k)); + } + + /** + * {@code luaV_flttointns} 的 F2Ieq 模式:浮点只有当精确等于某个整数 + * 时才转换。 + *

+ * 这条规则直接决定键的规范化,已用 Lua 5.4.7 实测确认: + *

+     *   t[1.0]  = "a"  →  next(t) 的键 math.type 是 "integer",#t 是 1
+     *   t[0.0]  = "a"  →  键是整数 0(但 0 不是数组下标,故 #t 仍是 0)
+     *   t[1.5]  = "a"  →  键是浮点
+     *   t[2^63] = "a"  →  键是浮点(超出 integer 可表示范围)
+     * 
+ */ + private static boolean floatToIntExact(double n, long[] out) { + return LuaValue.floatToIntExact(n, out); + } + + /** + * 写入前的键规范化:整值浮点转成整数键,nil 与 NaN 直接报错。 + *

+ * 放在这里而不是各调用点,是因为 {@code ltable.c} 里这两个检查就在 + * {@code luaH_newkey} 的开头——所有写路径(VM 的 {@code OP_SET*}、 + * {@code rawset}、宿主 API)最终都经过它,分散检查必漏一处。 + *

+ * NaN 的处理是 Lua 5.4.7 实测的:读 {@code t[0/0]} 返回 nil 不报错, + * 写 {@code t[0/0] = 1} 报 "table index is NaN"。所以这里只在 + * 写路径上拒绝。 + *

+ * 抛的是不带位置的裸错误值:{@code luaG_runerror} 的位置前缀由 + * {@link LuaState#pcall} 在保护边界补上,与 C 版 {@code luaG_addinfo} + * 的时机一致。 + */ + private static LuaValue normalizeKeyForWrite(LuaValue key) { + if (key.isNil()) { + throw new LuaError(LuaValue.ofString(LuaString.latin1("table index is nil"))); + } + if (key.tt == Lua.VNUMFLT) { + if (Double.isNaN(key.n)) { + throw new LuaError(LuaValue.ofString(LuaString.latin1("table index is NaN"))); + } + long[] out = new long[1]; + if (floatToIntExact(key.n, out)) { + return LuaValue.ofInteger(out[0]); + } + } + return key; + } + + private LuaValue getFromHash(LuaValue key) { + if (nodeSize == 0) { + return null; + } + Node n = node[mainPosition(key)]; + while (true) { + if (slotKeyEquals(n.key, key)) { + return n.val; + } + if (n.next < 0) { + return null; + } + n = node[n.next]; + } + } + + public LuaValue get(long k) { + return get(LuaValue.ofInteger(k)); + } + + public LuaValue get(LuaString k) { + return get(LuaValue.ofString(k)); + } + + public LuaValue get(String k) { + return get(LuaString.latin1(k)); + } + + /** 键是否存在且值非 nil */ + public boolean contains(LuaValue key) { + LuaValue v = get(key); + return v != null && !v.isNil(); + } + + /* ==================== 存 ==================== */ + + /** + * {@code luaH_set} + {@code setobj2t}。 + *

+ * 给不存在的键赋 nil 什么都不做——这是 {@code ltable.c: luaH_newkey} + * 开头那一行: + *

+     *   if (ttisnil(value)) return;  /* do not insert nil values *\/
+     * 
+ * 漏掉它的代价是一个很隐蔽的分歧:建条目会占掉哈希槽、进而触发 + * {@code rehash},而 {@code rehash} 算出来的数组段尺寸是 + * {@code #t} 的直接输入。实测(参考实现): + *
+     *   local b = {[1] = 4.0}
+     *   b[3] = nil      -- 参考实现不建条目(本实现建了 → 哈希不再是 dummy)
+     *   b[2] = 4.0
+     *   b[1] = nil
+     *   print(#b)       -- 参考 2;建了条目则 rehash 只给数组段 1,#b 变 0
+     * 
+ * {@code table.move} 逆序搬元素时正好是这个顺序,于是 + * {@code table.move({[1]=4.0}, 0, 2, 1)} 之后 {@code #t} 两边差了 2 和 0。 + *

+ * 键的合法性检查(nil 键、NaN 键)不受影响:C 版那两个检查在 + * {@code luaH_newkey} 里排在 nil 值检查之前,所以 + * {@code t[nil] = nil} 照样报 {@code table index is nil}。 + */ + public void set(LuaValue key, LuaValue value) { + purgeIfNeeded(); + LuaValue k = normalizeKeyForWrite(key); + if (value.isNil()) { + LuaValue slot = rawGet(k); + // 键不存在:不建条目;键存在:照旧写 nil(条目留着,等 rehash 再摘) + if (slot != null) { + slot.set(value); + } + return; + } + ensureSlot(k).set(weakWrap(value, weakValueFor(k, value))); + } + + /** + * 值这一侧要不要弱引用。 + *

+ * 通常是 {@code weakValue},但弱键表多一条:值就是键本身时也必须弱存。 + * 否则值会强引用键,键永远死不掉、条目永远清不掉。实测参考实现: + * {@code t[k] = k} 之后 {@code collectgarbage()},{@code next(t)} 是 nil。 + *

+ * 这个特例是安全的:值与键是同一个对象,键活着值就活着,键死了条目 + * 本来也要摘掉——弱存不会丢任何东西。 + *

+ * 反过来,{@code t[k] = {k}} 这种「值里套着键」的形态不在这里弱存: + * 两者不是同一个对象,一旦弱存,值会在键还活着的时候被回收, + * {@code t[k]} 凭空变成 nil。那是数据丢失,比漏摘条目严重得多。 + * 这是本实现已知的一处与参考实现不一致,README 里记着。 + */ + private boolean weakValueFor(LuaValue key, LuaValue value) { + if (weakValue) { + return true; + } + return weakKey && isCollectable(value) && payload(key) == payload(value); + } + + public void set(long k, LuaValue value) { + set(LuaValue.ofInteger(k), value); + } + + public void set(LuaString k, LuaValue value) { + set(LuaValue.ofString(k), value); + } + + public void set(String k, LuaValue value) { + set(LuaString.latin1(k), value); + } + + /** + * {@code luaH_set}:拿到键对应的可写槽,键不存在就先建条目。 + *

+ * 为什么不能一步到位。rehash 有可能把这个键直接安排进数组部分 + * (例如空表上 {@code t[1]=v}:rehash 算出数组部分该是 1,于是槽在 + * {@code array[0]},而它是 {@code LuaValue} 不是 {@code Node})。所以 + * rehash 之后必须重新查一次,不能假设结果还在哈希部分——这正是 C 版在 + * rehash 后重新调用 {@code luaH_set} 的原因,只是那里靠指针统一了两种槽。 + *

+ * 只用于非 nil 的值——赋 nil 走 {@link #set} 那条路,不建条目 + * (理由见那里的注释)。 + */ + private LuaValue ensureSlot(LuaValue key) { + LuaValue slot = rawGet(key); + if (slot != null) { + return slot; + } + while (true) { + if (nodeSize == 0) { + // 还没有哈希部分:交给 rehash 决定两段大小 + rehash(key); + } else { + Node n = newKey(key, false); + if (n != null) { + return n.val; + } + rehash(key); + } + slot = rawGet(key); + if (slot != null) { + return slot; + } + } + } + + /** + * {@code luaH_newkey}:为表中不存在的键分配条目。 + *

+ * 返回 {@code null} 表示「需要 rehash」,由调用方 rehash 后重试。之所以把这个 + * 决定交回调用方,是因为 {@link #resize} 搬运旧数据时必须保证不再触发 + * 嵌套 rehash(否则搬一半的表会被再次重建)。 + */ + private Node newKey(LuaValue key, boolean allowRehash) { + if (nodeSize == 0) { + return null; + } + int mpIdx = mainPosition(key); + Node mp = node[mpIdx]; + if (!mp.val.isNil()) { + int f = getFreePos(); + if (f < 0) { + return null; // 没有空位 + } + int otherIdx = mainPosition(mp.key); + if (otherIdx != mpIdx) { + // mp 不在它自己的主位置:把它挪到空闲位 f,腾出 mp 给新键 + int p = otherIdx; + while (node[p].next != mpIdx) { + p = node[p].next; + } + node[p].next = f; + copyNode(mp, node[f]); + mp.next = -1; + mp.val.setNil(); + } else { + // mp 就在自己的主位置:新节点挂到 f,链在 mp 之后 + node[f].next = mp.next; + mp.next = f; + mpIdx = f; + mp = node[f]; + } + } + mp.key.set(weakWrap(key, weakKey)); + mp.val.setNil(); + return mp; + } + + private static void copyNode(Node from, Node to) { + to.key.set(from.key); + to.val.set(from.val); + to.next = from.next; + } + + /** {@code getfreepos}:从 lastFree 往下找第一个键为 nil 的位置 */ + private int getFreePos() { + while (lastFree > 0) { + lastFree--; + if (node[lastFree].key.isNil()) { + return lastFree; + } + } + return -1; + } + + /* ==================== rehash ==================== */ + + /** + * {@code ltable.c: rehash}:插入新键却找不到空位时,重新分配两段的大小。 + *

+ * 核心是 {@code computesizes}:按 2 的幂逐档统计整数键,只要某一档里 + * 「已用数量超过该档容量的一半」,就把数组部分扩到那一档。这就是 + * 「先 {@code t[1]=…} 再 {@code t[2]=…} 会让数组部分从 0 长到 1 再长到 2」 + * 的原因,也是为什么乱序赋值(先 {@code t[1000]=1})不会撑出一百万长的数组。 + */ + private void rehash(LuaValue extraKey) { + /* + * 弱表先清一遍失效条目。 + *

+ * 不清会出两种事故:一是失效条目占着槽,{@code getFreePos} 找不到空位, + * 于是 {@code newKey} 返回 {@code null} → rehash → 还是找不到 → 死循环; + * 二是 {@code numUseHash} 把它们也算进 {@code totalUse},哈希部分按一个 + * 虚高的用量扩容,永远缩不回去。 + */ + if (isWeak()) { + purge(); + } + int[] nums = new int[Lua.MAXABITS + 1]; + int na = numUseArray(nums); + int totalUse = na; + int[] hashUse = { 0 }; + totalUse += numUseHash(nums, hashUse); + na += hashUse[0]; + + // 计入本次要插入的那个键 + if (extraKey.tt == Lua.VNUMINT) { + na += countInt(extraKey.i, nums); + } + totalUse++; + + int[] outNa = { na }; + int newArraySize = computeSizes(nums, outNa); + /* + * 弱表一律不用数组部分。数组槽是普通的 LuaValue,放不了 WeakRef, + * 而给数组另配一套弱存储会把查找路径撕成两份。哈希部分是唯一存储, + * 于是 {__mode='v'} 的表里 t[1] = {} 也能正常被回收。 + */ + if (isWeak()) { + newArraySize = 0; + /* + * 哈希大小必须重算,不能用 {@code totalUse - outNa[0]}:outNa[0] 是 + * 「会被安排进数组部分」的个数,而弱表的数组部分恒为 0,那部分键 + * 全部得留在哈希里。照抄原来的算式会算出 0 个哈希槽,于是 + * newKey 永远返回 null、ensureSlot 无限重试——表现是整个测试挂死。 + */ + resize(0, Math.max(totalUse, 1)); + return; + } + resize(newArraySize, Math.max(totalUse - outNa[0], 0)); + } + + /** {@code numusearray}:统计数组部分里非 nil 的元素,按 2 的幂分档 */ + private int numUseArray(int[] nums) { + int ause = 0; + int i = 1; + int ttlg = 1; + for (int lg = 0; lg <= Lua.MAXABITS; lg++, ttlg *= 2) { + int lim = Math.min(ttlg, array.length); + int lc = 0; + for (; i <= lim; i++) { + if (!array[i - 1].isNil()) { + lc++; + } + } + nums[lg] = lc; + ause += lc; + if (i > array.length) { + break; + } + } + return ause; + } + + /** {@code numusehash}:遍历哈希部分,统计总数与「可以放进数组部分」的整数键数 */ + private int numUseHash(int[] nums, int[] pna) { + int totalUse = 0; + int ause = 0; + for (int i = 0; i < nodeSize; i++) { + Node n = node[i]; + if (!n.val.isNil()) { + if (n.key.tt == Lua.VNUMINT) { + ause += countInt(n.key.i, nums); + } + totalUse++; + } + } + pna[0] += ause; + return totalUse; + } + + /** {@code countint}:整数键落在数组范围内就计入分档 */ + private static int countInt(long key, int[] nums) { + int k = arrayIndex(key); + if (k != 0) { + nums[Lua.ceilLog2(k)]++; + return 1; + } + return 0; + } + + private static int arrayIndex(long k) { + return (k > 0 && k <= Lua.MAXASIZE) ? (int) k : 0; + } + + /** {@code computesizes}:算出数组部分的「最优」大小 */ + private static int computeSizes(int[] nums, int[] pna) { + int optimal = 0; + int a = 0; + int na = 0; + for (int i = 0, twotoi = 1; twotoi > 0 && pna[0] > twotoi / 2; i++, twotoi *= 2) { + if (nums[i] > 0) { + a += nums[i]; + if (a > twotoi / 2) { + optimal = twotoi; + na = a; + } + } + } + pna[0] = na; + return optimal; + } + + /** + * {@code luaH_resize}:重建两段,把旧内容搬过去。 + *

+ * 搬的顺序有讲究:先把「数组部分被砍掉的那一段」和「旧哈希部分」的所有键值 + * 收集起来,等新空间就绪后再逐个插入。不能边搬边写目标表——插入过程中若再 + * 触发 rehash,还没搬的数据就丢了。因此这里用 {@code newKey(key, false)}: + * 空间不足视为实现错误,而不是静默 rehash。 + */ + private void resize(int newArraySize, int newHashSize) { + LuaValue[] oldArray = array; + Node[] oldNodes = node; + int oldNodeSize = nodeSize; + + List moved = new ArrayList<>(); + + // 数组部分收缩:被切掉的那一段要搬进哈希部分 + for (int j = newArraySize; j < oldArray.length; j++) { + if (!oldArray[j].isNil()) { + moved.add(new LuaValue[] { LuaValue.ofInteger(j + 1), oldArray[j] }); + } + } + + // 新数组部分,保留前缀 + setArrayVector(newArraySize); + int keep = Math.min(newArraySize, oldArray.length); + for (int j = 0; j < keep; j++) { + array[j].set(oldArray[j]); + } + + // 旧哈希部分的存活条目 + if (oldNodes != null) { + for (int i = 0; i < oldNodeSize; i++) { + Node n = oldNodes[i]; + // 失效条目不搬:搬过去也是垃圾,还白占一个槽 + if (!n.val.isNil() && !isDead(n.key) && !isDead(n.val)) { + moved.add(new LuaValue[] { n.key, n.val }); + } + } + } + + /* + * 新哈希部分。 + *

+ * 只有不会落进新数组部分的键才需要槽 —— 这一点是实测撞出来的。 + * C 的 {@code reinsert} 用 {@code luaH_set} 搬旧数据,整数键只要 + * {@code <= 新数组长度} 就被路由进数组部分,不占哈希槽; + * {@code luaH_resize(L, t, 4, 0)} 之后哈希部分因此是 dummy(0 槽)。 + * 这里早先按 {@code moved.size()} 撑哈希,于是「数组 4 + 哈希 2 槽」 + * 留了下来,而 {@code luaH_newkey} 的判据里有 {@code isdummy(t)}: + * 哈希不是 dummy 时,下一次插入直接占一个空槽,不再 rehash, + * 数组部分就再也长不大。实测 {@code t={}; t[2]=2 t[3]=3 t[4]=4 + * t[6]=6 t[7]=7}:参考实现数组部分 8、{@code #t} 是 7,我们是 4 和 4。 + */ + int hashSize = newHashSize; + int needSlots = 0; + for (LuaValue[] kv : moved) { + if (!landsInArray(kv[0], newArraySize)) { + needSlots++; + } + } + if (needSlots > hashSize) { + hashSize = needSlots; + } + setNodeVector(hashSize); + + for (LuaValue[] kv : moved) { + insertNoRehash(kv[0], kv[1]); + } + } + + /** + * 搬运旧数据时插入一项,不允许再触发 rehash。 + *

+ * 与 {@link #set} 的差别不只是「不许 rehash」:这里必须走 {@link #get} + * 的路由,让整数键有机会落进新扩出来的数组部分。若图省事直接塞进哈希, + * 就会破坏「所有 {@code 1..alimit} 的整数键都在数组部分」这条不变式—— + * 那样 {@code get} 会在数组槽上拿到 nil 就返回,哈希里那份数据再也读不到。 + */ + private void insertNoRehash(LuaValue key, LuaValue value) { + LuaValue slot = rawGet(key); + if (slot != null) { + slot.set(value); + return; + } + Node n = newKey(key, false); + if (n == null) { + throw new IllegalStateException("hash part too small after resize"); + } + n.val.set(value); + } + + /** 这个键搬进新表时会走数组部分吗?与 {@link #rawGet} 的路由保持一致 */ + private static boolean landsInArray(LuaValue key, int newArraySize) { + if (key.tt == Lua.VNUMINT) { + return key.i > 0 && key.i <= newArraySize; + } + if (key.tt == Lua.VNUMFLT) { + long[] k = new long[1]; + return floatToIntExact(key.n, k) && k[0] > 0 && k[0] <= newArraySize; + } + return false; + } + + private void setArrayVector(int size) { + LuaValue[] a = new LuaValue[size]; + for (int i = 0; i < size; i++) { + a[i] = new LuaValue(); + } + array = a; + } + + private void setNodeVector(int size) { + if (size <= 0) { + node = null; + nodeSize = 0; + lastFree = 0; + return; + } + // 向上取整到 2 的幂 + int n = 1; + while (n < size) { + n <<= 1; + } + Node[] nodes = new Node[n]; + for (int i = 0; i < n; i++) { + nodes[i] = new Node(); + } + node = nodes; + nodeSize = n; + lastFree = n; + } + + /** + * 直接写数组部分的第 {@code k} 项(1 起),必要时只把数组部分扩到 {@code k}。 + *

+ * 对应 {@code OP_SETLIST} 的 {@code luaH_resizearray + 直接写}。与 + * {@link #set} 的差别是不走 rehash:表构造的路径已经从字节码里知道 + * 该分配多大,再走一次 rehash 纯属浪费,而且会把刚攒起来的字段搅乱。 + *

+ * 不搬哈希部分里落在 {@code [1, k]} 的整数键——C 版也是这样,两边行为一致 + * (数组部分优先于哈希部分,读的时候只会看到数组里的那份)。 + */ + public void setArrayRaw(long k, LuaValue value) { + if (k <= 0 || isWeak()) { + // 弱表没有数组部分,表构造也得走哈希 + set(LuaValue.ofInteger(k), value); + return; + } + if (k > array.length) { + int n = array.length; + if (n == 0) { + n = 1; + } + while (n < k) { + n *= 2; + } + LuaValue[] bigger = new LuaValue[n]; + System.arraycopy(array, 0, bigger, 0, array.length); + for (int i = array.length; i < n; i++) { + bigger[i] = new LuaValue(); + } + array = bigger; + } + array[(int) k - 1].set(value); + } + + /* ==================== 长度(#t)==================== */ + + /** + * {@code luaH_getn}:返回一个「边界」——{@code t[n]} 非 nil 且 {@code t[n+1]} 为 nil。 + *

+ * Lua 的手册只要求返回某个边界,所以「二分查找落在某个合法边界上」 + * 听起来就够了。不够:目标是与参考实现逐字节一致,而真实程序会观察到 + * 具体是哪一个。{@code ltable.c} 在这里特意不走二分: + * + *

+     * if (limit > 0 && isempty(&t->array[limit - 1])) {   // (1)
+     *   if (limit >= 2 && !isempty(&t->array[limit - 2])) {
+     *     return limit - 1;             // (1a) 常见情形,别去二分
+     *   }
+     *   else { ... binsearch ... }      // (1b)
+     * }
+     * 
+ * + * 少了 (1a) 会差多少:数组 {@code [1, nil, 3, nil]} —— 二分给 {@code 1} + * ({@code i=0,j=4 → m=2 空 → j=2 → m=1 非空 → i=1}),而参考实现给 + * {@code 3}。实测 {@code table.pack(1, nil, 3, nil)} 之后 + * {@code #t} 是 {@code 3} 不是 {@code 1};{@code t = {1,2,3,4}; t[2]=nil; + * t[4]=nil} 同样是 {@code 3}。C 的注释写明了动机:{@code t[#t] = nil} + * 之后立刻再取长度是最常见的一串操作,直接答 {@code limit-1}。 + *

+ * 数组部分满且哈希部分非空时,边界可能藏在哈希里,走 {@code unbound_search}: + * 先指数级试探再二分。 + */ + public long length() { + int j = array.length; + if (j > 0 && array[j - 1].isNil()) { + // (1a):倒数第二格还在,那 limit-1 就是边界,别去二分 + if (j >= 2 && !array[j - 2].isNil()) { + return j - 1; + } + // (1b):在 [0, limit] 里二分 + int i = 0; + while (j - i > 1) { + int m = (i + j) / 2; + if (array[m - 1].isNil()) { + j = m; + } else { + i = m; + } + } + return i; + } + if (nodeSize == 0) { + return j; + } + return unboundSearch(j); + } + + /** {@code unbound_search}:边界在哈希部分,先翻倍试探上界再二分 */ + private long unboundSearch(long j) { + long i = j; + j++; + while (true) { + LuaValue v = get(LuaValue.ofInteger(j)); + if (v == null || v.isNil()) { + break; + } + i = j; + if (j > Integer.MAX_VALUE / 2) { + return i; // 表过大,无法继续翻倍(C 版在此报 "table too big") + } + j *= 2; + } + while (j - i > 1) { + long m = (i + j) / 2; + LuaValue v = get(LuaValue.ofInteger(m)); + if (v == null || v.isNil()) { + j = m; + } else { + i = m; + } + } + return i; + } + + /* ==================== 遍历(pairs/next)==================== */ + + /** + * {@code luaH_next}:返回 {@code key} 之后的下一项,形如 {@code [key, value]}; + * 没有下一项时返回 {@code null}。传入 nil 表示从头开始。 + *

+ * 先数组部分后哈希部分,与 C 版一致。顺序本身在 Lua 里是未定义的, + * 这里不保证任何顺序,只保证每项不重不漏。 + */ + public LuaValue[] next(LuaValue key) { + purgeIfNeeded(); + int start; + if (key == null || key.isNil()) { + start = 0; + } else if (key.tt == Lua.VNUMINT) { + long k = key.i; + if (k > 0 && k <= array.length) { + start = (int) k; // 下一个数组槽的 0 起下标 + } else { + int ni = findNodeIndex(key); + if (ni < 0) { + throw new IllegalArgumentException("invalid key to 'next'"); + } + start = array.length + ni + 1; + } + } else { + int ni = findNodeIndex(key); + if (ni < 0) { + throw new IllegalArgumentException("invalid key to 'next'"); + } + start = array.length + ni + 1; + } + + for (int i = Math.max(start, 0); i < array.length; i++) { + if (!array[i].isNil()) { + return new LuaValue[] { LuaValue.ofInteger(i + 1), array[i] }; + } + } + if (nodeSize > 0) { + for (int j = Math.max(start - array.length, 0); j < nodeSize; j++) { + if (!node[j].val.isNil() && !isDead(node[j].key) && !isDead(node[j].val)) { + return new LuaValue[] { weakUnwrap(node[j].key), weakUnwrap(node[j].val) }; + } + } + } + return null; + } + + /** 找到键在哈希部分的下标,找不到返回 -1 */ + private int findNodeIndex(LuaValue key) { + if (nodeSize == 0) { + return -1; + } + int i = mainPosition(key); + while (true) { + if (slotKeyEquals(node[i].key, key)) { + return i; + } + if (node[i].next < 0) { + return -1; + } + i = node[i].next; + } + } + + /* ==================== 元表 ==================== */ + + public LuaTable getMetatable() { + return metatable; + } + + public void setMetatable(LuaTable mt) { + boolean oldK = weakKey; + boolean oldV = weakValue; + metatable = mt; + updateWeakMode(); + if (weakKey != oldK || weakValue != oldV) { + rewrap(); + } + } + + /** + * {@code __mode} 变了之后重排整个表的存储。 + *

+ * 弱引用是本实现的存储形式(槽里放的是 {@code WeakRef}),不像 C 版那样 + * 可以在遍历时临时判断「这个键是不是已经死了」。所以强弱一变,已经存在的条目 + * 必须逐个重新包装一遍——变弱时把可收集的对象换成 {@code WeakRef},变强时换回来。 + *

+ * 实测参考实现:先建普通表、塞好条目、再 {@code setmetatable(t, {__mode='k'})}, + * 那些条目照样会被回收。不做这一步,它们会以强引用的形式永远留在表里。 + */ + private void rewrap() { + purge(); + List all = new ArrayList<>(); + for (int i = 0; i < array.length; i++) { + if (!array[i].isNil()) { + all.add(new LuaValue[] { LuaValue.ofInteger(i + 1), array[i] }); + } + } + for (int i = 0; i < nodeSize; i++) { + Node n = node[i]; + if (!n.val.isNil() && !isDead(n.key) && !isDead(n.val)) { + all.add(new LuaValue[] { weakUnwrap(n.key), weakUnwrap(n.val) }); + } + } + // 弱表不用数组部分(槽是普通 LuaValue,放不了 WeakRef),整数键一并进哈希 + int arr = isWeak() ? 0 : array.length; + array = EMPTY_ARRAY; + node = null; + nodeSize = 0; + lastFree = 0; + setArrayVector(arr); + setNodeVector(Math.max(all.size(), 1)); + for (LuaValue[] kv : all) { + set(kv[0], kv[1]); + } + } + + /** + * 按元表里的 {@code __mode} 重算弱标志。 + *

+ * 是动态读的——实测参考实现:先建一个普通表、塞进条目,之后再 + * {@code setmetatable(t, {__mode='k'})},那些条目照样会被回收。所以不能只在 + * 建表时看一次。 + *

+ * 只认 {@code 'k'} 与 {@code 'v'} 两个字符,其余字符忽略;{@code __mode} + * 不是字符串时也忽略。实测:{@code {__mode='x'}}、{@code {__mode=5}} 都不报错。 + */ + private void updateWeakMode() { + boolean k = false; + boolean v = false; + if (metatable != null) { + LuaValue mode = metatable.get(LuaString.latin1("__mode")); + if (mode != null && mode.isString()) { + LuaString s = mode.asString(); + for (int i = 0; i < s.length(); i++) { + int b = s.byteAt(i); + if (b == 'k') { + k = true; + } else if (b == 'v') { + v = true; + } + } + } + } + weakKey = k; + weakValue = v; + } + + /* ==================== __gc ==================== */ + + /** + * 有 {@code __gc} 就登记一个哨兵,并记进主线程的可终结清单。 + *

+ * 只在 {@code setmetatable} 的当下看一次。实测参考实现:先 + * {@code setmetatable(t, mt)}、之后才 {@code mt.__gc = f},{@code f} 不会被 + * 调用——Lua 是在设元表的那一刻给对象打上「有终结器」标记的。 + *

+ * {@code __gc} 不是函数(实测 {@code {__gc = 5}})时也照常登记:参考 + * 实现里它会在真正调用时炸,而 {@code __gc} 里的错误一律静默吞掉,所以对外 + * 表现就是「什么都没发生」。这里不登记也看不出差别,登记则更贴近 C 版的路径。 + * + * @param L 当前线程;队列归它的主线程 + * @return 本次是否新登记了一个可终结对象 + */ + boolean markFinalizable(LuaState L) { + if (gcSentinel != null || gcDone || metatable == null) { + return false; + } + LuaValue tm = metatable.get(LuaString.latin1("__gc")); + if (tm == null || tm.isNil()) { + return false; + } + gcSentinel = new GcSentinel(this, L.mainThread); + L.mainThread.registerFinalizable(this); + return true; + } + + /** + * 跑一次 {@code __gc}。 + *

+ * {@code __gc} 里的错误一律吞掉——实测参考实现:{@code __gc} 里 + * {@code error} 既不打印也不传播,脚本照常往下跑。 + *

+ * 先置 {@code gcDone} 再调,避免元方法里把对象重新挂回全局时再来一遍。 + */ + void runGcMetamethod(LuaState L) { + if (gcDone) { + return; + } + gcDone = true; + if (metatable == null) { + return; + } + LuaValue tm = metatable.get(LuaString.latin1("__gc")); + if (tm == null || tm.isNil()) { + return; + } + try { + int func = L.top; + L.ensureSlots(func + 2); + L.stack[func].set(tm); + L.stack[func + 1].set(LuaValue.ofTable(this)); + L.top = func + 2; + /* + * luaD_callnoyield:__gc 里不许 yield。实测参考实现会在那时报 + * 「attempt to yield across a C-call boundary」,用 L.call 就会让它 + * 「成功」,反而与参考实现不一致。 + */ + /* + * CIST_FIN 打在当前帧上({@code lgc.c: GCTM} 的做法), + * 不是终结器那帧。终结器里 {@code debug.getinfo(1)} 问的是 + * {@code ci->previous}——正是这里被标记的这一帧。 + */ + boolean wasFin = L.ci.fin; + L.ci.fin = true; + try { + L.callNoYield(func, 0); + } finally { + L.ci.fin = wasFin; + } + } catch (LuaError e) { + // 静默 + } catch (LuaYield y) { + // __gc 里 yield:参考实现报跨 C 调用边界,这里同样吞掉 + } + } + + /* ==================== 诊断 ==================== */ + + /** 表中非 nil 项的个数(遍历统计,仅供测试与诊断,不是 O(1)) */ + public int count() { + purgeIfNeeded(); + int n = 0; + for (LuaValue v : array) { + if (!v.isNil()) { + n++; + } + } + if (nodeSize > 0) { + for (int i = 0; i < nodeSize; i++) { + if (!node[i].val.isNil() && !isDead(node[i].key) && !isDead(node[i].val)) { + n++; + } + } + } + return n; + } + + /** 数组部分长度,供测试观察 rehash 行为 */ + public int arrayPartSize() { + return array.length; + } + + /** 哈希部分长度,供测试观察 rehash 行为 */ + public int hashPartSize() { + return nodeSize; + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/LuaTm.java b/src/main/java/com/github/esrrhs/fakescript/lua/LuaTm.java new file mode 100644 index 0000000..c418df3 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/LuaTm.java @@ -0,0 +1,445 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * 元方法 —— 对应 {@code ltm.c}。 + *

+ * 元表查找有两条路径,区别必须保留: + *

    + *
  • 表 / userdata:元表挂在对象自己身上;
  • + *
  • 其余类型(nil、number、string…):整个类型共用一个元表, + * 存在 {@link LuaState} 里,只能通过 {@code debug.setmetatable} 设置。 + * 字符串是个特例——所有字符串共享 {@code string} 库的元表。
  • + *
+ *

+ * {@code LUA_COMPAT_LT_LE} 的行为照搬:缺 {@code __le} 时退化成 + * {@code !(p2 < p1)},用 {@code CIST_LEQ} 标记这次替换(本实现不做 yield, + * 标记退化为 {@link CallInfo#leq},但保留以对应上游语义)。 + */ +final class LuaTm { + + private LuaTm() { + } + + /* ==================== 查找 ==================== */ + + /** + * {@code luaT_gettmbyobj}:按对象取元方法。 + * + * @return 元方法值;不存在返回 {@code null} + */ + static LuaValue getTmByObj(LuaState L, LuaValue o, int event) { + LuaTable mt = metatableOf(L, o); + if (mt == null) { + return null; + } + LuaValue v = mt.get(LuaValue.ofString(L.tmName[event])); + return (v == null || v.isNil()) ? null : v; + } + + /** + * {@code luaT_gettmbyobj} 的前半段:取一个值的元表。 + *

+ * 表与 userdata 的元表在对象自己身上,其余类型共享一张「类型元表」。 + * 这段判断之前在 {@code BaseLib} / {@code LuaState} / 这里各写了一份, + * userdata 那一支只有这份有——于是 {@code io.stdout} 的 + * {@code __name} / {@code __tostring} 走别的路径时全部落空。 + */ + static LuaTable metatableOf(LuaState L, LuaValue v) { + switch (v.baseType()) { + case Lua.TTABLE: + return v.asTable().getMetatable(); + case Lua.TUSERDATA: + return ((LuaUserdata) v.o).metatable; + default: + return L.typeMetatable(v.baseType()); + } + } + + /** + * {@link #metatableOf} 的反向操作 —— {@code lapi.c: lua_setmetatable} 里 + * 「按类型分派到三处」的那半段。 + *

+ * 之前只有 {@link BaseLib} 里的 {@code setmetatable} 自己写了这段分派, + * 于是 {@code debug.setmetatable(1, mt)} 这种「给整个类型设元表」的路径 + * 没有对应物。抽到这里,与 {@link #metatableOf} 成对,改一处不会漏另一处。 + */ + static void setMetatable(LuaState L, LuaValue v, LuaTable mt) { + switch (v.baseType()) { + case Lua.TTABLE: + v.asTable().setMetatable(mt); + break; + case Lua.TUSERDATA: + ((LuaUserdata) v.o).metatable = mt; + break; + default: + L.setTypeMetatable(v.baseType(), mt); + break; + } + } + + /** + * {@code luaT_objtypename}:{@code __name} 元字段优先于标准类型名。 + *

+ * 这是 {@code print(#io.stdout)} 报 + * {@code attempt to get length of a FILE* value} 的那条路径—— + * 光有 {@code typeName} 会报成 {@code userdata}。 + *

+ * 只有表与 userdata 认 {@code __name}:C 版的条件是 + * {@code ttistable(o) || ttisfulluserdata(o)},其余类型连类型元表都不看。 + * 实测 {@code debug.setmetatable(nil, {__name = "NilT"})} 之后 {@code nil + 1} + * 参考实现报的仍是 {@code ... on a nil value} —— 走 + * {@link #metatableOf} 会把类型元表也算进来,于是我们多报了 {@code NilT}。 + *

+ * {@code __name} 还走 C 字符串语义:{@code ltm.c: luaT_objtypename} 拿 + * 到的是 {@code getstr()} 那个 {@code const char *},而 {@code luaG_typeerror} + * 用 {@code "%s"} 拼进消息 —— 于是内容在第一个 {@code \0} 处被砍断。实测 + * {@code setmetatable({}, {__name = "A\0B"}) + 1}:参考实现报 + * {@code attempt to perform arithmetic on a A value},只有 {@code A}。 + * ({@code luaL_tolstring} 那条路上同样的问题另见 {@link LuaString#toCString}。) + */ + static String objTypeName(LuaState L, LuaValue v) { + LuaTable mt = null; + switch (v.baseType()) { + case Lua.TTABLE: + mt = v.asTable().getMetatable(); + break; + case Lua.TUSERDATA: + mt = ((LuaUserdata) v.o).metatable; + break; + default: + break; // 其余类型不看 __name + } + if (mt != null) { + LuaValue nm = mt.get(LuaString.latin1("__name")); + if (nm != null && nm.isString()) { + // Latin-1 往返:这个名字会走 luaG_runerror 的 %s 拼回 Lua 字符串 + return nm.asString().toCString().toLatin1String(); + } + } + return v.typeName(); + } + + /** + * {@code fasttm}:表的元方法快速路径。 + *

+ * 与 {@link #getTmByObj} 的差别只是省掉了类型分派——调用方已经确认了是表。 + */ + static LuaValue fastTm(LuaTable mt, int event, LuaState L) { + if (mt == null) { + return null; + } + LuaValue v = mt.get(LuaValue.ofString(L.tmName[event])); + return (v == null || v.isNil()) ? null : v; + } + + /* ==================== 调用 ==================== */ + + /** + * {@code luaT_callTM}:调 {@code f(p1, p2, p3)},不要结果。 + *

+ * 压栈位置是当前 {@code L.top}——C 版靠 {@code EXTRA_STACK} 保证金, + * 这里显式 {@code ensureSlots}。 + */ + /** + * 与 {@link #callTm} 同形,但 {@code forceYy} 为真时一定用可 yield 的 + * {@code luaD_call}。 + * + * @see #callTm(LuaState, LuaValue, LuaValue, LuaValue) + */ + static void callTmYy(LuaState L, LuaValue f, LuaValue p1, LuaValue p2, boolean forceYy) { + int func = L.top; + L.ensureSlots(func + 3); + L.stack[func].set(f); + L.stack[func + 1].set(p1); + L.stack[func + 2].set(p2); + L.top = func + 3; + if (forceYy) { + L.call(func, 0); + } else { + L.callTmMightYield(func, 0); + } + } + + static void callTm(LuaState L, LuaValue f, LuaValue p1, LuaValue p2, LuaValue p3) { + int func = L.top; + L.ensureSlots(func + 4); + L.stack[func].set(f); + L.stack[func + 1].set(p1); + L.stack[func + 2].set(p2); + L.stack[func + 3].set(p3); + L.top = func + 4; + L.callTmMightYield(func, 0); + } + + /** + * {@code luaT_callTMres}:调 {@code f(p1, p2)},把结果写到 {@code resIdx}。 + *

+ * 结果先落在 {@code func} 位置(因为 {@code poscall} 会把第一个结果搬到 + * {@code ci.func}),再拷到 {@code resIdx}。这两步不能合并:{@code resIdx} + * 是调用方寄存器,可能就在被覆盖的范围内。 + *

+ * 能不能 yield 要看当前帧:C 版这两句 + *

+     * if (isLuacode(L->ci)) luaD_call(L, func, 1);
+     * else luaD_callnoyield(L, func, 1);
+     * 
+ * 意思是「元方法只有在被 Lua 代码触发时才可以 yield」。少了这一刀, + * 从 C 函数(比如 {@code string.gsub} 查表做替换)触发的元方法里 yield + * 会一路穿过那个 C 帧;而那个帧没注册续体,恢复时 + * {@code finishCcall} 拿到 {@code k == null} —— Java 里是 NPE, + * C 里是段错误。参考实现报的是干净的 + * {@code attempt to yield across a C-call boundary},且能被 pcall 接住。 + */ + static void callTmRes(LuaState L, LuaValue f, LuaValue p1, LuaValue p2, int resIdx) { + int func = L.top; + L.ensureSlots(Math.max(func + 4, resIdx + 1)); + L.stack[func].set(f); + L.stack[func + 1].set(p1); + L.stack[func + 2].set(p2); + L.top = func + 3; + L.callTmMightYield(func, 1); + L.stack[resIdx].set(L.stack[--L.top]); + } + + /* ==================== 算术 / 位运算 ==================== */ + + /** {@code callbinTM}:依次试两个操作数的元方法 */ + private static boolean callBinTm(LuaState L, LuaValue p1, LuaValue p2, int resIdx, int event) { + LuaValue tm = getTmByObj(L, p1, event); + if (tm == null) { + tm = getTmByObj(L, p2, event); + } + if (tm == null) { + return false; + } + callTmRes(L, tm, p1, p2, resIdx); + return true; + } + + /** {@code luaT_trybinTM}:找不到元方法就报「对 X 做算术/位运算」 */ + static void tryBinTm(LuaState L, LuaValue p1, LuaValue p2, int resIdx, int event) { + if (!callBinTm(L, p1, p2, resIdx, event)) { + switch (event) { + case Tm.TM_BAND: + case Tm.TM_BOR: + case Tm.TM_BXOR: + case Tm.TM_SHL: + case Tm.TM_SHR: + case Tm.TM_BNOT: + if (p1.isNumber() && p2.isNumber()) { + /* + * {@code ldebug.c: luaG_tointerror}: + *
+                     * if (!luaV_tointegerns(p1, &temp, LUA_FLOORN2I))
+                     *   p2 = p1;
+                     * luaG_runerror(L, "number%s has no integer representation",
+                     *               varinfo(L, p2));
+                     * 
+ * 三处都不能想当然: + *
    + *
  • 报哪一个:先试 {@code p1},它转不成整数才报 + * {@code p1},否则报 {@code p2}。 + * {@code math.huge << 1} 报的是左边那个 inf。
  • + *
  • 模式是 {@code F2I_EQ},不是 {@code F2I_FLOOR}。 + * {@code lvm.h} 里默认 {@code LUA_FLOORN2I == F2Ieq} + * —— 名字带 floor,语义其实是「只接受整数值,不取整」。 + * 按 FLOOR 判的话 {@code 1.5 << 3} 里 {@code 1.5} + * 会被当成「能转」,于是去报右边的 {@code 3};参考实现 + * 报的是左边那个 {@code 1.5}。这个坑是「运算符 × 类型」 + * 全矩阵扫描撞出来的,手写的用例只有 inf/nan 那两种 + * 极端值,两种模式在它们上面结论一样。
  • + *
  • 带 {@code varinfo} 后缀:值若在寄存器里, + * 参考实现还会反推它是怎么来的——上面那句实测是 + * {@code number (field 'huge') has no integer + * representation}({@code math.huge} 是 + * {@code OP_GETFIELD} 取来的)。少了后缀,上游 + * {@code math.lua} 的 {@code checkcompt("field + * 'huge'", "return math.huge << 1")} 就对不上。
  • + *
+ */ + LuaValue bad = (Vm.toIntegerNs(p1, Arith.F2I_EQ) == null) ? p1 : p2; + throw L.runError("number" + L.varInfo(bad) + + " has no integer representation"); + } + throw L.typeError(p1.isNumber() ? p2 : p1, "perform bitwise operation on"); + default: + throw L.typeError(p1.isNumber() ? p2 : p1, "perform arithmetic on"); + } + } + } + + /** {@code luaT_trybinassocTM}:{@code flip} 表示两个操作数在字节码里是反着放的 */ + static void tryBinAssocTm(LuaState L, LuaValue p1, LuaValue p2, boolean flip, int resIdx, + int event) { + if (flip) { + tryBinTm(L, p2, p1, resIdx, event); + } else { + tryBinTm(L, p1, p2, resIdx, event); + } + } + + /** {@code luaT_trybiniTM}:第二个操作数是立即整数 */ + static void tryBinITm(LuaState L, LuaValue p1, long i2, boolean flip, int resIdx, int event) { + tryBinAssocTm(L, p1, LuaValue.ofInteger(i2), flip, resIdx, event); + } + + /** {@code luaT_tryconcatTM}:栈顶两个值拼不起来 */ + static void tryConcatTm(LuaState L) { + int t = L.top; + if (!callBinTm(L, L.stack[t - 2], L.stack[t - 1], t - 2, Tm.TM_CONCAT)) { + throw L.concatError(L.stack[t - 2], L.stack[t - 1]); + } + } + + /* ==================== 比较 ==================== */ + + /** {@code luaT_callorderTM} */ + static boolean callOrderTm(LuaState L, LuaValue p1, LuaValue p2, int event) { + int t = L.top; + L.ensureSlots(t + 1); + if (callBinTm(L, p1, p2, t, event)) { + return L.stack[t].toBoolean(); + } + // LUA_COMPAT_LT_LE:没有 __le 时用 !(p2 < p1) 顶替 + if (event == Tm.TM_LE) { + L.ci.leq = true; + if (callBinTm(L, p2, p1, t, Tm.TM_LT)) { + L.ci.leq = false; + return !L.stack[t].toBoolean(); + } + } + throw L.orderError(p1, p2); + } + + /** {@code luaT_callorderiTM}:第二个操作数是立即数 */ + static boolean callOrderITm(LuaState L, LuaValue p1, long v2, boolean flip, boolean isFloat, + int event) { + LuaValue aux = isFloat ? LuaValue.ofFloat(v2) : LuaValue.ofInteger(v2); + if (flip) { + return callOrderTm(L, aux, p1, event); + } + return callOrderTm(L, p1, aux, event); + } + + /* ==================== to-be-closed ==================== */ + + /** + * {@code callclosemethod} 的入口:先 {@code checkclosemth},再 + * {@code __close(obj, errObj)}。 + *

+ * 没有 {@code __close} 就是一个运行时错误——这正是 + * {@code local x = 1} 报「non-closable value」的来源。 + * 报错时要带上变量名({@code luaG_findlocal}),所以这里必须拿到 + * 寄存器序号,光有值是不够的。 + * + * @param varIdx {@code luaG_findlocal} 用的序号:{@code 槽位 - ci.func} + */ + /** + * {@code lfunc.c: prepcallclosemth + callclosemath}。 + * + * @param varIdx 变量本身的栈下标 + * @param localIdx 同一个变量相对 {@code ci.func} 的序号({@code luaG_findlocal}) + */ + static void callCloseMethod(LuaState L, LuaValue obj, LuaValue errObj, + int varIdx, int localIdx, boolean forceYy) { + /* + * 这里不做「有没有 __close」的检查。 + *

+ * {@code lfunc.c} 把检查放在 {@code luaF_newtbcupval} 里——也就是 + * {@code local x = ...} 赋值的当下;而真正关闭用的 + * {@code prepcallclosemth} 只管把查到的元方法调掉。两者不是一回事: + * 声明之后把 {@code __close} 改成 nil,参考实现会在关闭时报 + * {@code attempt to call a nil value (metamethod 'close')}, + * 而不是 {@code variable 'x' got a non-closable value} + * (上游 {@code locals.lua} 有一整条断言对着这句)。 + */ + LuaValue tm = getTmByObj(L, obj, Tm.TM_CLOSE); + if (tm == null) { + /* + * 没有元表时 C 版给的是 {@code luaO_nilobject}(一个 nil 的 TValue), + * 不是 NULL。这里对应成 nil 值——下面真会去调它,于是报 + * 「attempt to call a nil value」,与参考实现一致。 + */ + tm = LuaValue.ofNil(); + } + /* + * 错误对象必须拷一份。传进来的那个 {@code errObj} 往往是某个栈槽的 + * 别名(解绕时它就躺在 {@code top - 1}),而下面写调用帧会落到 + * {@code varIdx + 2} 起的三格——正好可能压在它自己身上。不拷贝的话 + * 写进去的就是刚刚被 {@code tm} 覆盖过的值:实测 {@code err} 变成了 + * {@code function: 0x...},也就是 {@code __close} 自己。 + */ + LuaValue errCopy = (errObj == null) ? LuaValue.ofNil() : new LuaValue(errObj); + int func; + if (errObj == null) { + /* + * {@code CLOSEKTOP}:错误对象用的是全局那个 nil,C 版不动 + * {@code L->top},调用帧就摆在 {@code top}({@code OP_RETURN} 在关 + * 之前已经把 {@code top} 抬到 {@code ci->top})。这里只兜底抬一下, + * 不往下压——压了就会盖住 {@code OP_RETURN} 已经算好的返回值 + * (实测 {@code locals.lua}「closing functions do not corrupt + * returning values」那条断言就炸在这一点上)。 + */ + func = Math.max(L.top, varIdx + 2); + } else { + /* + * 出错解绕:{@code luaD_seterrorobj(L, status, level + 1)} 把错误对象 + * 放到变量后面那一格,并把 {@code top} 设成 {@code level + 2}, + * 调用帧就摆在那儿。 + *

+ * 这一步不能省:解绕时 {@code top} 已经掉到变量底下,若直接用它当 + * 调用位置,{@code __close} 的帧会把还没关的 tbc 变量冲掉 + * ——实测第二个 {@code __close} 报「variable '?' got a non-closable + * value」,因为它的值被上一次调用的帧覆盖了。 + */ + func = varIdx + 2; + L.ensureSlots(varIdx + 5); + L.stack[varIdx + 1].set(errCopy); + } + L.ensureSlots(func + 3); + L.top = func; + // __close 只收 (self, err) 两个参数;多传一个会把 err 顶成 self + callTmYy(L, tm, obj, errCopy, forceYy); + } + + /* ==================== 可变参数 ==================== */ + + /** + * {@code luaT_adjustvarargs}:把固定参数搬到栈顶,可变参数留在原处。 + *

+ * 搬完之后 {@code ci.func} 前移 {@code actual+1},于是「多余参数」变成 + * {@code ci.func - nextra .. ci.func - 1},正好在帧的下方——这就是 + * {@code OP_VARARG} 取值的地方。 + */ + static void adjustVarargs(LuaState L, int nfixparams, CallInfo c, Proto p) { + int actual = L.top - c.func - 1; + int nextra = actual - nfixparams; + c.nextraargs = nextra; + L.ensureSlots(c.top + p.maxstacksize + 1); + L.stack[L.top++].set(L.stack[c.func]); // 把函数拷到栈顶 + for (int i = 1; i <= nfixparams; i++) { + L.stack[L.top++].set(L.stack[c.func + i]); + L.stack[c.func + i].setNil(); // 抹掉原位置 + } + c.func += actual + 1; + c.top += actual + 1; + } + + /** {@code luaT_getvarargs}:取 {@code wanted} 个可变参数写到 {@code where} */ + static void getVarargs(LuaState L, CallInfo c, int where, int wanted) { + int nextra = c.nextraargs; + if (wanted < 0) { + wanted = nextra; + L.ensureSlots(where + nextra); + L.top = where + nextra; // 下一条指令要用到 top + } + L.ensureSlots(where + wanted); + int i = 0; + for (; i < wanted && i < nextra; i++) { + L.stack[where + i].set(L.stack[c.func - nextra + i]); + } + for (; i < wanted; i++) { + L.stack[where + i].setNil(); + } + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/LuaUserdata.java b/src/main/java/com/github/esrrhs/fakescript/lua/LuaUserdata.java new file mode 100644 index 0000000..9c8a5bf --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/LuaUserdata.java @@ -0,0 +1,107 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * 完整 userdata —— 对应 {@code lobject.h} 的 {@code Udata}。 + *

+ * C 的 {@code Udata} 是「元表 + 用户值数组 + 字节载荷」,载荷由调用方 + * {@code lua_newuserdatauv} 之后自己解释。Java 里没有「在对象后面贴一块内存」 + * 这回事,所以载荷退化成一个 {@link Object} 引用;真要塞字节流的宿主自己 + * 传 {@code byte[]} 进来。 + *

+ * 元表必须在对象上,不能在类型上。之前的实现里 userdata 走的是 + * {@code LuaState#typeMetatable},也就是「同一类型共享一张元表」。这在只有 + * 内部 userdata(随机数状态、{@code gmatch} 状态)时看不出问题,一旦要开 + * {@code io} 库就崩:{@code luaL_testudata} 判的是「这个 userdata 的元表 + * 是不是注册表里 {@code FILEHANDLE*} 那一张」,要是所有 userdata 共用, + * 随机数状态也会被当成文件句柄。 + *

+ * 用户值({@code lua_getiuservalue})这里不实现——{@code liolib} 建的 + * {@code LStream} 用 {@code nuvalue = 0},阶段 7 用不上。 + */ +public final class LuaUserdata { + + /** 宿主载荷 */ + public final Object data; + + /** + * 元表;{@code null} 表示没有。 + *

+ * 注意与「已关闭」无关:文件句柄的关闭标记在载荷自己身上。 + */ + public LuaTable metatable; + + public LuaUserdata(Object data) { + LuaState.chargeGc(); + this.data = data; + } + + /** 取 {@link LuaValue} 里的 userdata 载荷;调用方已确认是 userdata */ + public static Object dataOf(LuaValue v) { + return ((LuaUserdata) v.o).data; + } + + /* ==================== __gc ==================== */ + + /** 挂了 {@code __gc} 时创建的哨兵;没挂则为 {@code null} */ + private GcSentinel gcSentinel; + + private boolean gcDone; + + /** + * {@code luaC_checkfinalizer}:挂了 {@code __gc} 就登记一个哨兵。 + *

+ * 与 {@link LuaTable#markFinalizable} 同构,判据也相同——只在设元表的 + * 当下看一次。{@code io} 库的文件句柄靠这个在脚本忘了 {@code f:close()} + * 时把文件关掉。 + */ + void markFinalizable(LuaState L) { + if (gcSentinel != null || gcDone || metatable == null) { + return; + } + LuaValue tm = metatable.get(LuaString.latin1("__gc")); + if (tm == null || tm.isNil()) { + return; + } + gcSentinel = new GcSentinel(this, L.mainThread); + L.mainThread.registerFinalizable(this); + } + + /** + * 跑一次 {@code __gc(ud)}。 + *

+ * 与 {@link LuaTable#runGcMetamethod} 一致:{@code __gc} 里的错误一律吞掉, + * 且只允许跑一次。 + */ + void runGcMetamethod(LuaState L) { + if (gcDone) { + return; + } + gcDone = true; + if (metatable == null) { + return; + } + LuaValue tm = metatable.get(LuaString.latin1("__gc")); + if (tm == null || tm.isNil()) { + return; + } + int func = L.top; + try { + L.ensureSlots(func + 2); + L.stack[func].set(tm); + L.stack[func + 1].setObject(Lua.VUSERDATA, this); + L.top = func + 2; + // 与 LuaTable 同:CIST_FIN 打在发起终结的那一帧上 + boolean wasFin = L.ci.fin; + L.ci.fin = true; + try { + L.callNoYield(func, 0); + } finally { + L.ci.fin = wasFin; + } + } catch (LuaError e) { + // __gc 里的错误一律静默(实测参考实现如此) + } finally { + L.top = func; + } + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/LuaValue.java b/src/main/java/com/github/esrrhs/fakescript/lua/LuaValue.java new file mode 100644 index 0000000..95072b9 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/LuaValue.java @@ -0,0 +1,391 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * Lua 值 —— 对应 {@code lobject.h} 的 {@code TValue}。 + *

+ * 可变槽,和 C 版一样:虚拟机要能原地改写栈槽({@code setobj}), + * 不可变的话每条赋值都要新建对象。代价是必须显式区分「复制值」与「共享槽」, + * 这也是 C 版用 {@code setobj} / {@code setobj2s} 表达的东西。 + *

+ * 载荷字段按类型复用,与 C 的 union 对应: + *

    + *
  • boolean / integer → {@code i}
  • + *
  • float → {@code n}
  • + *
  • string / table / function / userdata / thread → {@code o}
  • + *
+ */ +public final class LuaValue { + + /** 完整标签(含变体),见 {@link Lua} */ + public int tt = Lua.TNIL; + + /** 整数与布尔载荷 */ + public long i; + + /** 浮点载荷 */ + public double n; + + /** 引用载荷:{@link LuaString} / {@link LuaTable} / 闭包 / userdata / 线程 */ + public Object o; + + public LuaValue() { + } + + /* ---------- 构造 ---------- */ + + /** 复制构造:等价于 C 的 {@code setobj(L, dest, src)} */ + public LuaValue(LuaValue src) { + set(src); + } + + public static LuaValue ofNil() { + return new LuaValue(); + } + + public static LuaValue ofBoolean(boolean b) { + LuaValue v = new LuaValue(); + v.setBoolean(b); + return v; + } + + public static LuaValue ofInteger(long x) { + LuaValue v = new LuaValue(); + v.setInteger(x); + return v; + } + + public static LuaValue ofFloat(double x) { + LuaValue v = new LuaValue(); + v.setFloat(x); + return v; + } + + public static LuaValue ofString(LuaString s) { + LuaValue v = new LuaValue(); + v.setString(s); + return v; + } + + public static LuaValue ofTable(LuaTable t) { + LuaValue v = new LuaValue(); + v.setTable(t); + return v; + } + + /** C 函数(轻量 C 函数变体 {@link Lua#VLCF}) */ + public static LuaValue ofFunction(LuaFunction f) { + LuaValue v = new LuaValue(); + v.setObject(Lua.VLCF, f); + return v; + } + + /** Lua 闭包(变体 {@link Lua#VLIF}) */ + public static LuaValue ofClosure(LClosure c) { + LuaValue v = new LuaValue(); + v.setObject(Lua.VLIF, c); + return v; + } + + /** 线程 / 协程({@link Lua#TTHREAD}) */ + public static LuaValue ofThread(LuaState t) { + LuaValue v = new LuaValue(); + v.setObject(Lua.VTHREAD, t); + return v; + } + + /** + * 完整 userdata(变体 {@link Lua#VUSERDATA}):宿主对象进 Lua 值的通道。 + *

+ * 载荷一律包一层 {@link LuaUserdata},好让每个 userdata 带上自己的元表 + * (见 {@link LuaUserdata} 的类注释)。取回载荷用 {@link LuaUserdata#dataOf}。 + */ + public static LuaValue ofUserdata(Object o) { + LuaValue v = new LuaValue(); + v.setObject(Lua.VUSERDATA, new LuaUserdata(o)); + return v; + } + + /** + * 轻量 userdata({@link Lua#TLIGHTUSERDATA})。 + *

+ * 与完整 userdata 的差别是没有元表、不参与回收,载荷就是宿主指针本身。 + * 上游用它给「身份」一类东西打标记——最典型的是 + * {@code debug.upvalueid}:C 版把某个 upvalue 的地址 + * ({@code lua_upvalueid} 返回的 {@code void *})直接推成轻量 userdata, + * 于是「两个闭包共享同一个 upvalue」就等价于「两个 id 原始相等」。 + *

+ * 这里换成 Java 对象引用做身份:Lua 闭包给 {@link UpVal} 对象自己 + * (对应 C 的 {@code *getupvalref(...)},是 {@code UpVal *} 而不是槽的地址), + * C 闭包给那个 upvalue 的 {@code LuaValue} 槽对象(对应 C 的 + * {@code &f->upvalue[n-1]})。槽对象永不被替换,所以身份稳定,与 C 一一对应。 + */ + public static LuaValue ofLightUserdata(Object p) { + LuaValue v = new LuaValue(); + v.setObject(Lua.TLIGHTUSERDATA, p); + return v; + } + + /** C 闭包(变体 {@link Lua#VCCL}) */ + public static LuaValue ofCClosure(CClosure c) { + LuaValue v = new LuaValue(); + v.setObject(Lua.VCCL, c); + return v; + } + + /* ---------- 写入 ---------- */ + + public void set(LuaValue src) { + this.tt = src.tt; + this.i = src.i; + this.n = src.n; + this.o = src.o; + } + + public void setNil() { + tt = Lua.TNIL; + i = 0; + n = 0; + o = null; + } + + public void setBoolean(boolean b) { + tt = Lua.TBOOLEAN; + i = b ? 1 : 0; + n = 0; + o = null; + } + + public void setInteger(long x) { + tt = Lua.VNUMINT; + i = x; + n = 0; + o = null; + } + + public void setFloat(double x) { + tt = Lua.VNUMFLT; + i = 0; + n = x; + o = null; + } + + public void setString(LuaString s) { + tt = s.isShort() ? Lua.VSHRSTR : Lua.VLNGSTR; + i = 0; + n = 0; + o = s; + } + + public void setTable(LuaTable t) { + tt = Lua.TTABLE; + i = 0; + n = 0; + o = t; + } + + /** 直接写入引用载荷,调用方负责保证标签匹配(供函数/userdata/线程使用) */ + public void setObject(int variantTag, Object obj) { + tt = variantTag; + i = 0; + n = 0; + o = obj; + } + + /* ---------- 类型判定 ---------- */ + + public boolean isNil() { + return tt == Lua.TNIL || tt == Lua.TNONE; + } + + public boolean isNone() { + return tt == Lua.TNONE; + } + + public boolean isBoolean() { + return tt == Lua.TBOOLEAN; + } + + /** 数字(整数或浮点) */ + public boolean isNumber() { + return Lua.noVariant(tt) == Lua.TNUMBER; + } + + public boolean isInteger() { + return tt == Lua.VNUMINT; + } + + public boolean isFloat() { + return tt == Lua.VNUMFLT; + } + + public boolean isString() { + return Lua.noVariant(tt) == Lua.TSTRING; + } + + public boolean isTable() { + return tt == Lua.TTABLE; + } + + public boolean isFunction() { + return Lua.noVariant(tt) == Lua.TFUNCTION; + } + + public boolean isThread() { + return tt == Lua.VTHREAD; + } + + public LuaState asThread() { + return (LuaState) o; + } + + public int baseType() { + return Lua.noVariant(tt); + } + + public String typeName() { + return Lua.typeName(tt); + } + + /* ---------- 相等性 ---------- */ + + /** + * {@code lvm.c: luaV_rawequalobj}:不经元方法的原始相等。 + *

+ * 表拿它当键的比较用,所以这里的行为直接决定 {@code t[1]} 与 {@code t[1.0]} + * 是同一个键、而 {@code t[1]} 与 {@code t["1"]} 不是: + *

    + *
  • 基础类型不同 → 不等(所以整数与字符串永不等)
  • + *
  • 数字 → 按数值比较,整数与浮点可以相等({@code 1 == 1.0})
  • + *
  • 字符串 → 逐字节比较
  • + *
  • 表 / 函数 / userdata / 线程 → 引用相等
  • + *
+ * NaN 与自身不等,与 Lua 一致(所以用 NaN 作键每次都会新建条目)。 + */ + public static boolean rawEqual(LuaValue a, LuaValue b) { + if (a.isNil() && b.isNil()) { + return true; + } + if (a.baseType() != b.baseType()) { + return false; + } + switch (a.baseType()) { + case Lua.TNIL: + return true; + case Lua.TBOOLEAN: + return a.i == b.i; + case Lua.TNUMBER: + return numberEqual(a, b); + case Lua.TSTRING: + return a.asString().equals(b.asString()); + default: + return a.o == b.o; // 表/函数/userdata/线程:引用相等 + } + } + + /** + * 数值相等。 + *

+ * 整数与浮点(变体不同)不是「都转 double 再比」。Lua 5.4 的 + * {@code luaV_equalobj} / {@code luaV_rawequalobj} 遇到变体不同的两个数, + * 是两边都按 F2Ieq 转成整数再比;浮点那一侧转不成整数就判不等。 + *

+ * 上一版统一转 double({@code luai_numeq} 的语义),于是 + * {@code rawequal(math.maxinteger, math.maxinteger + 1.0)} 给的是 + * {@code true}:{@code (double)(2^63 - 1)} 舍入成 {@code 2^63},正好撞上 + * 右边那个浮点。实测参考实现给 {@code false}——并且表键也据此判成两个键 + * ({@code t[math.maxinteger]} 与 {@code t[2^63]} 不是同一项)。 + *

+ * 两条规则只在 {@code math.maxinteger} 往前约一千个整数上分岔(那里整数 + * 转 double 会舍入到 {@code 2^63},而 {@code 2^63} 本身又转不回整数), + * 所以只能靠扫边界发现,读 {@code lvm.c} 的宏容易看漏。 + */ + public static boolean numberEqual(LuaValue a, LuaValue b) { + if (a.isInteger() && b.isInteger()) { + return a.i == b.i; + } + if (a.isFloat() && b.isFloat()) { + return a.n == b.n; + } + // 变体不同:把浮点那侧 F2Ieq 转整数,转不成就是不等 + long i = a.isInteger() ? a.i : b.i; + double n = a.isFloat() ? a.n : b.n; + return fltIsExactInt(n) && (long) n == i; + } + + /** {@code luaV_flttointns} 的 F2Ieq 判据:这个浮点精确等于某个整数吗 */ + private static boolean fltIsExactInt(double n) { + if (Double.isNaN(n) || Double.isInfinite(n)) { + return false; + } + if (n >= Long.MIN_VALUE && n < 9223372036854775808.0) { + long k = (long) n; + return (double) k == n; // 必须精确往返,否则 1.5 会被当成 1 + } + return false; + } + + /** + * {@code luaV_flttointns} 的 F2Ieq 模式:浮点只有当精确等于某个整数 + * 时才转换。键的规范化与 {@link #numberEqual} 共用这一份判据——放两处 + * 迟早会有一处漏改。 + */ + public static boolean floatToIntExact(double n, long[] out) { + if (!fltIsExactInt(n)) { + return false; + } + out[0] = (long) n; + return true; + } + + /* ---------- 读取 ---------- */ + + /** + * Lua 的真假判定:只有 {@code nil} 与 {@code false} 为假, + * 其余(含 {@code 0} 与空字符串)全为真 —— 这是 Lua 与 C 最容易搞混的一条。 + */ + public boolean toBoolean() { + return !(isNil() || (tt == Lua.TBOOLEAN && i == 0)); + } + + public boolean asBoolean() { + return i != 0; + } + + public long asInteger() { + return i; + } + + public double asFloat() { + return n; + } + + public LuaString asString() { + return (LuaString) o; + } + + public LuaTable asTable() { + return (LuaTable) o; + } + + @Override + public String toString() { + switch (Lua.noVariant(tt)) { + case Lua.TNIL: + return "nil"; + case Lua.TNONE: + return "none"; + case Lua.TBOOLEAN: + return i != 0 ? "true" : "false"; + case Lua.TNUMBER: + return isInteger() ? Long.toString(i) : Double.toString(n); + case Lua.TSTRING: + return asString().toJavaString(); + case Lua.TTABLE: + return "table: " + System.identityHashCode(o); + case Lua.TFUNCTION: + return "function: " + System.identityHashCode(o); + default: + return Lua.typeName(tt); + } + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/LuaYield.java b/src/main/java/com/github/esrrhs/fakescript/lua/LuaYield.java new file mode 100644 index 0000000..033e3d7 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/LuaYield.java @@ -0,0 +1,27 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * {@code luaD_throw(L, LUA_YIELD)} 的 Java 形态。 + *

+ * C 版用 {@code longjmp} 一跳到底,这里用异常把「挂起」一层层带出 Java 调用栈。 + * 之所以能这么做,是因为Lua 那边的状态全在堆上:栈、调用帧、开放 upvalue + * 都是 {@link LuaState} 里的对象,异常只弹掉 Java 栈帧,Lua 侧原样留着。 + * 等 {@code resume} 接住它,就能从 {@code L.ci} 继续跑。 + *

+ * 两条硬性约束: + *

    + *
  • 不能被 {@code pcall} 捕获。{@code pcall} 只捕 {@link LuaError}, + * 所以 yield 能穿过 {@code pcall} / {@code xpcall}——实测参考实现就是 + * 这样({@code pcall(function() return coroutine.yield(1) end)} 正常挂起);
  • + *
  • 单例、不填栈轨迹。yield 是控制流不是故障,每次 new 一个异常并 + * 抓栈轨迹会让协程比函数调用还慢。
  • + *
+ */ +final class LuaYield extends RuntimeException { + + static final LuaYield INSTANCE = new LuaYield(); + + private LuaYield() { + super(null, null, false, false); + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/MathLib.java b/src/main/java/com/github/esrrhs/fakescript/lua/MathLib.java new file mode 100644 index 0000000..7156763 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/MathLib.java @@ -0,0 +1,625 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * math 库 —— 对应 {@code lmathlib.c}。 + *

+ * 分成三块:普通数学函数(大多一行一个 {@code Math.xxx})、 + * {@code random/randomseed} 的 xoshiro256** 发生器、以及 + * {@code LUA_COMPAT_MATHLIB} 那批兼容函数({@code atan2/pow/log10/frexp/ldexp/cosh/sinh/tanh}, + * 参考实现默认编进去了,实测都存在,所以这里也照给)。 + *

+ * 三处容易写错、且都是「照 C 一行行翻才翻得出来」的地方: + *

    + *
  1. {@code pushnumint}:{@code floor/ceil/modf} 的结果 + * 能表示为整数就返回整数。所以 {@code math.floor(2.0)} 是 + * {@code 2} 不是 {@code 2.0},{@code math.type} 一测就露。 + * 反过来 {@code math.floor(1e300)} 还是 float——装不下。
  2. + *
  3. {@code max/min} 用的是 {@code lua_compare},不是数字比较。 + * {@code math.max("a","b")} 返回 {@code "b"},{@code math.max(1,"b")} + * 报 {@code attempt to compare number with string}。另外算子类型要保 + * 留:{@code math.min(1, 2.0)} 是 {@code 1}(integer)。
  4. + *
  5. 随机数状态是「共享的」:C 里 {@code random} 与 + * {@code randomseed} 是两个 C 闭包,共用同一个 userdata upvalue。 + * 用静态字段存会串到别的 {@code lua_State} 上(本实现里协程是独立 + * {@link LuaState} 但共享 math 表,测试也常并行开几个状态),所以 + * 必须走 C 闭包的 upvalue。
  6. + *
+ *

+ * {@code math.random} 的初始种子在 C 里取自 {@code time(NULL)} 与 + * {@code L} 的地址,本无可比性;这里用 {@code System.currentTimeMillis()} + * 与 {@link System#identityHashCode}。只要显式 + * {@code math.randomseed(n1, n2)},后续序列必须与参考实现逐位一致—— + * 差分测试里就是这么验的。 + *

+ * 一处已知的、无法消除的偏离:超越函数的最后一位。 + * {@code sin/cos/tan/asin/acos/atan/exp/log/log10/sinh/cosh/tanh/pow} + * 走的是 Java 的 {@code Math}(内部是 fdlibm),而参考实现链接的是平台 + * libm(这里是 macOS 的 libsystem_m)。两者都「误差小于 1 ulp」但不是 + * 逐位相同:实测 30 个典型输入里 {@code sin} 差 1 个、{@code tan} 差 12 个、 + * {@code cosh} 差 6 个,且都是差在最后一位。要消掉只能自带一份 libm, + * 那既不现实也超出了「能跑现有 Lua 项目」的目标——Lua 自己在换平台、 + * 换 libm 时同样会漂。因此差分用例对这些函数只比对到 14 位有效数字 + * ({@code %.14g}),1 ulp 是 2.2e-16 相对误差,落在 14 位之下。 + *

+ * 反过来,能对齐的地方就必须对齐:{@code log} 的 base 2/10 分支 + * (见 {@link #log2})与 {@code sqrt/abs/floor/ceil/frexp/ldexp} 都是逐位一致的。 + */ +public final class MathLib { + + /** {@code PI}:与 {@code lmathlib.c} 的 {@code #define PI} 同一个字面量 */ + private static final double PI = 3.141592653589793238462643383279502884; + + /* ==================== xoshiro256** ==================== */ + + /** + * {@code RanState}:四个 64 位字的状态。 + *

+ * C 里是 {@code lua_newuserdatauv} 出来的一块裸内存,四个 {@code Rand64} + * (无符号 64 位)。Java 没有无符号 long,但 {@code + - * ^ <<} + * 在补码下逐位等价,只有右移和比较要换成 + * {@code >>>} / {@code Long.compareUnsigned},这两处已经单独标出。 + */ + private static final class RanState { + final long[] s = new long[4]; + } + + /** {@code rotl}:循环左移。右移必须是逻辑右移 */ + private static long rotl(long x, int n) { + return (x << n) | (x >>> (64 - n)); + } + + /** {@code nextrand}:xoshiro256** 的一步,就地更新状态并返回新值 */ + private static long nextrand(long[] s) { + long state0 = s[0]; + long state1 = s[1]; + long state2 = s[2] ^ state0; + long state3 = s[3] ^ state1; + long res = rotl(state1 * 5, 7) * 9; + s[0] = state0 ^ state3; + s[1] = state1 ^ state2; + s[2] = state2 ^ (state1 << 17); + s[3] = rotl(state3, 45); + return res; + } + + /** + * {@code I2d}:把随机整数的高 {@code FIGS} 位变成 {@code [0,1)} 的浮点。 + *

+ * {@code FIGS} 是 double 的尾数位数(53),所以丢弃低 11 位。 + * 先按逻辑右移再转 signed,再乘 {@code 2^-53};若转出来是负数 + * (说明被截的高位是 1),补一个 1.0 —— 这正是 C 里 + * {@code if (sx < 0) res += 1.0} 那句,照抄即可,别自作聪明用 + * {@code Math.abs}。 + */ + private static double i2d(long x) { + long sx = x >>> 11; + double res = (double) sx * (0.5 / (1L << 52)); + if (sx < 0) { + res += 1.0; + } + return res; + } + + /** + * {@code project}:把随机整数投到 {@code [0, n]}。 + *

+ * {@code n+1} 是 2 的幂时可以直接掩码(无偏);否则先扩成不小于 + * {@code n} 的最小梅森数 {@code lim},投进 {@code [0,lim]} 后落在 + * {@code [0,n]} 外就换一个数重投。写成简单的 {@code ran % (n+1)} + * 会有取模偏差——虽然结果仍是「看着随机」,但与参考实现逐位对不上。 + */ + private static long project(long ran, long n, long[] s) { + if ((n & (n + 1)) == 0) { // n + 1 是 2 的幂 + return ran & n; + } + long lim = n; + lim |= (lim >>> 1); + lim |= (lim >>> 2); + lim |= (lim >>> 4); + lim |= (lim >>> 8); + lim |= (lim >>> 16); + lim |= (lim >>> 32); + ran &= lim; + while (Long.compareUnsigned(ran, n) > 0) { + ran = nextrand(s); + ran &= lim; + } + return ran; + } + + /** {@code setseed}:装种子后先空转 16 次把位「摊开」 */ + private static void setseed(LuaState L, long[] s, long n1, long n2) { + s[0] = n1; + s[1] = 0xff; // avoid a zero state + s[2] = n2; + s[3] = 0; + for (int i = 0; i < 16; i++) { + nextrand(s); + } + L.push(LuaValue.ofInteger(n1)); + L.push(LuaValue.ofInteger(n2)); + } + + /** {@code randseed}:无参 {@code randomseed} 用的「随机」种子 */ + private static void randseed(LuaState L, long[] s) { + long seed1 = System.currentTimeMillis() / 1000; + long seed2 = System.identityHashCode(L); + setseed(L, s, seed1, seed2); + } + + /* ==================== 注册 ==================== */ + + public static void open(LuaState L) { + LuaTable m = new LuaTable(); + + setFn(m, "abs", MathLib::abs); + setFn(m, "acos", MathLib::acos); + setFn(m, "asin", MathLib::asin); + setFn(m, "atan", MathLib::atan); + setFn(m, "ceil", MathLib::ceil); + setFn(m, "cos", MathLib::cos); + setFn(m, "deg", MathLib::deg); + setFn(m, "exp", MathLib::exp); + setFn(m, "tointeger", MathLib::tointeger); + setFn(m, "floor", MathLib::floor); + setFn(m, "fmod", MathLib::fmod); + setFn(m, "ult", MathLib::ult); + setFn(m, "log", MathLib::log); + setFn(m, "max", MathLib::max); + setFn(m, "min", MathLib::min); + setFn(m, "modf", MathLib::modf); + setFn(m, "rad", MathLib::rad); + setFn(m, "sin", MathLib::sin); + setFn(m, "sqrt", MathLib::sqrt); + setFn(m, "tan", MathLib::tan); + setFn(m, "type", MathLib::type); + // LUA_COMPAT_MATHLIB:参考实现默认编进去的兼容函数 + setFn(m, "atan2", MathLib::atan); + setFn(m, "cosh", MathLib::cosh); + setFn(m, "sinh", MathLib::sinh); + setFn(m, "tanh", MathLib::tanh); + setFn(m, "pow", MathLib::pow); + setFn(m, "frexp", MathLib::frexp); + setFn(m, "ldexp", MathLib::ldexp); + setFn(m, "log10", MathLib::log10); + + m.set("pi", LuaValue.ofFloat(PI)); + m.set("huge", LuaValue.ofFloat(Double.POSITIVE_INFINITY)); + m.set("maxinteger", LuaValue.ofInteger(Long.MAX_VALUE)); + m.set("mininteger", LuaValue.ofInteger(Long.MIN_VALUE)); + + // random / randomseed:两个 C 闭包共用一个 RanState upvalue + RanState st = new RanState(); + randseed(L, st.s); + L.top -= 2; // 丢掉 randseed 压上来的两个种子 + LuaValue uv = LuaValue.ofUserdata(st); + m.set("random", LuaValue.ofCClosure(closure(MathLib::random, uv))); + m.set("randomseed", LuaValue.ofCClosure(closure(MathLib::randomseed, uv))); + + LuaAux.registerLib(L, "math", m); + } + + private static CClosure closure(LuaFunction f, LuaValue uv) { + CClosure c = new CClosure(f, 1); + c.upvalue[0] = uv; + return c; + } + + private static RanState ranState(LuaState L) { + return (RanState) LuaUserdata.dataOf(L.upvalue(0)); + } + + /* ==================== 一元 / 二元数学函数 ==================== */ + + private static int abs(LuaState L) { + LuaValue v = L.arg1(1); + if (v.isInteger()) { + // C 里写成 (lua_Integer)(0u - (lua_Unsigned)n) 是为了避开有符号取负 + // 的 UB;Java 里 -Long.MIN_VALUE 同样还是 MIN_VALUE,结果一致 + L.push(LuaValue.ofInteger(v.i < 0 ? -v.i : v.i)); + } else { + L.push(LuaValue.ofFloat(Math.abs(LuaAux.checkNumber(L, 1)))); + } + return 1; + } + + private static int sin(LuaState L) { + L.push(LuaValue.ofFloat(Math.sin(LuaAux.checkNumber(L, 1)))); + return 1; + } + + private static int cos(LuaState L) { + L.push(LuaValue.ofFloat(Math.cos(LuaAux.checkNumber(L, 1)))); + return 1; + } + + private static int tan(LuaState L) { + L.push(LuaValue.ofFloat(Math.tan(LuaAux.checkNumber(L, 1)))); + return 1; + } + + private static int asin(LuaState L) { + L.push(LuaValue.ofFloat(Math.asin(LuaAux.checkNumber(L, 1)))); + return 1; + } + + private static int acos(LuaState L) { + L.push(LuaValue.ofFloat(Math.acos(LuaAux.checkNumber(L, 1)))); + return 1; + } + + /** {@code math.atan(y [, x])}:给第二个参数就是 {@code atan2} */ + private static int atan(LuaState L) { + double y = LuaAux.checkNumber(L, 1); + double x = LuaAux.optNumber(L, 2, 1.0); + L.push(LuaValue.ofFloat(Math.atan2(y, x))); + return 1; + } + + private static int sqrt(LuaState L) { + L.push(LuaValue.ofFloat(Math.sqrt(LuaAux.checkNumber(L, 1)))); + return 1; + } + + private static int exp(LuaState L) { + L.push(LuaValue.ofFloat(Math.exp(LuaAux.checkNumber(L, 1)))); + return 1; + } + + private static int deg(LuaState L) { + L.push(LuaValue.ofFloat(LuaAux.checkNumber(L, 1) * (180.0 / PI))); + return 1; + } + + private static int rad(LuaState L) { + L.push(LuaValue.ofFloat(LuaAux.checkNumber(L, 1) * (PI / 180.0))); + return 1; + } + + /** + * {@code math.log(x [, base])}。 + *

+ * base 为 2 / 10 时走专用函数而不是 {@code log(x)/log(base)}——C 用的是 + * {@code log2} 与 {@code log10},与「两次 {@code log} 相除」不是同一个 + * 结果:实测 43 个样本里 {@code log(x)/log(2)} 有 9 个与 {@code log2(x)} + * 差 1 ulp,base 10 更是差到 25 个。所以这两条分支必须真的分开写。 + *

+ * Java 没有 {@code Math.log2},这里用尾数分解补出来(见 {@link #log2}): + * 分解后 192 个样本只差 2 个,而朴素除法差 21%。 + */ + private static int log(LuaState L) { + double x = LuaAux.checkNumber(L, 1); + double res; + if (L.isNoneArg(2) || L.arg1(2).isNil()) { + res = Math.log(x); + } else { + double base = LuaAux.checkNumber(L, 2); + if (base == 2.0) { + res = log2(x); + } else if (base == 10.0) { + res = Math.log10(x); + } else { + res = Math.log(x) / Math.log(base); + } + } + L.push(LuaValue.ofFloat(res)); + return 1; + } + + /** {@code ln 2},取到 double 能表示的最高精度 */ + private static final double LN2 = 0.693147180559945309417232121458176568; + + /** + * {@code log2(x)}:Java 标准库没有,用尾数分解补。 + *

+ * 把 {@code x} 写成 {@code m * 2^e}({@code m ∈ [0.5,1)}),则 + * {@code log2(x) = e + log(m)/ln2}。这样 {@code log} 只在 {@code [0.5,1)} + * 这个最平缓的区间上取值,误差被压到最低——直接 {@code log(x)/log(2)} + * 会把 {@code x} 本身的量级误差一起带进来。 + *

+ * 非有限值与非正数一律退回 {@code log(x)/ln2}:{@code 0 → -inf}、 + * {@code -1 → nan}、{@code inf → inf} 都正好对得上,而尾数分解在这几处 + * 会算出 1024 这种荒唐值。 + */ + private static double log2(double x) { + if (Double.isNaN(x) || Double.isInfinite(x) || x <= 0.0) { + return Math.log(x) / LN2; + } + long bits = Double.doubleToRawLongBits(x); + int e = (int) ((bits >>> 52) & 0x7ff); + if (e == 0) { // 次正规数:先放大成正规数,指数再扣回去 + x *= 0x1p+64; + bits = Double.doubleToRawLongBits(x); + e = (int) ((bits >>> 52) & 0x7ff) - 64; + } + double m = Double.longBitsToDouble((bits & 0x800fffffffffffffL) | (1022L << 52)); + return (e - 1022) + Math.log(m) / LN2; + } + + /** {@code math.ult}:按无符号比较,所以 {@code ult(-1, 2)} 是 false */ + private static int ult(LuaState L) { + long a = LuaAux.checkInteger(L, 1); + long b = LuaAux.checkInteger(L, 2); + L.push(LuaValue.ofBoolean(Long.compareUnsigned(a, b) < 0)); + return 1; + } + + /** + * {@code math.tointeger}:能精确取到整数才给,否则 nil。 + *

+ * 注意这里是 {@code lua_tointegerx} 而不是 {@code luaV_tointegerns}—— + * 带字符串转换:{@code math.tointeger('7') == 7}。库函数参数走的是 + * {@code luaL_check*} 那一套(会做类型转换),与 VM 算术指令刻意不转换 + * 的做法相反,别把两者写成同一个。 + */ + private static int tointeger(LuaState L) { + LuaValue v = L.arg1(1); + LuaAux.checkAny(L, 1); + // 不能写成三元表达式:一边是 long、一边是 Long 会被自动拆箱, + // 返回 null 的那次直接在拆箱处 NPE + Long i; + if (v.isInteger()) { + i = Long.valueOf(v.i); + } else { + i = Vm.toInteger(v, Arith.F2I_EQ); + } + if (i == null) { + LuaAux.pushFail(L); + } else { + L.push(LuaValue.ofInteger(i)); + } + return 1; + } + + /** {@code math.type}:子类型探针 */ + private static int type(LuaState L) { + LuaValue v = L.arg1(1); + if (v.isInteger()) { + L.push(LuaValue.ofString(LuaString.latin1("integer"))); + } else if (v.isFloat()) { + L.push(LuaValue.ofString(LuaString.latin1("float"))); + } else { + LuaAux.checkAny(L, 1); + LuaAux.pushFail(L); + } + return 1; + } + + /** {@code pushnumint}:能表示为整数就返回整数,否则返回浮点 */ + private static void pushIntOrFloat(LuaState L, double d) { + Long i = Arith.fltToInteger(d, Arith.F2I_EQ); + L.push((i == null) ? LuaValue.ofFloat(d) : LuaValue.ofInteger(i)); + } + + /** {@code math.floor}:整数就是自己的 floor,原样返回(连子类型一起) */ + private static int floor(LuaState L) { + LuaValue v = L.arg1(1); + if (v.isInteger()) { + L.push(v); + } else { + pushIntOrFloat(L, Math.floor(LuaAux.checkNumber(L, 1))); + } + return 1; + } + + private static int ceil(LuaState L) { + LuaValue v = L.arg1(1); + if (v.isInteger()) { + L.push(v); + } else { + pushIntOrFloat(L, Math.ceil(LuaAux.checkNumber(L, 1))); + } + return 1; + } + + /** + * {@code math.modf}:返回整数部分与小数部分两个值。 + *

+ * 整数入参时整数部分就是它自己、小数部分是 {@code 0.0}(浮点零, + * 实测 {@code math.modf(3)} 输出 {@code 3 0.0})。 + * {@code n == ip} 那个判断是给 inf 用的:{@code inf - inf} 是 nan, + * 而 {@code math.modf(math.huge)} 的第二个返回值是 {@code 0.0}。 + */ + private static int modf(LuaState L) { + LuaValue v = L.arg1(1); + if (v.isInteger()) { + L.push(v); + L.push(LuaValue.ofFloat(0.0)); + } else { + double n = LuaAux.checkNumber(L, 1); + double ip = (n < 0) ? Math.ceil(n) : Math.floor(n); + pushIntOrFloat(L, ip); + L.push(LuaValue.ofFloat((n == ip) ? 0.0 : (n - ip))); + } + return 2; + } + + /** + * {@code math.fmod}:两个整数就走整数取模,否则走 C 的 {@code fmod}。 + *

+ * 两条路径都不能省:整数路径给出 integer 结果({@code math.fmod(7,3)} + * 是 {@code 1}),浮点路径给出 float 结果({@code math.fmod(7.5,2)} + * 是 {@code 1.5})——统一成浮点会让 {@code math.type} 露馅。 + *

+ * 浮点路径用的是 C 的 {@code fmod}(向零取整,结果取被除数 + * 的符号),不是 Lua 的 {@code %}({@code Arith.modf},结果取 + * 除数的符号)。两者的差就摆在 {@code math.fmod(-7.5, 2)} 上: + * 参考实现给 {@code -1.5},照 Lua 的 {@code %} 写会给出 {@code 0.5}。 + *

+ * {@code -1} 与 {@code 0} 两个除数是特例:0 要报错,-1 要给 0 以避开 + * {@code Long.MIN_VALUE / -1} 的溢出。 + */ + private static int fmod(LuaState L) { + LuaValue a = L.arg1(1); + LuaValue b = L.arg1(2); + if (a.isInteger() && b.isInteger()) { + long d = b.i; + if (d == 0) { + throw L.argError(2, "zero"); + } + if (d == -1) { + L.push(LuaValue.ofInteger(0)); + } else { + L.push(LuaValue.ofInteger(a.i % d)); + } + return 1; + } + double x = LuaAux.checkNumber(L, 1); + double y = LuaAux.checkNumber(L, 2); + // Java 的 double % 就是 C 的 fmod(向零取整、取被除数符号), + // 与 Arith.modf(Lua 的 %,取除数符号)不是一回事 + L.push(LuaValue.ofFloat(x % y)); + return 1; + } + + /** + * {@code math.max}:走 {@code lua_compare(LUA_OPLT)},保留操作数的子类型。 + *

+ * 所以 {@code math.max(1, 2.0)} 是 {@code 2.0}(float), + * {@code math.max(1, 2)} 是 {@code 2}(integer),而 + * {@code math.max("a","b")} 是 {@code "b"}。 + */ + private static int max(LuaState L) { + int n = L.argCount(); + if (n < 1) { + throw L.argError(1, "value expected"); + } + LuaValue best = L.arg(0); + for (int i = 1; i < n; i++) { + LuaValue v = L.arg(i); + if (Vm.lessThan(L, best, v)) { + best = v; + } + } + L.push(best); + return 1; + } + + private static int min(LuaState L) { + int n = L.argCount(); + if (n < 1) { + throw L.argError(1, "value expected"); + } + LuaValue best = L.arg(0); + for (int i = 1; i < n; i++) { + LuaValue v = L.arg(i); + if (Vm.lessThan(L, v, best)) { + best = v; + } + } + L.push(best); + return 1; + } + + /* ==================== LUA_COMPAT_MATHLIB ==================== */ + + private static int cosh(LuaState L) { + L.push(LuaValue.ofFloat(Math.cosh(LuaAux.checkNumber(L, 1)))); + return 1; + } + + private static int sinh(LuaState L) { + L.push(LuaValue.ofFloat(Math.sinh(LuaAux.checkNumber(L, 1)))); + return 1; + } + + private static int tanh(LuaState L) { + L.push(LuaValue.ofFloat(Math.tanh(LuaAux.checkNumber(L, 1)))); + return 1; + } + + private static int pow(LuaState L) { + double x = LuaAux.checkNumber(L, 1); + double y = LuaAux.checkNumber(L, 2); + L.push(LuaValue.ofFloat(Math.pow(x, y))); + return 1; + } + + /** {@code math.frexp}:拆成 {@code m * 2^e},{@code m ∈ [0.5,1)} */ + private static int frexp(LuaState L) { + double x = LuaAux.checkNumber(L, 1); + int e; + if (x == 0.0 || Double.isNaN(x) || Double.isInfinite(x)) { + e = 0; + } else { + long bits = Double.doubleToRawLongBits(x); + int exp = (int) ((bits >>> 52) & 0x7ff); + if (exp == 0) { // 次正规数:先放大成正规数,再从指数里扣回去 + x *= 0x1p+64; + bits = Double.doubleToRawLongBits(x); + exp = (int) ((bits >>> 52) & 0x7ff) - 64; + } + bits = (bits & 0x800fffffffffffffL) | (1022L << 52); + x = Double.longBitsToDouble(bits); + e = exp - 1022; + } + L.push(LuaValue.ofFloat(x)); + L.push(LuaValue.ofInteger(e)); + return 2; + } + + private static int ldexp(LuaState L) { + double x = LuaAux.checkNumber(L, 1); + int ep = (int) LuaAux.checkInteger(L, 2); + L.push(LuaValue.ofFloat(Math.scalb(x, ep))); + return 1; + } + + private static int log10(LuaState L) { + L.push(LuaValue.ofFloat(Math.log10(LuaAux.checkNumber(L, 1)))); + return 1; + } + + /* ==================== random ==================== */ + + /** {@code math.random([m [, n]])} */ + private static int random(LuaState L) { + long[] s = ranState(L).s; + long rv = nextrand(s); // 不管用不用得到,状态都要前进一步 + int n = L.argCount(); + long low; + long up; + switch (n) { + case 0: + L.push(LuaValue.ofFloat(i2d(rv))); + return 1; + case 1: + low = 1; + up = LuaAux.checkInteger(L, 1); + if (up == 0) { // 单个 0:给一个完整的随机整数 + L.push(LuaValue.ofInteger(rv)); + return 1; + } + break; + case 2: + low = LuaAux.checkInteger(L, 1); + up = LuaAux.checkInteger(L, 2); + break; + default: + throw L.libError("wrong number of arguments"); + } + if (low > up) { + throw L.argError(1, "interval is empty"); + } + long p = project(rv, up - low, s); + L.push(LuaValue.ofInteger(p + low)); + return 1; + } + + /** {@code math.randomseed([n1 [, n2]])}:返回装进去的两个种子 */ + private static int randomseed(LuaState L) { + long[] s = ranState(L).s; + if (L.isNoneArg(1)) { + randseed(L, s); + } else { + long n1 = LuaAux.checkInteger(L, 1); + long n2 = LuaAux.optInteger(L, 2, 0); + setseed(L, s, n1, n2); + } + return 2; + } + + private static void setFn(LuaTable t, String name, LuaFunction f) { + t.set(name, LuaValue.ofFunction(f)); + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/Numeral.java b/src/main/java/com/github/esrrhs/fakescript/lua/Numeral.java new file mode 100644 index 0000000..032223b --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/Numeral.java @@ -0,0 +1,312 @@ +package com.github.esrrhs.fakescript.lua; + +import java.util.regex.Pattern; + +/** + * 数字字面量的解析 —— 对应 {@code lobject.c} 里的 {@code luaO_str2num}、 + * {@code l_str2int}、{@code l_str2d}、{@code lua_strx2number} 与 {@code luaO_utf8esc}。 + *

+ * 这一段是整个词法层最容易「差不多就行」、然后悄悄错的地方,两条规则必须精确: + *

    + *
  1. 先按整数解析,失败才按浮点({@code luaO_str2num})。这决定了 + * {@code 1} 是 integer 而 {@code 1.0} 是 float,进而决定 + * {@code math.type} 与 {@code //} 的行为;
  2. + *
  3. 十六进制整数字面量是 integer,带 p 指数的十六进制是 float。 + * {@code 0x10} 走 {@code l_str2int} 得到整数 16,而 {@code 0x1p4} + * 因为末尾的 'p' 让 {@code l_str2int} 失败,落到浮点路径得到 16.0。
  4. + *
+ * 两个细节与 Java 直觉相反,所以不能直接用 {@code Double.parseDouble} 顶上: + *
    + *
  • Java 的 {@code parseDouble} 接受 {@code "1d"} / {@code "1f"} 这类 + * 类型后缀,C 的 {@code strtod} 不收——而 Lua 正是靠「数字后面紧跟字母 + * 要报错」来拒绝 {@code 1d} 这种写法的;
  • + *
  • Java 的 {@code parseDouble} 接受 {@code "inf"} / {@code "nan"}, + * Lua 明确拒绝({@code l_str2d} 里 mode == 'n' 直接返回失败)。
  • + *
+ */ +final class Numeral { + + private Numeral() { + } + + /** {@code luaO_hexavalue} */ + static int hexaValue(int c) { + if (LCType.isDigit(c)) { + return c - '0'; + } + return (LCType.toLower(c) - 'a') + 10; + } + + /** 最多读取的有效数字位数(避免溢出) */ + private static final int MAXSIGDIG = 30; + + /** {@code UTF8BUFFSZ} */ + public static final int UTF8BUFFSZ = 8; + + /** + * {@code luaO_str2num}:把数字字面量转成 Lua 值。 + * + * @return 整数或浮点值;格式错误返回 {@code null} + */ + static LuaValue str2num(String s) { + Long i = str2int(s); + if (i != null) { + return LuaValue.ofInteger(i); + } + Double d = str2d(s); + if (d != null) { + return LuaValue.ofFloat(d); + } + return null; + } + + /* ---------- 整数路径 ---------- */ + + private static final long MAXBY10 = Long.MAX_VALUE / 10; + private static final int MAXLASTD = (int) (Long.MAX_VALUE % 10); + + /** {@code l_str2int}:解析十进制或十六进制整数;溢出或格式不符返回 null */ + static Long str2int(String s) { + int p = 0; + boolean neg = false; + while (p < s.length() && LCType.isSpace(s.charAt(p))) { + p++; + } + if (p < s.length() && s.charAt(p) == '-') { + p++; + neg = true; + } else if (p < s.length() && s.charAt(p) == '+') { + p++; + } + long a = 0; + boolean empty = true; + if (p + 1 < s.length() && s.charAt(p) == '0' && (s.charAt(p + 1) == 'x' || s.charAt(p + 1) == 'X')) { + p += 2; + while (p < s.length() && LCType.isXdigit(s.charAt(p))) { + a = a * 16 + hexaValue(s.charAt(p)); + p++; + empty = false; + } + } else { + while (p < s.length() && LCType.isDigit(s.charAt(p))) { + int d = s.charAt(p) - '0'; + /* + * 累加器在 C 里是 {@code lua_Unsigned},比较必须按无符号做。 + * + * 有符号的 {@code long} 在这里会漏判:{@code a} 一旦越过 2^63 + * 就变成负数,{@code a >= MAXBY10} 从此恒为假,溢出检查整个失效。 + * 命中条件是「前 19 位已经 ≥ 2^63 且还有下一位」—— + * {@code "-92233720368547758080"}(= -10 * 2^63)正是这一类: + * 它按无符号一路回绕,最后恰好等于 0,于是 + * {@code 1 + "-92233720368547758080"} 给出 1, + * 而参考实现先拒绝它、落到浮点路径,给出 -9.2233720368548e+19。 + * + * 负数的下界比正数多一个(Long.MIN_VALUE),故阈值随符号放宽 1。 + */ + if (Long.compareUnsigned(a, MAXBY10) >= 0 + && (Long.compareUnsigned(a, MAXBY10) > 0 + || d > MAXLASTD + (neg ? 1 : 0))) { + return null; + } + a = a * 10 + d; + p++; + empty = false; + } + } + while (p < s.length() && LCType.isSpace(s.charAt(p))) { + p++; + } + if (empty || p != s.length()) { + return null; + } + return neg ? -a : a; + } + + /* ---------- 浮点路径 ---------- */ + + /** + * {@code l_str2d}。 + *

+ * C 版在首次解析失败后还有一段「按当前 locale 的小数点再试一次」的回退。 + * Java 的 {@code Double.parseDouble} 只认 '.',不存在 locale 小数点, + * 因此那段回退在这里没有对应物——不需要也不该造一个。 + */ + static Double str2d(String s) { + int mode = 0; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '.' || c == 'x' || c == 'X' || c == 'n' || c == 'N') { + mode = LCType.toLower(c); + break; + } + } + if (mode == 'n') { + return null; // 拒绝 inf / nan + } + return str2dLoc(s, mode); + } + + /** 严格匹配 C strtod 接受的十进制浮点形状(不含 d/f 后缀、不含 inf/nan) */ + private static final Pattern DECIMAL = Pattern + .compile("[+-]?((\\d+(\\.\\d*)?)|(\\.\\d+))([eE][+-]?\\d+)?"); + + private static Double str2dLoc(String s, int mode) { + if (mode == 'x') { + double[] out = new double[1]; + int[] end = new int[1]; + strx2number(s, out, end); + if (end[0] == 0) { + return null; // endptr == s,一个字符都没认出来 + } + int e = end[0]; + while (e < s.length() && LCType.isSpace(s.charAt(e))) { + e++; + } + return (e == s.length()) ? out[0] : null; + } + /* + * 不能用 {@code String.trim()}:Java 的 {@code trim} 去掉的是 + * 「小于等于 U+0020 的字符」,连 {@code \0} 一起去掉,而 C 版 + * {@code l_str2d} 只认 {@code SPACECHARS}(" \f\n\r\t\v")。 + * 于是 {@code tonumber('1\0')} 会被这里算成 1.0——参考实现给的是 + * {@code nil}(上游 {@code math.lua} 有一整条断言对着它)。 + * 这里手工按 {@code SPACECHARS} 掐头去尾,剩下的子串里若还有别的 + * 字符(包括 {@code \0}),正则就匹配不上。 + */ + int sp = 0; + while (sp < s.length() && LCType.isSpace(s.charAt(sp))) { + sp++; + } + int se = s.length(); + while (se > sp && LCType.isSpace(s.charAt(se - 1))) { + se--; + } + String t = s.substring(sp, se); + if (!DECIMAL.matcher(t).matches()) { + return null; + } + try { + return Double.valueOf(t); + } catch (NumberFormatException ex) { + return null; + } + } + + /** + * {@code lua_strx2number}:按 C99 的 {@code strtod} 规范解析十六进制浮点。 + *

+ * 与标准 C99 的一处差别:指数部分可以省略({@code 0x1.8} 合法)。 + * 这是 Lua 自己的实现,不是标准行为,必须照搬。 + *

+ * {@code end} 回填的是「已成功解析到的位置」,用来判断字面量尾部是否还有 + * 垃圾字符。指数部分格式错误时会提前返回,但 {@code end} 保持在数字部分 + * 的末尾——于是 {@code str2dLoc} 会因为「尾部还有字符」而判失败, + * 与 C 版一致。 + */ + private static void strx2number(String s, double[] out, int[] end) { + int p = 0; + double r = 0.0; + int sigdig = 0; + int nosigdig = 0; + int e = 0; + boolean neg = false; + boolean hasdot = false; + end[0] = 0; // endptr = s + while (p < s.length() && LCType.isSpace(s.charAt(p))) { + p++; + } + if (p < s.length() && s.charAt(p) == '-') { + p++; + neg = true; + } else if (p < s.length() && s.charAt(p) == '+') { + p++; + } + if (!(p + 1 < s.length() && s.charAt(p) == '0' && (s.charAt(p + 1) == 'x' || s.charAt(p + 1) == 'X'))) { + out[0] = 0.0; + return; // 没有 0x 前缀 + } + p += 2; + for (; p < s.length(); p++) { + char c = s.charAt(p); + if (c == '.') { + if (hasdot) { + break; // 第二个小数点,停 + } + hasdot = true; + } else if (LCType.isXdigit(c)) { + if (sigdig == 0 && c == '0') { + nosigdig++; // 前导零不算有效位 + } else if (++sigdig <= MAXSIGDIG) { + r = (r * 16.0) + hexaValue(c); + } else { + e++; // 位数过多,忽略但仍计入指数 + } + if (hasdot) { + e--; + } + } else { + break; + } + } + if (nosigdig + sigdig == 0) { + out[0] = 0.0; + return; // 一个数字都没有 + } + end[0] = p; // 到此为止是有效的 + e *= 4; + if (p < s.length() && (s.charAt(p) == 'p' || s.charAt(p) == 'P')) { + int exp1 = 0; + boolean neg1 = false; + p++; // 跳过 'p' + if (p < s.length() && s.charAt(p) == '-') { + p++; + neg1 = true; + } else if (p < s.length() && s.charAt(p) == '+') { + p++; + } + if (!(p < s.length() && LCType.isDigit(s.charAt(p)))) { + // 指数必须至少有一位数字。注意:此处不更新 end, + // 于是调用方会因「尾部还有字符」而判失败 + out[0] = 0.0; + return; + } + while (p < s.length() && LCType.isDigit(s.charAt(p))) { + exp1 = exp1 * 10 + (s.charAt(p) - '0'); + p++; + } + if (neg1) { + exp1 = -exp1; + } + e += exp1; + end[0] = p; + } + if (neg) { + r = -r; + } + out[0] = Math.scalb(r, e); + } + + /* ---------- UTF-8 转义 ---------- */ + + /** + * {@code luaO_utf8esc}:把码点编码成 UTF-8,从缓冲区尾部往前填。 + * + * @return 写入的字节数 {@code n},字节位于 {@code buff[len-n .. len-1]} + */ + static int utf8esc(long x, byte[] buff) { + int len = buff.length; + int n = 1; + if (x < 0x80) { + buff[len - 1] = (byte) x; + } else { + int mfb = 0x3f; // 首字节还能放下的最大值 + do { + buff[len - (n++)] = (byte) (0x80 | (x & 0x3f)); + x >>= 6; + mfb >>= 1; + } while (x > mfb); + buff[len - n] = (byte) ((~mfb << 1) | x); + } + return n; + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/OpCode.java b/src/main/java/com/github/esrrhs/fakescript/lua/OpCode.java new file mode 100644 index 0000000..9b2da75 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/OpCode.java @@ -0,0 +1,458 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * 指令集与指令编解码 —— 逐行翻译 {@code lopcodes.h} / {@code lopcodes.c}。 + *

+ * 指令是 32 位字,用 Java {@code int} 承载。两处必须小心的差别: + *

    + *
  1. C 版的 {@code Instruction} 是无符号 32 位,Java 的 {@code int} + * 是有符号的。所有取字段一律用 {@code >>>}(无符号右移)再掩码, + * 否则最高位那个 1 会带着符号扩展进来;
  2. + *
  3. 带符号字段用「偏移 K」表示:写入值 = 真值 + K,K 是对应无符号字段 + * 最大值的一半({@code sBx} 的 K 是 65535,{@code sC} 的 K 是 127)。
  4. + *
+ *

+ * 指令格式(与上游一致): + *

+ * iABC          C(8)     |      B(8)     |k|     A(8)      |   Op(7)
+ * iABx                Bx(17)               |     A(8)      |   Op(7)
+ * iAsBx              sBx (signed)(17)      |     A(8)      |   Op(7)
+ * iAx                           Ax(25)                     |   Op(7)
+ * isJ                           sJ (signed)(25)            |   Op(7)
+ * 
+ */ +public final class OpCode { + + private OpCode() { + } + + /* ---------- 指令格式 ---------- */ + + public static final int IABC = 0; + public static final int IABX = 1; + public static final int IASBX = 2; + public static final int IAX = 3; + public static final int ISJ = 4; + + public static final int SIZE_C = 8; + public static final int SIZE_B = 8; + public static final int SIZE_BX = SIZE_C + SIZE_B + 1; // 17 + public static final int SIZE_A = 8; + public static final int SIZE_AX = SIZE_BX + SIZE_A; // 25 + public static final int SIZE_SJ = SIZE_BX + SIZE_A; // 25 + public static final int SIZE_OP = 7; + + public static final int POS_OP = 0; + public static final int POS_A = POS_OP + SIZE_OP; // 7 + public static final int POS_K = POS_A + SIZE_A; // 15 + public static final int POS_B = POS_K + 1; // 16 + public static final int POS_C = POS_B + SIZE_B; // 24 + public static final int POS_BX = POS_K; // 15 + public static final int POS_AX = POS_A; // 7 + public static final int POS_SJ = POS_A; // 7 + + public static final int MAXARG_BX = (1 << SIZE_BX) - 1; + public static final int OFFSET_SBX = MAXARG_BX >> 1; + public static final int MAXARG_AX = (1 << SIZE_AX) - 1; + public static final int MAXARG_SJ = (1 << SIZE_SJ) - 1; + public static final int OFFSET_SJ = MAXARG_SJ >> 1; + public static final int MAXARG_A = (1 << SIZE_A) - 1; + public static final int MAXARG_B = (1 << SIZE_B) - 1; + public static final int MAXARG_C = (1 << SIZE_C) - 1; + public static final int OFFSET_SC = MAXARG_C >> 1; + + /** {@code int2sC} */ + public static int int2sC(int i) { + return i + OFFSET_SC; + } + + /** {@code sC2int} */ + public static int sC2int(int i) { + return i - OFFSET_SC; + } + + /** {@code NO_REG}:一个能塞进 8 位的非法寄存器号 */ + public static final int NO_REG = MAXARG_A; + + /** {@code LFIELDS_PER_FLUSH}:构造表时攒多少个字段才发一次 SETLIST */ + public static final int LFIELDS_PER_FLUSH = 50; + + /* ---------- 操作码(ORDER OP,顺序与上游 enum 严格一致) ---------- */ + + public static final int OP_MOVE = 0; /* A B R[A] := R[B] */ + public static final int OP_LOADI = 1; /* A sBx R[A] := sBx */ + public static final int OP_LOADF = 2; /* A sBx R[A] := (float)sBx */ + public static final int OP_LOADK = 3; /* A Bx R[A] := K[Bx] */ + public static final int OP_LOADKX = 4; /* A R[A] := K[extra arg] */ + public static final int OP_LOADFALSE = 5; /* A R[A] := false */ + public static final int OP_LFALSESKIP = 6; /* A R[A] := false; pc++ */ + public static final int OP_LOADTRUE = 7; /* A R[A] := true */ + public static final int OP_LOADNIL = 8; /* A B R[A], R[A+1], ..., R[A+B] := nil */ + public static final int OP_GETUPVAL = 9; /* A B R[A] := UpValue[B] */ + public static final int OP_SETUPVAL = 10; /* A B UpValue[B] := R[A] */ + public static final int OP_GETTABUP = 11; /* A B C R[A] := UpValue[B][K[C]:string] */ + public static final int OP_GETTABLE = 12; /* A B C R[A] := R[B][R[C]] */ + public static final int OP_GETI = 13; /* A B C R[A] := R[B][C] */ + public static final int OP_GETFIELD = 14; /* A B C R[A] := R[B][K[C]:string] */ + public static final int OP_SETTABUP = 15; /* A B C UpValue[A][K[B]:string] := RK(C) */ + public static final int OP_SETTABLE = 16; /* A B C R[A][R[B]] := RK(C) */ + public static final int OP_SETI = 17; /* A B C R[A][B] := RK(C) */ + public static final int OP_SETFIELD = 18; /* A B C R[A][K[B]:string] := RK(C) */ + public static final int OP_NEWTABLE = 19; /* A B C k R[A] := {} */ + public static final int OP_SELF = 20; /* A B C R[A+1] := R[B]; R[A] := R[B][RK(C):string] */ + public static final int OP_ADDI = 21; /* A B sC R[A] := R[B] + sC */ + public static final int OP_ADDK = 22; /* A B C R[A] := R[B] + K[C]:number */ + public static final int OP_SUBK = 23; /* A B C R[A] := R[B] - K[C]:number */ + public static final int OP_MULK = 24; /* A B C R[A] := R[B] * K[C]:number */ + public static final int OP_MODK = 25; /* A B C R[A] := R[B] % K[C]:number */ + public static final int OP_POWK = 26; /* A B C R[A] := R[B] ^ K[C]:number */ + public static final int OP_DIVK = 27; /* A B C R[A] := R[B] / K[C]:number */ + public static final int OP_IDIVK = 28; /* A B C R[A] := R[B] // K[C]:number */ + public static final int OP_BANDK = 29; /* A B C R[A] := R[B] & K[C]:integer */ + public static final int OP_BORK = 30; /* A B C R[A] := R[B] | K[C]:integer */ + public static final int OP_BXORK = 31; /* A B C R[A] := R[B] ~ K[C]:integer */ + public static final int OP_SHRI = 32; /* A B sC R[A] := R[B] >> sC */ + public static final int OP_SHLI = 33; /* A B sC R[A] := sC << R[B] */ + public static final int OP_ADD = 34; /* A B C R[A] := R[B] + R[C] */ + public static final int OP_SUB = 35; /* A B C R[A] := R[B] - R[C] */ + public static final int OP_MUL = 36; /* A B C R[A] := R[B] * R[C] */ + public static final int OP_MOD = 37; /* A B C R[A] := R[B] % R[C] */ + public static final int OP_POW = 38; /* A B C R[A] := R[B] ^ R[C] */ + public static final int OP_DIV = 39; /* A B C R[A] := R[B] / R[C] */ + public static final int OP_IDIV = 40; /* A B C R[A] := R[B] // R[C] */ + public static final int OP_BAND = 41; /* A B C R[A] := R[B] & R[C] */ + public static final int OP_BOR = 42; /* A B C R[A] := R[B] | R[C] */ + public static final int OP_BXOR = 43; /* A B C R[A] := R[B] ~ R[C] */ + public static final int OP_SHL = 44; /* A B C R[A] := R[B] << R[C] */ + public static final int OP_SHR = 45; /* A B C R[A] := R[B] >> R[C] */ + public static final int OP_MMBIN = 46; /* A B C call C metamethod over R[A] and R[B] */ + public static final int OP_MMBINI = 47; /* A sB C k call C metamethod over R[A] and sB */ + public static final int OP_MMBINK = 48; /* A B C k call C metamethod over R[A] and K[B] */ + public static final int OP_UNM = 49; /* A B R[A] := -R[B] */ + public static final int OP_BNOT = 50; /* A B R[A] := ~R[B] */ + public static final int OP_NOT = 51; /* A B R[A] := not R[B] */ + public static final int OP_LEN = 52; /* A B R[A] := #R[B] */ + public static final int OP_CONCAT = 53; /* A B R[A] := R[A].. ... ..R[A+B-1] */ + public static final int OP_CLOSE = 54; /* A close all upvalues >= R[A] */ + public static final int OP_TBC = 55; /* A mark variable A "to be closed" */ + public static final int OP_JMP = 56; /* sJ pc += sJ */ + public static final int OP_EQ = 57; /* A B k if ((R[A] == R[B]) ~= k) then pc++ */ + public static final int OP_LT = 58; /* A B k if ((R[A] < R[B]) ~= k) then pc++ */ + public static final int OP_LE = 59; /* A B k if ((R[A] <= R[B]) ~= k) then pc++ */ + public static final int OP_EQK = 60; /* A B k if ((R[A] == K[B]) ~= k) then pc++ */ + public static final int OP_EQI = 61; /* A sB k if ((R[A] == sB) ~= k) then pc++ */ + public static final int OP_LTI = 62; /* A sB k if ((R[A] < sB) ~= k) then pc++ */ + public static final int OP_LEI = 63; /* A sB k if ((R[A] <= sB) ~= k) then pc++ */ + public static final int OP_GTI = 64; /* A sB k if ((R[A] > sB) ~= k) then pc++ */ + public static final int OP_GEI = 65; /* A sB k if ((R[A] >= sB) ~= k) then pc++ */ + public static final int OP_TEST = 66; /* A k if (not R[A] == k) then pc++ */ + public static final int OP_TESTSET = 67; /* A B k if (not R[B] == k) then pc++ else R[A] := R[B] */ + public static final int OP_CALL = 68; /* A B C R[A], ... ,R[A+C-2] := R[A](R[A+1], ... ,R[A+B-1]) */ + public static final int OP_TAILCALL = 69; /* A B C k return R[A](R[A+1], ... ,R[A+B-1]) */ + public static final int OP_RETURN = 70; /* A B C k return R[A], ... ,R[A+B-2] */ + public static final int OP_RETURN0 = 71; /* return */ + public static final int OP_RETURN1 = 72; /* A return R[A] */ + public static final int OP_FORLOOP = 73; /* A Bx update counters; if loop continues then pc-=Bx */ + public static final int OP_FORPREP = 74; /* A Bx */ + public static final int OP_TFORPREP = 75; /* A Bx create upvalue for R[A+3]; pc+=Bx */ + public static final int OP_TFORCALL = 76; /* A C R[A+4], ... ,R[A+3+C] := R[A](R[A+1], R[A+2]) */ + public static final int OP_TFORLOOP = 77; /* A Bx if R[A+2] ~= nil then { R[A]=R[A+2]; pc -= Bx } */ + public static final int OP_SETLIST = 78; /* A B C k R[A][C+i] := R[A+i], 1 <= i <= B */ + public static final int OP_CLOSURE = 79; /* A Bx R[A] := closure(KPROTO[Bx]) */ + public static final int OP_VARARG = 80; /* A C R[A], R[A+1], ..., R[A+C-2] = vararg */ + public static final int OP_VARARGPREP = 81; /* A (adjust vararg parameters) */ + public static final int OP_EXTRAARG = 82; /* Ax extra arg for previous opcode */ + + public static final int NUM_OPCODES = OP_EXTRAARG + 1; + + /* ---------- 操作码属性表(lopcodes.c: luaP_opmodes) ---------- */ + + /** + * {@code opmode(mm,ot,it,t,a,m)}: + *
+     * bit 0-2  指令格式
+     * bit 3    指令写寄存器 A
+     * bit 4    是测试指令(下一条必须是跳转)
+     * bit 5    使用上一条指令设置的 top(B == 0 时)
+     * bit 6    为下一条指令设置 top(C == 0 时)
+     * bit 7    是元方法指令
+     * 
+ */ + private static int opmode(int mm, int ot, int it, int t, int a, int m) { + return ((mm) << 7) | ((ot) << 6) | ((it) << 5) | ((t) << 4) | ((a) << 3) | (m); + } + + /** ORDER OP —— 与上面的操作码常量严格同序 */ + private static final int[] OPMODES = { + opmode(0, 0, 0, 0, 1, IABC), /* OP_MOVE */ + opmode(0, 0, 0, 0, 1, IASBX), /* OP_LOADI */ + opmode(0, 0, 0, 0, 1, IASBX), /* OP_LOADF */ + opmode(0, 0, 0, 0, 1, IABX), /* OP_LOADK */ + opmode(0, 0, 0, 0, 1, IABX), /* OP_LOADKX */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_LOADFALSE */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_LFALSESKIP */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_LOADTRUE */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_LOADNIL */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_GETUPVAL */ + opmode(0, 0, 0, 0, 0, IABC), /* OP_SETUPVAL */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_GETTABUP */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_GETTABLE */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_GETI */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_GETFIELD */ + opmode(0, 0, 0, 0, 0, IABC), /* OP_SETTABUP */ + opmode(0, 0, 0, 0, 0, IABC), /* OP_SETTABLE */ + opmode(0, 0, 0, 0, 0, IABC), /* OP_SETI */ + opmode(0, 0, 0, 0, 0, IABC), /* OP_SETFIELD */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_NEWTABLE */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_SELF */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_ADDI */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_ADDK */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_SUBK */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_MULK */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_MODK */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_POWK */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_DIVK */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_IDIVK */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_BANDK */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_BORK */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_BXORK */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_SHRI */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_SHLI */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_ADD */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_SUB */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_MUL */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_MOD */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_POW */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_DIV */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_IDIV */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_BAND */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_BOR */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_BXOR */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_SHL */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_SHR */ + opmode(1, 0, 0, 0, 0, IABC), /* OP_MMBIN */ + opmode(1, 0, 0, 0, 0, IABC), /* OP_MMBINI */ + opmode(1, 0, 0, 0, 0, IABC), /* OP_MMBINK */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_UNM */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_BNOT */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_NOT */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_LEN */ + opmode(0, 0, 0, 0, 1, IABC), /* OP_CONCAT */ + opmode(0, 0, 0, 0, 0, IABC), /* OP_CLOSE */ + opmode(0, 0, 0, 0, 0, IABC), /* OP_TBC */ + opmode(0, 0, 0, 0, 0, ISJ), /* OP_JMP */ + opmode(0, 0, 0, 1, 0, IABC), /* OP_EQ */ + opmode(0, 0, 0, 1, 0, IABC), /* OP_LT */ + opmode(0, 0, 0, 1, 0, IABC), /* OP_LE */ + opmode(0, 0, 0, 1, 0, IABC), /* OP_EQK */ + opmode(0, 0, 0, 1, 0, IABC), /* OP_EQI */ + opmode(0, 0, 0, 1, 0, IABC), /* OP_LTI */ + opmode(0, 0, 0, 1, 0, IABC), /* OP_LEI */ + opmode(0, 0, 0, 1, 0, IABC), /* OP_GTI */ + opmode(0, 0, 0, 1, 0, IABC), /* OP_GEI */ + opmode(0, 0, 0, 1, 0, IABC), /* OP_TEST */ + opmode(0, 0, 0, 1, 1, IABC), /* OP_TESTSET */ + opmode(0, 1, 1, 0, 1, IABC), /* OP_CALL */ + opmode(0, 1, 1, 0, 1, IABC), /* OP_TAILCALL */ + opmode(0, 0, 1, 0, 0, IABC), /* OP_RETURN */ + opmode(0, 0, 0, 0, 0, IABC), /* OP_RETURN0 */ + opmode(0, 0, 0, 0, 0, IABC), /* OP_RETURN1 */ + opmode(0, 0, 0, 0, 1, IABX), /* OP_FORLOOP */ + opmode(0, 0, 0, 0, 1, IABX), /* OP_FORPREP */ + opmode(0, 0, 0, 0, 0, IABX), /* OP_TFORPREP */ + opmode(0, 0, 0, 0, 0, IABC), /* OP_TFORCALL */ + opmode(0, 0, 0, 0, 1, IABX), /* OP_TFORLOOP */ + opmode(0, 0, 1, 0, 0, IABC), /* OP_SETLIST */ + opmode(0, 0, 0, 0, 1, IABX), /* OP_CLOSURE */ + opmode(0, 1, 0, 0, 1, IABC), /* OP_VARARG */ + opmode(0, 0, 1, 0, 1, IABC), /* OP_VARARGPREP */ + opmode(0, 0, 0, 0, 0, IAX), /* OP_EXTRAARG */ + }; + + /** ORDER OP —— 与操作码常量严格同序 */ + /** + * 操作码名字({@code lopnames.h: opnames},ORDER OP)。 + *

+ * 上游把它放在单独的头文件里、只有 {@code luac} 用得上;这里留一份是因为 + * 差分比对要用它把指令打成可读文本,VM 的 trace 也要用。 + */ + public static final String[] NAMES = { + "MOVE", "LOADI", "LOADF", "LOADK", "LOADKX", "LOADFALSE", "LFALSESKIP", + "LOADTRUE", "LOADNIL", "GETUPVAL", "SETUPVAL", "GETTABUP", "GETTABLE", + "GETI", "GETFIELD", "SETTABUP", "SETTABLE", "SETI", "SETFIELD", "NEWTABLE", + "SELF", "ADDI", "ADDK", "SUBK", "MULK", "MODK", "POWK", "DIVK", "IDIVK", + "BANDK", "BORK", "BXORK", "SHRI", "SHLI", "ADD", "SUB", "MUL", "MOD", + "POW", "DIV", "IDIV", "BAND", "BOR", "BXOR", "SHL", "SHR", "MMBIN", + "MMBINI", "MMBINK", "UNM", "BNOT", "NOT", "LEN", "CONCAT", "CLOSE", + "TBC", "JMP", "EQ", "LT", "LE", "EQK", "EQI", "LTI", "LEI", "GTI", + "GEI", "TEST", "TESTSET", "CALL", "TAILCALL", "RETURN", "RETURN0", + "RETURN1", "FORLOOP", "FORPREP", "TFORPREP", "TFORCALL", "TFORLOOP", + "SETLIST", "CLOSURE", "VARARG", "VARARGPREP", "EXTRAARG", + }; + + /** {@code getOpMode} */ + public static int getOpMode(int op) { + return OPMODES[op] & 7; + } + + public static boolean testAMode(int op) { + return (OPMODES[op] & (1 << 3)) != 0; + } + + public static boolean testTMode(int op) { + return (OPMODES[op] & (1 << 4)) != 0; + } + + public static boolean testITMode(int op) { + return (OPMODES[op] & (1 << 5)) != 0; + } + + public static boolean testOTMode(int op) { + return (OPMODES[op] & (1 << 6)) != 0; + } + + public static boolean testMMMode(int op) { + return (OPMODES[op] & (1 << 7)) != 0; + } + + public static String name(int op) { + return NAMES[op]; + } + + /* ---------- 编解码 ---------- */ + + /** 生成 {@code n} 个 1 位于 {@code p} 的掩码({@code MASK1}) */ + private static int mask1(int n, int p) { + return (~((~0) << n)) << p; + } + + /** {@code getarg}。用 {@code >>>}:指令是有符号 int,但字段是无符号的 */ + private static int getarg(int i, int pos, int size) { + return (i >>> pos) & mask1(size, 0); + } + + /** {@code setarg} */ + private static int setarg(int i, int v, int pos, int size) { + return (i & ~mask1(size, pos)) | ((v << pos) & mask1(size, pos)); + } + + public static int getOpCode(int i) { + return getarg(i, POS_OP, SIZE_OP); + } + + public static int setOpCode(int i, int op) { + return setarg(i, op, POS_OP, SIZE_OP); + } + + public static int getArgA(int i) { + return getarg(i, POS_A, SIZE_A); + } + + public static int setArgA(int i, int v) { + return setarg(i, v, POS_A, SIZE_A); + } + + public static int getArgB(int i) { + return getarg(i, POS_B, SIZE_B); + } + + /** {@code GETARG_sB} */ + public static int getArgSB(int i) { + return sC2int(getArgB(i)); + } + + public static int setArgB(int i, int v) { + return setarg(i, v, POS_B, SIZE_B); + } + + public static int getArgC(int i) { + return getarg(i, POS_C, SIZE_C); + } + + /** {@code GETARG_sC} */ + public static int getArgSC(int i) { + return sC2int(getArgC(i)); + } + + public static int setArgC(int i, int v) { + return setarg(i, v, POS_C, SIZE_C); + } + + /** {@code GETARG_k} */ + public static int getArgK(int i) { + return getarg(i, POS_K, 1); + } + + public static int setArgK(int i, int v) { + return setarg(i, v, POS_K, 1); + } + + public static int getArgBx(int i) { + return getarg(i, POS_BX, SIZE_BX); + } + + public static int setArgBx(int i, int v) { + return setarg(i, v, POS_BX, SIZE_BX); + } + + /** {@code GETARG_sBx} */ + public static int getArgSBx(int i) { + return getArgBx(i) - OFFSET_SBX; + } + + /** {@code SETARG_sBx} */ + public static int setArgSBx(int i, int v) { + return setArgBx(i, v + OFFSET_SBX); + } + + public static int getArgAx(int i) { + return getarg(i, POS_AX, SIZE_AX); + } + + public static int setArgAx(int i, int v) { + return setarg(i, v, POS_AX, SIZE_AX); + } + + /** {@code GETARG_sJ} */ + public static int getArgSJ(int i) { + return getarg(i, POS_SJ, SIZE_SJ) - OFFSET_SJ; + } + + /** {@code SETARG_sJ} */ + public static int setArgSJ(int i, int v) { + return setarg(i, v + OFFSET_SJ, POS_SJ, SIZE_SJ); + } + + /** {@code CREATE_ABCk} */ + public static int createABCk(int o, int a, int b, int c, int k) { + return (o << POS_OP) | (a << POS_A) | (b << POS_B) | (c << POS_C) | (k << POS_K); + } + + /** {@code CREATE_ABx} */ + public static int createABx(int o, int a, int bx) { + return (o << POS_OP) | (a << POS_A) | (bx << POS_BX); + } + + /** {@code CREATE_Ax} */ + public static int createAx(int o, int ax) { + return (o << POS_OP) | (ax << POS_AX); + } + + /** {@code CREATE_sJ} */ + public static int createsJ(int o, int j, int k) { + return (o << POS_OP) | (j << POS_SJ) | (k << POS_K); + } + + /* ---------- top 相关(ldo.c / ldebug.c 用) ---------- */ + + /** {@code isOT}:本指令会为下一条设置 top */ + public static boolean isOT(int i) { + int op = getOpCode(i); + return (testOTMode(op) && getArgC(i) == 0) || op == OP_TAILCALL; + } + + /** {@code isIT}:本指令使用上一条设置的 top */ + public static boolean isIT(int i) { + return testITMode(getOpCode(i)) && getArgB(i) == 0; + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/OsLib.java b/src/main/java/com/github/esrrhs/fakescript/lua/OsLib.java new file mode 100644 index 0000000..6990a8b --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/OsLib.java @@ -0,0 +1,902 @@ +package com.github.esrrhs.fakescript.lua; + +import java.io.File; +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +/** + * os 库 —— 对应 {@code loslib.c}。 + *

+ * 这一整块是平台相关的,翻译的重点不是「照抄 C」,而是把 C 那些依赖 + * libc 的行为换成 Java 里语义等价的东西,并且如实标出哪里对不上: + *

    + *
  • {@code os.date} 要自己实现一套 {@code strftime}——Java 没有。转换符 + * 集合照抄 {@code LUA_STRFTIMEOPTIONS}(含 {@code E} / {@code O} 修饰), + * 连「不认识的转换符报 {@code invalid conversion specifier '%XX'}」都一致。
  • + *
  • {@code os.time} 要自己实现 {@code mktime} 的归一化: + * {@code {year=2023, month=13, day=32, hour=25}} 会被正规化成 + * {@code 2024-02-02 02:00},并且回写进传入的那张表。 + * Java 的 {@link GregorianCalendar} 开了 lenient 之后正好做这件事。
  • + *
  • 历法切换要关掉:{@code mktime} 用的是前推格里历,而 + * {@code GregorianCalendar} 默认在 1582-10-15 之前走儒略历,不关会差好几天。
  • + *
+ *

+ * 三处承认下来的偏离(都写进了 README): + *

    + *
  1. {@code setlocale}:Java 没有 {@code setlocale},这里只能认 + * {@code nil} / {@code "C"} / {@code "POSIX"},其余一律返回 nil。
  2. + *
  3. {@code os.clock}:取的是当前线程的 CPU 时间 + * ({@code ThreadMXBean}),不是整个进程的——本实现本来就只在一条线程上跑。
  4. + *
  5. {@code os.execute} 拿不到信号:进程被信号杀掉时 C 给 + * {@code ("signal", 信号编号)},Java 只能给 {@code ("exit", 128+n)}。
  6. + *
+ */ +public final class OsLib { + + private OsLib() { + } + + /* ==================== 注册 ==================== */ + + public static void open(LuaState L) { + LuaTable o = new LuaTable(); + LuaAux.setFn(o, "clock", OsLib::clock); + LuaAux.setFn(o, "date", OsLib::date); + LuaAux.setFn(o, "difftime", OsLib::difftime); + LuaAux.setFn(o, "execute", OsLib::execute); + LuaAux.setFn(o, "exit", OsLib::exit); + LuaAux.setFn(o, "getenv", OsLib::getenv); + LuaAux.setFn(o, "remove", OsLib::remove); + LuaAux.setFn(o, "rename", OsLib::rename); + LuaAux.setFn(o, "setlocale", OsLib::setlocale); + LuaAux.setFn(o, "time", OsLib::time); + LuaAux.setFn(o, "tmpname", OsLib::tmpname); + LuaAux.registerLib(L, "os", o); + } + + /* ==================== 日历 ==================== */ + + private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + + /** 前推格里历的「切换点」:设成下界就是全程格里历,与 {@code mktime} 一致 */ + private static final Date PURE_GREGORIAN = new Date(Long.MIN_VALUE); + + /** {@code time_t} 是 64 位,但秒 → 毫秒会溢出,先挡一道 */ + private static final long MAX_SEC = Long.MAX_VALUE / 1000; + + private static GregorianCalendar newCal(TimeZone tz) { + GregorianCalendar c = new GregorianCalendar(tz); + c.setGregorianChange(PURE_GREGORIAN); + c.setLenient(true); + return c; + } + + /** + * {@code l_gmtime} / {@code l_localtime}:把 epoch 秒变成一个 {@code struct tm}。 + *

+ * 参考实现给不出结果时({@code gmtime} 返回 NULL)报 + * {@code date result cannot be represented in this installation};这里用 + * 秒→毫秒的溢出挡同一件事。 + */ + private static Calendar tm(long sec, boolean utc) { + if (sec > MAX_SEC || sec < -MAX_SEC) { + return null; + } + Calendar c = newCal(utc ? UTC : TimeZone.getDefault()); + c.setTimeInMillis(sec * 1000); + return c; + } + + private static LuaError badDate(LuaState L) { + return L.libError("date result cannot be represented in this installation"); + } + + private static LuaError badTime(LuaState L) { + return L.libError("time result cannot be represented in this installation"); + } + + /* ==================== 字段读写 ==================== */ + + /** 走 {@code __index} 的字段读({@code lua_getfield} 会走) */ + private static LuaValue field(LuaState L, LuaValue t, String key) { + int f = L.top; + L.ensureSlots(f + 1); + Vm.getTable(L, t, LuaValue.ofString(LuaString.latin1(key)), f); + return new LuaValue(L.stack[f]); + } + + /** 走 {@code __newindex} 的字段写({@code lua_setfield} 会走) */ + private static void setField(LuaState L, LuaValue t, String key, LuaValue v) { + Vm.setTable(L, t, LuaValue.ofString(LuaString.latin1(key)), v); + } + + /** + * {@code loslib.c: getfield}。 + *

+ * 两个报错文本都不带 {@code bad argument} 前缀——C 用的是 {@code luaL_error} + * 而不是 {@code luaL_argerror},别套错。 + */ + private static int getField(LuaState L, LuaValue t, String key, int def, int delta) { + LuaValue v = field(L, t, key); + /* + * C 这里是 {@code lua_tointegerx},带字符串转换 + * ({@code cvt2num} 让可解析的字符串也走 {@code luaV_tointeger})。 + * 实测 {@code os.time{year="2000", month=1, day=1}} 在参考实现里就是 + * 2000 年;只认整数/浮点会把它报成 {@code field 'year' is not an + * integer}。库函数参数走 {@code luaL_check*} 那一套(会做类型转换), + * 与 VM 算术指令刻意不转换的做法相反。 + */ + Long i = Vm.toInteger(v, Arith.F2I_EQ); + long res; + if (i == null) { + if (!v.isNil()) { + throw L.libError("field '" + key + "' is not an integer"); + } + if (def < 0) { + throw L.libError("field '" + key + "' missing in date table"); + } + res = def; + } else { + res = i; + boolean ok = (res >= 0) ? (res - (long) delta <= Integer.MAX_VALUE) + : (Integer.MIN_VALUE + (long) delta <= res); + if (!ok) { + throw L.libError("field '" + key + "' is out-of-bound"); + } + res -= delta; + } + return (int) res; + } + + /** {@code getboolfield}:nil 是「没给」,返回 -1 */ + private static int getBoolField(LuaState L, LuaValue t, String key) { + LuaValue v = field(L, t, key); + return v.isNil() ? -1 : (v.toBoolean() ? 1 : 0); + } + + /** {@link #setAllFields(LuaState, LuaValue, Calendar, boolean)} 的纯算术版 */ + private static void setAllFields(LuaState L, LuaValue t, long sec) { + long[] c = epochToCivil(sec); + setField(L, t, "year", LuaValue.ofInteger(c[0])); + setField(L, t, "month", LuaValue.ofInteger(c[1])); + setField(L, t, "day", LuaValue.ofInteger(c[2])); + setField(L, t, "hour", LuaValue.ofInteger(c[3])); + setField(L, t, "min", LuaValue.ofInteger(c[4])); + setField(L, t, "sec", LuaValue.ofInteger(c[5])); + setField(L, t, "yday", LuaValue.ofInteger(c[7])); + setField(L, t, "wday", LuaValue.ofInteger(c[6])); + setField(L, t, "isdst", LuaValue.ofBoolean(false)); + } + + /** {@code setallfields}:{@code isdst} 只在已知时才写({@code tm_isdst < 0} 不写) */ + private static void setAllFields(LuaState L, LuaValue t, Calendar c, boolean utc) { + TimeZone tz = utc ? UTC : TimeZone.getDefault(); + setField(L, t, "year", LuaValue.ofInteger(c.get(Calendar.YEAR))); + setField(L, t, "month", LuaValue.ofInteger(c.get(Calendar.MONTH) + 1)); + setField(L, t, "day", LuaValue.ofInteger(c.get(Calendar.DAY_OF_MONTH))); + setField(L, t, "hour", LuaValue.ofInteger(c.get(Calendar.HOUR_OF_DAY))); + setField(L, t, "min", LuaValue.ofInteger(c.get(Calendar.MINUTE))); + setField(L, t, "sec", LuaValue.ofInteger(c.get(Calendar.SECOND))); + setField(L, t, "yday", LuaValue.ofInteger(c.get(Calendar.DAY_OF_YEAR))); + // Lua 的 wday 是 1=星期天,正好与 Calendar 的 SUNDAY..SATURDAY = 1..7 重合 + setField(L, t, "wday", LuaValue.ofInteger(c.get(Calendar.DAY_OF_WEEK))); + int isdst = utc ? 0 + : ((tz.getOffset(c.getTimeInMillis()) != tz.getRawOffset()) ? 1 : 0); + setField(L, t, "isdst", LuaValue.ofBoolean(isdst != 0)); + } + + /* ==================== os.date ==================== */ + + /** {@code LUA_STRFTIMEOPTIONS},照 C 源码原样搬({@code ||} 之后是两字符那组) */ + private static final String OPTIONS = "aAbBcCdDeFgGhHIjmMnprRStTuUVwWxXyYzZ%" + + "||" + "EcECExEXEyEY" + "OdOeOHOIOmOMOSOuOUOVOwOWOy"; + + private static int date(LuaState L) { + LuaString fmt = LuaAux.optString(L, 1, LuaString.latin1("%c")); + long t; + if (L.isNoneArg(2) || L.arg1(2).isNil()) { + t = System.currentTimeMillis() / 1000; + } else { + t = LuaAux.checkInteger(L, 2); + } + byte[] f = fmt.bytes; + int p = 0; + boolean utc = false; + if (f.length > 0 && f[0] == '!') { + utc = true; + p = 1; + } + Calendar c = tm(t, utc); + if (c == null) { + throw badDate(L); + } + /* + * C 这里是 {@code strcmp(s, "*t") == 0},不是「以 *t 开头」—— 所以 + * {@code os.date("*t x")} 走的是 strftime 那条路,{@code *}、{@code t}、 + * 空格、{@code x} 全当字面量,结果是字符串 {@code "*t x"} 而不是表。 + * 按前缀匹配会把这类格式串错认成取表。 + *

+ * 另一个细节:{@code strcmp} 遇到 {@code \0} 就停,所以 + * {@code os.date("*t\0x")} 在 C 里仍然是取表 —— 先截到第一个 NUL + * 再比长度,不能直接用 {@code f.length}。 + */ + int n = 0; + while (p + n < f.length && f[p + n] != 0) { + n++; + } + if (n == 2 && f[p] == '*' && f[p + 1] == 't') { + LuaTable r = new LuaTable(0, 9); + LuaValue tv = LuaValue.ofTable(r); + setAllFields(L, tv, c, utc); + L.push(tv); + return 1; + } + LuaAux.Buf b = new LuaAux.Buf(); + int i = p; + while (i < f.length) { + if (f[i] != '%') { + b.addChar(f[i++] & 0xFF); + continue; + } + i++; // 跳过 '%' + int[] next = new int[1]; + int spec = checkOption(L, f, i, next); + i = next[0]; + emit(b, spec, c, utc); + } + L.push(LuaValue.ofString(b.result())); + return 1; + } + + /** + * {@code loslib.c: checkoption}。 + *

+ * 报错里带的是从 {@code %} 开始剩下的全部,不是一个字符—— + * {@code os.date("%QQ")} 报的是 {@code '%QQ'},照 {@code conv} 的原样输出。 + */ + private static int checkOption(LuaState L, byte[] f, int i, int[] next) { + int end = f.length; + int o = 0; + int oplen = 1; + while (o < OPTIONS.length() && oplen <= end - i) { + if (OPTIONS.charAt(o) == '|') { + oplen++; + o += oplen; + continue; + } + boolean match = true; + for (int k = 0; k < oplen; k++) { + if (i + k >= end || (f[i + k] & 0xFF) != OPTIONS.charAt(o + k)) { + match = false; + break; + } + } + if (match) { + next[0] = i + oplen; + // E/O 修饰在 C locale 下与基础转换符等价,取第二个字符即可 + return OPTIONS.charAt(o + oplen - 1); + } + o += oplen; + } + String conv = new String(f, i, Math.max(0, end - i), StandardCharsets.ISO_8859_1); + throw L.argError(1, "invalid conversion specifier '%" + conv + "'"); + } + + /* ==================== strftime ==================== */ + + private static final String[] WDAY = { "Sunday", "Monday", "Tuesday", "Wednesday", + "Thursday", "Friday", "Saturday" }; + private static final String[] MONTH = { "January", "February", "March", "April", "May", + "June", "July", "August", "September", "October", "November", "December" }; + + private static void addAscii(LuaAux.Buf b, String s) { + b.addAscii(s); + } + + private static void pad(LuaAux.Buf b, long v, int w) { + addAscii(b, zeroPad(v, w)); + } + + private static void padSpace(LuaAux.Buf b, long v, int w) { + String s = Long.toString(v); + for (int k = s.length(); k < w; k++) { + b.addChar(' '); + } + addAscii(b, s); + } + + private static String zeroPad(long v, int w) { + boolean neg = v < 0; + String s = Long.toString(Math.abs(v)); + StringBuilder sb = new StringBuilder(); + for (int k = s.length(); k < w; k++) { + sb.append('0'); + } + sb.append(s); + if (neg) { + sb.insert(0, '-'); + } + return sb.toString(); + } + + private static void emit(LuaAux.Buf b, int spec, Calendar c, boolean utc) { + long ms = c.getTimeInMillis(); + int year = c.get(Calendar.YEAR); + int mon = c.get(Calendar.MONTH) + 1; // 1..12 + int day = c.get(Calendar.DAY_OF_MONTH); + int hour = c.get(Calendar.HOUR_OF_DAY); + int min = c.get(Calendar.MINUTE); + int sec = c.get(Calendar.SECOND); + int yday = c.get(Calendar.DAY_OF_YEAR); // 1..366 + int wday = c.get(Calendar.DAY_OF_WEEK) - 1; // 0=星期天,与 tm_wday 一致 + int isoWday = (wday + 6) % 7 + 1; // 1=星期一 + + switch (spec) { + case 'a': + addAscii(b, WDAY[wday].substring(0, 3)); + return; + case 'A': + addAscii(b, WDAY[wday]); + return; + case 'b': + case 'h': + addAscii(b, MONTH[mon - 1].substring(0, 3)); + return; + case 'B': + addAscii(b, MONTH[mon - 1]); + return; + case 'c': // C locale: "%a %b %e %H:%M:%S %Y" + addAscii(b, WDAY[wday].substring(0, 3)); + b.addChar(' '); + addAscii(b, MONTH[mon - 1].substring(0, 3)); + b.addChar(' '); + padSpace(b, day, 2); + b.addChar(' '); + pad(b, hour, 2); + b.addChar(':'); + pad(b, min, 2); + b.addChar(':'); + pad(b, sec, 2); + b.addChar(' '); + addAscii(b, zeroPad(year, 4)); + return; + case 'C': + pad(b, year / 100, 2); + return; + case 'd': + pad(b, day, 2); + return; + case 'D': + pad(b, mon, 2); + b.addChar('/'); + pad(b, day, 2); + b.addChar('/'); + pad(b, Math.floorMod(year, 100L), 2); + return; + case 'e': + padSpace(b, day, 2); + return; + case 'F': + addAscii(b, zeroPad(year, 4)); + b.addChar('-'); + pad(b, mon, 2); + b.addChar('-'); + pad(b, day, 2); + return; + case 'g': + pad(b, Math.floorMod(isoYear(c), 100L), 2); + return; + case 'G': + addAscii(b, zeroPad(isoYear(c), 4)); + return; + case 'H': + pad(b, hour, 2); + return; + case 'I': + pad(b, (hour % 12 == 0) ? 12 : (hour % 12), 2); + return; + case 'j': + pad(b, yday, 3); + return; + case 'm': + pad(b, mon, 2); + return; + case 'M': + pad(b, min, 2); + return; + case 'n': + b.addChar('\n'); + return; + case 'p': + addAscii(b, hour < 12 ? "AM" : "PM"); + return; + case 'r': // "%I:%M:%S %p" + pad(b, (hour % 12 == 0) ? 12 : (hour % 12), 2); + b.addChar(':'); + pad(b, min, 2); + b.addChar(':'); + pad(b, sec, 2); + b.addChar(' '); + addAscii(b, hour < 12 ? "AM" : "PM"); + return; + case 'R': + pad(b, hour, 2); + b.addChar(':'); + pad(b, min, 2); + return; + case 'S': + pad(b, sec, 2); + return; + case 't': + b.addChar('\t'); + return; + case 'T': + pad(b, hour, 2); + b.addChar(':'); + pad(b, min, 2); + b.addChar(':'); + pad(b, sec, 2); + return; + case 'u': + pad(b, isoWday, 1); + return; + case 'U': // 星期天为每周第一天 + pad(b, (yday - 1 + 7 - wday) / 7, 2); + return; + case 'V': + pad(b, isoWeek(c)[0], 2); + return; + case 'w': + pad(b, wday, 1); + return; + case 'W': // 星期一为每周第一天 + pad(b, (yday - 1 + 7 - ((wday + 6) % 7)) / 7, 2); + return; + case 'x': // C locale: "%m/%d/%y" + pad(b, mon, 2); + b.addChar('/'); + pad(b, day, 2); + b.addChar('/'); + pad(b, Math.floorMod(year, 100L), 2); + return; + case 'X': // C locale: "%H:%M:%S" + pad(b, hour, 2); + b.addChar(':'); + pad(b, min, 2); + b.addChar(':'); + pad(b, sec, 2); + return; + case 'y': + pad(b, Math.floorMod(year, 100L), 2); + return; + case 'Y': + addAscii(b, zeroPad(year, 4)); + return; + case 'z': { + TimeZone tz = utc ? UTC : TimeZone.getDefault(); + int off = utc ? 0 : tz.getOffset(ms); + b.addChar(off < 0 ? '-' : '+'); + int a = Math.abs(off) / 60000; + pad(b, a / 60, 2); + pad(b, a % 60, 2); + return; + } + case 'Z': { + TimeZone tz = utc ? UTC : TimeZone.getDefault(); + String n = utc ? "UTC" + : tz.getDisplayName(tz.inDaylightTime(new Date(ms)), TimeZone.SHORT); + addAscii(b, n); + return; + } + case '%': + b.addChar('%'); + return; + default: + b.addChar('%'); + b.addChar(spec); + return; + } + } + + /* ==================== ISO 周 / 年 ==================== */ + + /** 一年的 ISO 周数:52 或 53 */ + private static int isoWeeksInYear(int y) { + long p = pOf(y); + return (p == 4 || pOf(y - 1) == 3) ? 53 : 52; + } + + /** {@code p(y) = (y + y/4 - y/100 + y/400) mod 7} */ + private static long pOf(long y) { + long r = y + y / 4 - y / 100 + y / 400; + return Math.floorMod(r, 7L); + } + + private static long[] isoWeek(Calendar c) { + int y = c.get(Calendar.YEAR); + int ord = c.get(Calendar.DAY_OF_YEAR); + int wd = (c.get(Calendar.DAY_OF_WEEK) - 1 + 6) % 7 + 1; // ISO: 1=周一..7=周日 + long w = (ord - wd + 10) / 7; + if (w < 1) { + return new long[] { isoWeeksInYear(y - 1), y - 1 }; + } + if (w > isoWeeksInYear(y)) { + return new long[] { 1, y + 1 }; + } + return new long[] { w, y }; + } + + private static int isoYear(Calendar c) { + return (int) isoWeek(c)[1]; + } + + /* ==================== os.time ==================== */ + + private static int time(LuaState L) { + if (L.isNoneArg(1) || L.arg1(1).isNil()) { + L.push(LuaValue.ofInteger(System.currentTimeMillis() / 1000)); + return 1; + } + LuaAux.checkType(L, 1, Lua.TTABLE); + LuaValue t = L.arg1(1); + int tmYear = getField(L, t, "year", -1, 1900); + int tmMon = getField(L, t, "month", -1, 1); + int tmDay = getField(L, t, "day", -1, 0); + int tmHour = getField(L, t, "hour", 12, 0); + int tmMin = getField(L, t, "min", 0, 0); + int tmSec = getField(L, t, "sec", 0, 0); + int isdst = getBoolField(L, t, "isdst"); + + TimeZone tz = TimeZone.getDefault(); + long year = (long) tmYear + 1900; + // 先把这六个字段当成 UTC 算出一个「纯本地时刻」,再按偏移量折回 epoch + // tmMon 是 0 起(Calendar 的规矩),{@code civilToEpoch} 收的是 1 起的月份 + long asUtcSec = civilToEpoch(year, tmMon + 1, tmDay, tmHour, tmMin, tmSec); + + long sec; + if (isdst < 0) { + /* + * 自动判定:交给 Calendar,它会按当时生效的规则挑偏移量。 + * 但 Calendar 存的是毫秒,年份一大就溢出(见 + * {@link #civilToEpoch}),所以撑不住的年份退成「只有原始偏移」—— + * 那种年份本来也不存在任何夏令时规则。 + */ + if (year > 9999 || year < -9999) { + sec = asUtcSec - tz.getRawOffset() / 1000; + } else { + GregorianCalendar c = newCal(tz); + c.clear(); + c.set((int) year, tmMon, tmDay, tmHour, tmMin, tmSec); + sec = c.getTimeInMillis() / 1000; + } + } else { + long dst = tz.getDSTSavings(); + if (dst == 0) { + // 这个时区根本没有夏令时规则,但 C 的 mktime 收到 tm_isdst=1 + // 仍然照惯例挪一小时(实测如此) + dst = 3600000L; + } + sec = asUtcSec - tz.getRawOffset() / 1000 - (isdst == 1 ? dst / 1000 : 0); + } + + Calendar out = tm(sec, false); + if (out != null) { + setAllFields(L, t, out, false); // mktime 之后字段被正规化并回写 + } else { + /* + * Calendar 存毫秒,{@code |sec|} 一大就装不下({@link #MAX_SEC})。但 C 的 + * {@code time_t} 是 64 位,这一档 mktime 照样给得出结果——所以这里 + * 不能跟着报 badTime,改用纯算术回写。 + */ + setAllFields(L, t, sec); + } + if (sec == -1) { + throw badTime(L); + } + L.push(LuaValue.ofInteger(sec)); + return 1; + } + + /** + * 前推格里历的 civil → epoch 秒(Howard Hinnant 的算法,全程 {@code long})。 + *

+ * 为什么要另写一份而不继续用 {@link GregorianCalendar}:{@code Calendar} 存的是 + * 毫秒,年份到 {@code 2^31} 那一档 {@code getTimeInMillis()} 直接溢出、 + * 折回一个莫名其妙的负数。实测 {@code os.time{year=2147483647}} 参考实现给 + * {@code 67767976201968000},我们原来给 {@code -6019000092870206};而 + * {@code 67767976201968000} 正好等于 {@code 86400 * days_from_civil} 减时区 + * 偏移——纯算术,两边 libc 都一样。 + */ + private static long civilToEpoch(long y, long m, long d, long h, long mi, long s) { + long mm = m - 1; // 0..11 之外的月份先折进年份 + long yy = y + Math.floorDiv(mm, 12); + long m0 = Math.floorMod(mm, 12) + 1; // 回到 1..12 + long ya = yy - ((m0 <= 2) ? 1 : 0); + long era = Math.floorDiv(ya, 400); + long yoe = ya - era * 400; // [0, 399] + long doy = (153 * (m0 + ((m0 > 2) ? -3 : 9)) + 2) / 5 + d - 1; + long doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + long days = era * 146097 + doe - 719468; + return days * 86400 + h * 3600 + mi * 60 + s; + } + + /** {@link #civilToEpoch} 的逆;返回 {year, month, day, hour, min, sec, wday, yday} */ + private static long[] epochToCivil(long sec) { + long days = Math.floorDiv(sec, 86400L); + long rem = Math.floorMod(sec, 86400L); + long z = days + 719468; + long era = Math.floorDiv(z, 146097); + long doe = z - era * 146097; // [0, 146096] + long yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399] + long y = yoe + era * 400; + long doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365] + long mp = (5 * doy + 2) / 153; // [0, 11] + long d = doy - (153 * mp + 2) / 5 + 1; // [1, 31] + long m = mp + ((mp < 10) ? 3 : -9); // [1, 12] + y += (m <= 2) ? 1 : 0; + long wday = Math.floorMod(days + 4, 7) + 1; // 1970-01-01 是周四;Lua 的 wday 1=周日 + long yday = days - civilToEpoch(y, 1, 1, 0, 0, 0) / 86400 + 1; + return new long[] { y, m, d, rem / 3600, (rem / 60) % 60, rem % 60, wday, yday }; + } + + /* ==================== 其余 ==================== */ + + private static int difftime(LuaState L) { + long t1 = LuaAux.checkInteger(L, 1); + long t2 = LuaAux.checkInteger(L, 2); + L.push(LuaValue.ofFloat((double) (t1 - t2))); + return 1; + } + + /** + * {@code os.clock}。 + *

+ * C 的 {@code clock()} 是进程 CPU 时间;Java 拿得到的最接近的东西是 + * 当前线程的时间({@code ThreadMXBean})。本实现本来就只在一条线程上跑脚本, + * 差别只在 JVM 自己的后台线程上。 + */ + private static int clock(LuaState L) { + ThreadMXBean bean = ManagementFactory.getThreadMXBean(); + long ns = bean.isCurrentThreadCpuTimeSupported() ? bean.getCurrentThreadCpuTime() : -1; + L.push(LuaValue.ofFloat(ns < 0 ? 0.0 : ns / 1e9)); + return 1; + } + + private static int getenv(LuaState L) { + // getenv 的参数是 const char*:在第一个 \0 处砍断 + String v = System.getenv(LuaAux.checkString(L, 1).toCString().toJavaString()); + if (v == null) { + L.pushNil(); + } else { + L.push(LuaValue.ofString(L.newString(v.getBytes(StandardCharsets.UTF_8)))); + } + return 1; + } + + /** + * {@code os.execute}。 + *

+ * 无参时 {@code system(NULL)} 问「有没有 shell」,这里一律给 true。 + * 有参时走 {@code sh -c},并照 {@code luaL_execresult} 给三元组 + * {@code (true|fail, "exit", 状态码)}——注意成功时也返回三个值, + * 不是只返回 true。 + *

+ * 命令名写 {@code "sh"} 而不是 {@code "/bin/sh"}:C 的 {@code system()} 是 + * {@code execl("/bin/sh", "sh", "-c", cmd)},argv[0] 是 {@code "sh"}。 + * shell 的错误信息用 argv[0] 打头({@code sh: 42: command not found}), + * 写成全路径就会变成 {@code /bin/sh: ...},和参考实现对不上。 + */ + private static int execute(LuaState L) { + if (L.isNoneArg(1) || L.arg1(1).isNil()) { + L.push(LuaValue.ofBoolean(true)); + return 1; + } + String cmd = LuaAux.checkString(L, 1).toCString().toJavaString(); + int stat; + try { + Process p = new ProcessBuilder("sh", "-c", cmd).start(); + // 子进程的输出必须抄到 L.out 上,不能用 inheritIO:后者走的是 + // 进程的文件描述符,脚本里换掉的输出流接不到,于是 + // os.execute("nosuch") 连「command not found」都看不见 + Thread g1 = gobbler(p.getInputStream(), L); + Thread g2 = gobbler(p.getErrorStream(), L); + g1.start(); + g2.start(); + stat = p.waitFor(); + g1.join(); + g2.join(); + } catch (IOException e) { + LuaAux.fileResult(L, false, null, LuaAux.errnoOf(e)); + return 3; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LuaAux.fileResult(L, false, null, 4); // EINTR + return 3; + } + // Java 区分不出「被信号杀掉」,一律按 exit 汇报(见类注释)。 + // 失败时给的是 luaL_pushfail(5.4 里就是 nil),不是 false—— + // print(os.execute("false")) 出的是「nil exit 1」 + if (stat == 0) { + L.push(LuaValue.ofBoolean(true)); + } else { + L.pushNil(); + } + L.push(LuaValue.ofString(LuaString.latin1("exit"))); + L.push(LuaValue.ofInteger(stat)); + return 3; + } + + /** 把子进程的一条输出流抄到 {@code L.out} 上,并负责关流 */ + private static Thread gobbler(java.io.InputStream in, LuaState L) { + Thread t = new Thread(() -> { + byte[] buf = new byte[4096]; + try (java.io.InputStream s = in) { + for (;;) { + int n = s.read(buf); + if (n < 0) { + break; + } + L.out.write(buf, 0, n); + } + } catch (IOException ignored) { + // 子进程把管道关了就是 EOF,不当错误 + } + L.out.flush(); + }); + t.setDaemon(true); + return t; + } + + private static int remove(LuaState L) { + // remove 的参数是 const char*:在第一个 \0 处砍断。不砍的话 Java 的 + // Paths.get 会抛 InvalidPathException —— 而它不是 LuaError,pcall 拦不住, + // 会整颗逃逸成 Java 异常(实测 os.remove("a\0b") 直接崩掉进程)。 + LuaString pname = LuaAux.checkString(L, 1).toCString(); + String path = pname.toJavaString(); + /* + * 空路径:C 的 remove("") 走 unlink 直接给 ENOENT(实测 + * os.remove("") -> nil, ": No such file or directory", 2), + * 而 Java 的 Paths.get("") 抛的是别的异常、落进 errnoOf 的兜底成了 EIO。 + * 与 loadfile("") 是同一类「C 与 Java 判路径不一致」的角落。 + */ + if (path.isEmpty()) { + LuaAux.fileResult(L, false, pname, 2); // ENOENT + return 3; + } + try { + Files.delete(Paths.get(path)); + L.push(LuaValue.ofBoolean(true)); + return 1; + } catch (IOException | RuntimeException e) { + LuaAux.fileResult(L, false, pname, LuaAux.errnoOf(e)); + return 3; + } + } + + private static int rename(LuaState L) { + String from = LuaAux.checkString(L, 1).toCString().toJavaString(); + String to = LuaAux.checkString(L, 2).toCString().toJavaString(); + /* + * 目标与源类型冲突的那两个形状必须自己先拦下来:Java 的 + * {@code Files.move} 比 C 的 {@code rename(2)} 宽松得多 ——「文件改名到 + * 目录上」「目录改名到文件上」它都成功(前者是把文件挪进那个目录), + * 而 C 一律失败。不拦就是「参考给 nil、我们给 true」这种最坏的分歧。 + *

+ * 但拦下来之后 errno 报哪个,**是内核定的,不由 POSIX 统一**: + * macOS 给 {@code Is a directory, 21} / {@code Not a directory, 20}, + * Linux 的 {@code os.rename(文件, 非空目录)} 给 + * {@code Directory not empty, 39}。两边都是各自内核的答案,都不是错 + * —— 所以差分用例只比「它失败了」,不比失败成哪个 errno(约定 #98)。 + * 试过撤掉这道预检、一律交给 {@code errnoOf} 反查,结果更糟:Java 根本 + * 不报错,直接成功。 + */ + if (from.isEmpty() || to.isEmpty()) { + LuaAux.fileResult(L, false, null, 2); // ENOENT + return 3; + } + java.io.File sf = new java.io.File(from); + java.io.File df = new java.io.File(to); + int en = 0; + if (!sf.exists()) { + en = 2; // ENOENT + } else if (sf.isFile() && df.isDirectory()) { + en = 21; // EISDIR(Linux 上是 ENOTEMPTY,见上) + } else if (sf.isDirectory() && df.isFile()) { + en = 20; // ENOTDIR + } + if (en != 0) { + LuaAux.fileResult(L, false, null, en); + return 3; + } + try { + Files.move(sf.toPath(), df.toPath(), + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + L.push(LuaValue.ofBoolean(true)); + return 1; + } catch (IOException | RuntimeException e) { + LuaAux.fileResult(L, false, null, LuaAux.errnoOf(e)); + return 3; + } + } + + /** + * {@code os.tmpname}:C 默认是 {@code mkstemp("/tmp/lua_XXXXXX")}。 + *

+ * 目录要写死 {@code /tmp}:Java 的 {@code java.io.tmpdir} 在 macOS 上是 + * {@code /var/folders/...},跟参考实现给的路径对不上。 + */ + private static int tmpname(LuaState L) { + try { + File dir = new File("/tmp"); + if (!dir.isDirectory()) { + dir = null; // 退回 java.io.tmpdir + } + File f = File.createTempFile("lua_", "", dir); + L.push(LuaValue.ofString(L.newString( + f.getAbsolutePath().getBytes(StandardCharsets.UTF_8)))); + return 1; + } catch (IOException e) { + throw L.libError("unable to generate a unique filename"); + } + } + + private static final String[] CATS = { "all", "collate", "ctype", "monetary", "numeric", + "time" }; + + /** + * 当前 locale。C 里这是进程级状态:{@code setlocale} 设完之后, + * 后面的查询返回的就是新名字。实测 {@code os.setlocale("POSIX")} 之后再 + * {@code os.setlocale(nil, "time")} 返回 {@code "POSIX"},所以这里也得是 + * 一个可变字段,不能每次都回 {@code "C"}。 + */ + private static volatile String currentLocale = "C"; + + /** + * {@code os.setlocale}。 + *

+ * Java 没有 {@code setlocale},这是整块里偏离最大的一个:只认 {@code "C"} / + * {@code "POSIX"},其余一律 nil(表示没设成、locale 不变)。程序启动时是 + * {@code "C"}——Lua 自己从不调 setlocale,所以查询类调用对得上。 + */ + private static int setlocale(LuaState L) { + String loc = null; + if (!L.isNoneArg(1) && !L.arg1(1).isNil()) { + loc = LuaAux.checkString(L, 1).toCString().toJavaString(); + } + LuaAux.checkOption(L, 2, "all", CATS); // 只为复现「invalid option」那条报错 + if (loc == null) { + L.push(LuaValue.ofString(LuaString.latin1(currentLocale))); + } else if ("C".equals(loc) || "POSIX".equals(loc)) { + currentLocale = loc; + L.push(LuaValue.ofString(LuaString.latin1(loc))); + } else { + L.pushNil(); // 设不上:保持原样 + } + return 1; + } + + private static int exit(LuaState L) { + int status; + LuaValue a = L.arg1(1); + if (a.isBoolean()) { + status = a.toBoolean() ? 0 : 1; + } else { + status = (int) LuaAux.optInteger(L, 1, 0); + } + if (!L.isNoneArg(2) && L.arg1(2).toBoolean()) { + L.close(); + } + System.exit(status); + return 0; + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/PackageLib.java b/src/main/java/com/github/esrrhs/fakescript/lua/PackageLib.java new file mode 100644 index 0000000..a4ecdc7 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/PackageLib.java @@ -0,0 +1,551 @@ +package com.github.esrrhs.fakescript.lua; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +/** + * package 库与 {@code require} —— 对应 {@code loadlib.c}。 + *

+ * 与 {@code io} 那类库不同,这里没有 libc 可借:{@code dlopen} / + * {@code dlsym} 在 JVM 上没有对应物(真要接得走 JNI 或 FFI,那是另一个工程)。 + * 所以 C 搜索器走的是 {@code loadlib.c} 里那段「未启用动态库」的兜底分支—— + * 不是偷懒,那就是 C 版在这种平台上的真实行为。 + *

+ * 与参考实现不一致的地方(参考二进制是用 {@code dlopen} 编出来的): + *

    + *
  • {@code package.loadlib} 的第二个返回值是 + * {@code "dynamic libraries not enabled; check your Lua installation"} + * 而不是 {@code dlerror()} 的文本(第三个返回值 {@code "open"} 是 + * 一致的,实测过);
  • + *
  • {@code require} 一个真能找到 {@code .so} 的模块时,本实现会停在 + * 「动态库未启用」这一步,参考实现会真的去 {@code dlopen}。
  • + *
+ * 两者都只影响 C 模块;纯 Lua 的 {@code require} 逐字节一致。 + */ +public final class PackageLib { + + private PackageLib() { + } + + /* ==================== 常量 ==================== */ + + /** {@code LUA_LOADED_TABLE} */ + private static final String LOADED = "_LOADED"; + + /** {@code LUA_PRELOAD_TABLE} */ + private static final String PRELOAD = "_PRELOAD"; + + /** {@code LUA_DIRSEP} */ + private static final String DIRSEP = "/"; + + /** {@code LUA_PATH_SEP} */ + private static final String PATH_SEP = ";"; + + /** {@code LUA_PATH_MARK} */ + private static final String PATH_MARK = "?"; + + /** {@code LUA_EXEC_DIR} */ + private static final String EXEC_DIR = "!"; + + /** {@code LUA_IGMARK} */ + private static final String IGMARK = "-"; + + /** {@code LUA_VERSUFFIX} */ + private static final String VER_SUFFIX = "_5_4"; + + private static final String LUA_PATH_VAR = "LUA_PATH"; + private static final String LUA_CPATH_VAR = "LUA_CPATH"; + + /** {@code LUA_PATH_DEFAULT}({@code LUA_ROOT} 为 {@code /usr/local/} 时展开的结果) */ + private static final String PATH_DEFAULT = "/usr/local/share/lua/5.4/?.lua;" + + "/usr/local/share/lua/5.4/?/init.lua;" + + "/usr/local/lib/lua/5.4/?.lua;" + + "/usr/local/lib/lua/5.4/?/init.lua;" + + "./?.lua;./?/init.lua"; + + /** {@code LUA_CPATH_DEFAULT} */ + private static final String CPATH_DEFAULT = "/usr/local/lib/lua/5.4/?.so;" + + "/usr/local/lib/lua/5.4/loadall.so;./?.so"; + + /** 未启用动态库时的消息({@code loadlib.c} 兜底分支的 {@code DLMSG}) */ + private static final String DLMSG = + "dynamic libraries not enabled; check your Lua installation"; + + /** + * {@code LIB_FAIL}:{@code lookforfunc} 返回 {@code ERRLIB}(库打不开)时的 + * 第三个返回值。启用 dlopen 的构建里是 {@code "open"};本实现没有动态库, + * 失败一律发生在「打开」这一步,所以也必须是 {@code "open"}。 + */ + private static final String LIB_FAIL = "open"; + + private static final int ERRLIB = 1; + private static final int ERRFUNC = 2; + + /* ==================== 注册 ==================== */ + + public static void open(LuaState L) { + LuaTable pkg = new LuaTable(); + LuaAux.setFn(pkg, "loadlib", PackageLib::loadlib); + LuaAux.setFn(pkg, "searchpath", PackageLib::searchpath); + + /* + * searchers 的四个搜索器都把 package 绑成 upvalue 1。C 是 + * lua_pushvalue(-2) + lua_pushcclosure;这里直接填 CClosure, + * 排布一样。 + */ + LuaTable searchers = new LuaTable(); + LuaFunction[] sf = { + PackageLib::searcherPreload, + PackageLib::searcherLua, + PackageLib::searcherC, + PackageLib::searcherCroot, + }; + for (int i = 0; i < sf.length; i++) { + CClosure cl = new CClosure(sf[i], 1); + cl.upvalue[0].set(LuaValue.ofTable(pkg)); + searchers.set(i + 1, LuaValue.ofCClosure(cl)); + } + pkg.set("searchers", LuaValue.ofTable(searchers)); + + pkg.set("path", LuaValue.ofString(LuaString.latin1( + setPath(L, LUA_PATH_VAR, PATH_DEFAULT)))); + pkg.set("cpath", LuaValue.ofString(LuaString.latin1( + setPath(L, LUA_CPATH_VAR, CPATH_DEFAULT)))); + + pkg.set("config", LuaValue.ofString(LuaString.latin1(DIRSEP + "\n" + PATH_SEP + "\n" + + PATH_MARK + "\n" + EXEC_DIR + "\n" + IGMARK + "\n"))); + + pkg.set("loaded", LuaValue.ofTable(L.loadedTable())); + pkg.set("preload", LuaValue.ofTable(preloadTable(L))); + LuaAux.registerLib(L, "package", pkg); + + // require 进 _G,带 package 作 upvalue(不是进 package 表) + CClosure req = new CClosure(PackageLib::requireFn, 1); + req.upvalue[0].set(LuaValue.ofTable(pkg)); + L.globals.set("require", LuaValue.ofCClosure(req)); + } + + private static LuaTable preloadTable(LuaState L) { + LuaValue v = L.registry.get(PRELOAD); + if (v != null && v.isTable()) { + return v.asTable(); + } + LuaTable t = new LuaTable(); + L.registry.set(PRELOAD, LuaValue.ofTable(t)); + return t; + } + + /** {@code noenv}:注册表里的 {@code LUA_NOENV} 为真就不读环境变量 */ + private static boolean noEnv(LuaState L) { + LuaValue v = L.registry.get("LUA_NOENV"); + return v != null && v.toBoolean(); + } + + /** + * {@code setpath}:先看 {@code LUA_PATH_5_4},再看 {@code LUA_PATH},都没有 + * 才用默认值。 + *

+ * 变量值里出现 {@code ";;"} 时把默认路径插在那个位置——这是 Lua 让用户 + * 「在默认路径前面/后面追加」的机制。 + */ + private static String setPath(LuaState L, String envname, String dft) { + String path = System.getenv(envname + VER_SUFFIX); + if (path == null) { + path = System.getenv(envname); + } + if (path == null || noEnv(L)) { + return dft; + } + int mark = path.indexOf(";;"); + if (mark < 0) { + return path; + } + StringBuilder b = new StringBuilder(); + if (mark > 0) { // ';;' 之前还有一段 + b.append(path, 0, mark).append(PATH_SEP); + } + b.append(dft); + if (mark < path.length() - 2) { // ';;' 之后还有一段 + b.append(PATH_SEP).append(path, mark + 2, path.length()); + } + return b.toString(); + } + + /* ==================== 路径搜索 ==================== */ + + /** + * {@code getnextfilename} 的等价物:按 {@code ';'} 切出下一个模板。 + *

+ * 没有直接用 {@code String.split}:两者的边界行为不同—— + * {@code String.split(";", -1)} 在 {@code ";;"} 上给三个空串里的两个, + * 而 C 给整整三个。 + *

+ * 末尾分号也会产生一项空模板({@code "a;"} 切出 {@code ["a", ""]}): + * C 的 {@code getnextfilename} 先还原分隔符、再往后挪一格,此时 {@code name} + * 可能已经等于 {@code end},但它仍然返回这个空串,下一轮才认出结束。 + * 这一项观察不到——空文件名 {@code readable("")} 必为假,而错误消息 + * 又是拿未切分的整条 {@code path} 拼的({@code #errorNotFound}),不是拿 + * 这张表拼的。这里照样照 C 写,省得哪天有人拿这张表干别的事时对不上。 + */ + private static List splitPath(String path) { + List out = new ArrayList<>(); + char[] s = path.toCharArray(); + int end = s.length; + int p = 0; + while (true) { + if (p >= end) { + break; // name == end:没有更多模板了 + } + if (s[p] == '\0') { // 上一轮把分隔符改成了 '\0':还原并跳过 + s[p] = PATH_SEP.charAt(0); + p++; + } + int sep = p; + while (sep < end && s[sep] != PATH_SEP.charAt(0)) { + sep++; + } + /* + * 这步不能省。C 是把分隔符位置写成 '\0' 来切出当前模板的, + * 下一轮正是靠「这个位置是 \0」认出「刚切过」并跳过它。不写就会 + * 在遇到连续分隔符({@code ";;"})时停在原地死循环。 + */ + if (sep < end) { + s[sep] = '\0'; + } + out.add(new String(s, p, sep - p)); + p = sep; + } + return out; + } + + /** + * {@code readable}:能打开读就算数。 + *

+ * C 用的是 {@code fopen(filename, "r")},而它在目录上是成功的 —— + * 于是 {@code searchpath} 会把目录当成找到的文件返回,后面的 + * {@code luaL_loadfilex} 才在读的时候报 {@code cannot read ...: Is a directory}。 + * Java 的 {@code RandomAccessFile} 开不了目录,所以这里得先认下目录。 + */ + private static boolean readable(String filename) { + if (new java.io.File(filename).isDirectory()) { + return true; + } + try (java.io.RandomAccessFile f = new java.io.RandomAccessFile(filename, "r")) { + return true; + } catch (IOException e) { + return false; + } + } + + /** {@code pusherrornotfound}:{@code no file 'a'\n\tno file 'b'} */ + private static String errorNotFound(String path) { + StringBuilder b = new StringBuilder("no file '"); + b.append(path.replace(PATH_SEP, "'\n\tno file '")); + b.append("'"); + return b.toString(); + } + + /** + * {@code searchpath}。 + * + * @return 找到的文件名;找不到返回 {@code null} 且错误消息已压栈 + */ + private static String searchPath(LuaState L, String name, String path, String sep, + String dirsep) { + // C 只判 sep 的第一个字符在不在 name 里,替换时却替换整个 sep + if (!sep.isEmpty() && name.indexOf(sep.charAt(0)) >= 0) { + name = name.replace(sep, dirsep); + } + String pathname = path.replace(PATH_MARK, name); + for (String f : splitPath(pathname)) { + if (readable(f)) { + return f; + } + } + L.push(LuaValue.ofString(LuaString.latin1(errorNotFound(pathname)))); + return null; + } + + /** {@code ll_searchpath} */ + private static int searchpath(LuaState L) { + // 四个参数全都是 const char*(ll_searchpath 的签名 + luaL_gsub 的替换串), + // 在第一个 \0 处砍断:searchpath("a\0b", "./?.lua") 找的是 "./a.lua"。 + String name = LuaAux.checkString(L, 1).toCString().toLatin1String(); + String path = LuaAux.checkString(L, 2).toCString().toLatin1String(); + String sep = LuaAux.optString(L, 3, LuaString.latin1(".")).toCString().toLatin1String(); + String dirsep = LuaAux.optString(L, 4, LuaString.latin1(DIRSEP)).toCString().toLatin1String(); + String f = searchPath(L, name, path, sep, dirsep); + if (f != null) { + L.push(LuaValue.ofString(LuaString.latin1(f))); + return 1; + } + // 错误消息已在栈顶:把它挪到 nil 之后,返回 nil + 消息 + LuaValue msg = new LuaValue(L.stack[--L.top]); + LuaAux.pushFail(L); + L.push(msg); + return 2; + } + + /** {@code findfile}:从 {@code package.} 里找模块文件 */ + private static String findFile(LuaState L, String name, String pname, String dirsep) { + LuaTable pkg = (LuaTable) L.upvalue(0).o; + LuaValue p = pkg.get(pname); + // C 用的是 lua_tostring:数字也算,会被就地转成字符串 + if (p == null || !(p.isString() || p.isNumber())) { + throw L.libError("'package." + pname + "' must be a string"); + } + return searchPath(L, name, LuaAux.asString(p).toLatin1String(), ".", dirsep); + } + + /* ==================== 动态库(兜底:一律失败) ==================== */ + + /** {@code lookforfunc}:本实现没有动态库,永远是 {@code ERRLIB} + {@code DLMSG} */ + private static int lookForFunc(LuaState L, String path, String sym) { + L.push(LuaValue.ofString(LuaString.latin1(DLMSG))); + return ERRLIB; + } + + /** {@code loadfunc}:{@code a.b-c} 先试 {@code luaopen_a_b},失败再试 {@code luaopen_c} */ + private static int loadFunc(LuaState L, String filename, String modname) { + String m = modname.replace('.', '_'); + int mark = m.indexOf(IGMARK); + if (mark >= 0) { + int stat = lookForFunc(L, filename, "luaopen_" + m.substring(0, mark)); + if (stat != ERRFUNC) { + return stat; + } + m = m.substring(mark + 1); + } + return lookForFunc(L, filename, "luaopen_" + m); + } + + /** {@code ll_loadlib} */ + private static int loadlib(LuaState L) { + String path = LuaAux.checkString(L, 1).toJavaString(); + String init = LuaAux.checkString(L, 2).toJavaString(); + int stat = lookForFunc(L, path, init); + if (stat == 0) { + return 1; + } + LuaValue msg = new LuaValue(L.stack[--L.top]); + LuaAux.pushFail(L); + L.push(msg); + /* + * 第三个返回值的判据是 失败发生在哪一步:{@code ERRLIB} 是 + * 「库打不开」({@code lsys_load} 失败),给 {@code LIB_FAIL} + * (启用 dlopen 的构建里是 {@code "open"});{@code ERRFUNC} 是 + * 「库里没这个符号」,C 写死 {@code "init"}——与 {@code LIB_FAIL} + * 是两个不同的常量,兜底分支把 {@code LIB_FAIL} 重新定义成 + * {@code "absent"} 时并没有动它。 + *

+ * 之前这里写死 {@code "absent"},是因为照兜底分支的 {@code LIB_FAIL} + * 记的;实测(参考二进制,macOS + dlopen):库打不开给 {@code "open"}, + * 而库能打开、符号找不到给 {@code "init"}。本实现的 + * {@code lookForFunc} 恒返回 {@code ERRLIB},所以这一支走不到—— + * 照样照 C 写,免得哪天真接上 dlopen 时对不上。 + */ + L.push(LuaValue.ofString(LuaString.latin1(stat == ERRLIB ? LIB_FAIL : "init"))); + return 3; + } + + /* ==================== 搜索器 ==================== */ + + /** {@code checkload}:成功返回 {@code (loader, filename)},失败报错 */ + private static int checkLoad(LuaState L, boolean stat, String filename) { + if (stat) { + L.push(LuaValue.ofString(LuaString.latin1(filename))); + return 2; + } + LuaValue msg = L.stack[L.top - 1]; + throw L.libError("error loading module '" + L.arg1(1).asString().toLatin1String() + + "' from file '" + filename + "':\n\t" + + LuaAux.asString(msg).toLatin1String()); + } + + /** {@code searcher_preload} */ + private static int searcherPreload(LuaState L) { + // 模块名在 C 里是 const char*,第一个 \0 之后的内容连错误消息里都不出现 + String name = LuaAux.checkString(L, 1).toCString().toLatin1String(); + LuaValue v = preloadTable(L).get(name); + if (v == null || v.isNil()) { + L.push(LuaValue.ofString(LuaString.latin1( + "no field package.preload['" + name + "']"))); + return 1; + } + L.push(v); + L.push(LuaValue.ofString(LuaString.latin1(":preload:"))); + return 2; + } + + /** {@code searcher_Lua} */ + private static int searcherLua(LuaState L) { + String name = LuaAux.checkString(L, 1).toCString().toLatin1String(); + String filename = findFile(L, name, "path", DIRSEP); + if (filename == null) { + return 1; // 消息已压栈 + } + try { + /* + * 与 {@code BaseLib.loadfile} 的目录分支是同一件事的两处: + * 模块文件是目录时,C 的 fopen 成功、读才失败, + * {@code luaL_loadfilex} 报的是 + * {@code cannot read : Is a directory}。 + * 实测(参考二进制): + * {@code require("adir")} -> "error loading module 'adir' from file + * '

':\n\tcannot read

: Is a directory"。 + */ + if (new java.io.File(filename).isDirectory()) { + throw new java.io.IOException("cannot read " + filename + ": " + + LuaAux.strError(21)); + } + byte[] src = Files.readAllBytes(Paths.get(filename)); + /* + * luaL_loadfilex 的 chunk 名是 "@" + 文件名。不带这个前缀, + * 模块里的运行期错误报出来的位置就对不上。 + */ + LClosure cl = L.loadFile(src, "@" + filename); + L.push(LuaValue.ofClosure(cl)); + } catch (Exception e) { + // 与 BaseLib 的 loadZio 同理:这一层也是 luaD_protectedparser, + // 非直跳的错要在「抛出点」过一遍 L->errfunc + String m = (e instanceof LuaError) + ? LuaAux.asString(((((LuaError) e).directThrow) ? ((LuaError) e).value + : L.applyErrFunc(((LuaError) e).value))).toLatin1String() + : String.valueOf(e.getMessage()); + L.push(LuaValue.ofString(LuaString.latin1(m))); + return checkLoad(L, false, filename); + } + return checkLoad(L, true, filename); + } + + /** {@code searcher_C} */ + private static int searcherC(LuaState L) { + // 与 searcher_preload / searcher_Lua 同理:name 是 const char* + String name = LuaAux.checkString(L, 1).toCString().toLatin1String(); + String filename = findFile(L, name, "cpath", DIRSEP); + if (filename == null) { + return 1; + } + return checkLoad(L, loadFunc(L, filename, name) == 0, filename); + } + + /** {@code searcher_Croot} */ + private static int searcherCroot(LuaState L) { + String name = LuaAux.checkString(L, 1).toCString().toLatin1String(); + int p = name.indexOf('.'); + if (p < 0) { + return 0; // 就是根,交给别的搜索器 + } + String filename = findFile(L, name.substring(0, p), "cpath", DIRSEP); + if (filename == null) { + return 1; + } + int stat = loadFunc(L, filename, name); + if (stat != 0) { + if (stat != ERRFUNC) { + return checkLoad(L, false, filename); + } + L.push(LuaValue.ofString(LuaString.latin1( + "no module '" + name + "' in file '" + filename + "'"))); + return 1; + } + L.push(LuaValue.ofString(LuaString.latin1(filename))); + return 2; + } + + /* ==================== require ==================== */ + + /** 调一个 Lua 可调用值,取 {@code nres} 个结果(不足补 nil) */ + private static LuaValue[] callGet(LuaState L, LuaValue f, LuaValue a1, int nres) { + int idx = L.top; + L.ensureSlots(idx + 2 + nres); + L.stack[idx].set(f); + L.stack[idx + 1].set(a1); + L.top = idx + 2; + L.call(idx, nres); + LuaValue[] r = new LuaValue[nres]; + for (int i = 0; i < nres; i++) { + r[i] = new LuaValue(L.stack[idx + i]); + } + L.top = idx; + return r; + } + + /** + * {@code findloader}:挨个试搜索器,攒错误消息。 + * + * @return {@code [loader, loaderData]} + */ + private static LuaValue[] findLoader(LuaState L, String name) { + LuaTable pkg = (LuaTable) L.upvalue(0).o; + LuaValue sv = pkg.get("searchers"); + if (sv == null || !sv.isTable()) { + throw L.libError("'package.searchers' must be a table"); + } + LuaTable searchers = sv.asTable(); + StringBuilder msg = new StringBuilder(); + for (int i = 1;; i++) { + msg.append("\n\t"); + LuaValue s = searchers.get(LuaValue.ofInteger(i)); + if (s == null || s.isNil()) { + msg.setLength(msg.length() - 2); // 去掉刚加的前缀 + throw L.libError("module '" + name + "' not found:" + msg); + } + LuaValue[] r = callGet(L, s, LuaValue.ofString(LuaString.latin1(name)), 2); + if (r[0].baseType() == Lua.TFUNCTION) { + return r; + } + /* + * 判据是 lua_isstring,数字也算——搜索器返回 42 时消息里 + * 会出现 "42"。写成 isString 会把它当成「没给消息」整条丢掉。 + */ + if (r[0].isString() || r[0].isNumber()) { + msg.append(LuaAux.asString(r[0]).toLatin1String()); + } else { + msg.setLength(msg.length() - 2); // 这个搜索器没给消息 + } + } + } + + /** {@code ll_require} */ + private static int requireFn(LuaState L) { + // 模块名是 const char*,第一个 \0 之后的内容连 package.loaded 的键都进不去 + String name = LuaAux.checkString(L, 1).toCString().toLatin1String(); + L.setTop(1); + LuaTable loaded = L.loadedTable(); + LuaValue cached = loaded.get(name); + if (cached != null && cached.toBoolean()) { + L.push(cached); + return 1; // 已经加载过 + } + LuaValue[] found = findLoader(L, name); + LuaValue loader = found[0]; + LuaValue loaderData = found[1]; + + int idx = L.top; + L.ensureSlots(idx + 3); + L.stack[idx].set(loader); + L.stack[idx + 1].set(LuaValue.ofString(LuaString.latin1(name))); + L.stack[idx + 2].set(loaderData); + L.top = idx + 3; + L.call(idx, 1); + LuaValue res = new LuaValue(L.stack[idx]); + L.top = idx; + + if (!res.isNil()) { + loaded.set(name, res); + } + LuaValue mod = loaded.get(name); + if (mod == null || mod.isNil()) { + // 模块没设值:用 true 顶上,require 返回 true + loaded.set(name, LuaValue.ofBoolean(true)); + mod = LuaValue.ofBoolean(true); + } + L.push(mod); + L.push(loaderData); + return 2; + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/Parser.java b/src/main/java/com/github/esrrhs/fakescript/lua/Parser.java new file mode 100644 index 0000000..5b84035 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/Parser.java @@ -0,0 +1,1836 @@ +package com.github.esrrhs.fakescript.lua; + +import java.util.Locale; + +/** + * 语法分析器 —— 直译 {@code lparser.c}。 + *

+ * 上游是手写递归下降,而且边解析边发指令(没有独立的 AST):表达式解析 + * 出来的不是树,而是一个 {@link ExpDesc}(「这个值现在在哪儿」),等到不得不 + * 落地时才由 {@link Codegen} 决定最省指令的做法。所以 parser 与 codegen + * 在结构上无法分开落地——这也是这两个阶段必须一起做的原因。 + *

+ * 几处 Java 化但语义等价的改动: + *

    + *
  • {@code BlockCnt} 在 C 里是栈上分配、靠 {@code fs->bl} 串成链;Java 只能 + * 堆分配,于是每进一个块 {@code new} 一个。链的结构和遍历顺序不变;
  • + *
  • C 的 {@code enterlevel/leavelevel} 靠 {@code nCcalls} 防 C 栈溢出, + * 与 {@code luaD_call} 共用同一个计数器——所以「调用深度」和 + * 「表达式嵌套深度」是互相挤占的。给得出 {@link LuaState} 时这里就 + * 直接用它的 {@code nCcalls};只有测试那种无状态的调用才退回自带的 + * 局部计数器;
  • + *
  • {@code TString*} 的指针相等({@code eqstr})换成 {@code LuaString} 的 + * 同一性 + 内容比较。所有名字都经同一个 {@link StringPool} 驻留, + * 同一性判断在实际运行中恒成立;写 {@code eqstr} 是为了不依赖这一点;
  • + *
  • {@code int *escapelist} 这类出参改成 {@code int[]}。
  • + *
+ */ +public final class Parser { + + /** {@code MAXVARS}:单个函数的局部变量上限(必须 < 250,受字节码格式限制) */ + private static final int MAXVARS = 200; + /** {@code MAXUPVAL}({@code lfunc.h}) */ + private static final int MAXUPVAL = 255; + /** {@code SHRT_MAX}——{@code locvars} 的下标是 short */ + private static final int SHRT_MAX = 32767; + /** 递归深度上限,对应 {@code LUAI_MAXCCALLS} 的作用 */ + private static final int MAXDEPTH = 200; + + /** {@code LUA_ENV}:主函数的环境 upvalue 名字 */ + private static final String ENV_NAME = "_ENV"; + + private final Lexer ls; + private final DynData dyd; + private final LuaString source; + private final LuaString envn; + private final LuaString breakName; + + /** 当前函数的编译状态 */ + private FuncState fs; + /** + * 递归深度。给得出 {@link #L} 时不用它——深度记在 {@code L.nCcalls} 里, + * 与调用深度共用一份;只有 {@link #L} 为 {@code null}(测试直调)时才 + * 退回这个局部计数器。 + */ + private int depth; + /** + * 所属状态。{@code null} 表示这次解析不挂在某个 {@code lua_State} 上 + * (测试里比对字节码就是这么调的)。 + */ + private final LuaState L; + + private Parser(Lexer ls, DynData dyd, LuaString source, LuaState L) { + this.ls = ls; + this.dyd = dyd; + this.source = source; + this.L = L; + this.envn = ls.newString(LuaString.latin1(ENV_NAME).bytes, 0, ENV_NAME.length()); + this.breakName = ls.newString(LuaString.latin1("break").bytes, 0, 5); + } + + /** + * 解析一段源码,返回主函数的原型。 + *

+ * 上游这里返回的是带一个 upvalue({@code _ENV})的 {@code LClosure}; + * 闭包是阶段 2 后半的内容,先把 {@link Proto} 交出来,等 {@code LClosure} + * 落地后再套一层。 + */ + public static Proto parse(byte[] data, byte[] chunkName, StringPool strings) { + return parse(new Zio(data), chunkName, strings); + } + + /** + * 从一条(可能是惰性的)输入流解析。 + *

+ * {@code load} 接读函数时走这一条:字节是边解析边要的,读函数的调用次数 + * 也就与 C 一致。 + */ + public static Proto parse(Zio z, byte[] chunkName, StringPool strings) { + return parse(z, chunkName, strings, z.getc()); + } + + /** + * {@code firstChar} 见 {@link Lexer#Lexer(Zio, byte[], StringPool, int)}: + * 首字节由调用方({@code f_parser} 形态)读过了,原样交给词法器,不要重读。 + */ + public static Proto parse(Zio z, byte[] chunkName, StringPool strings, int firstChar) { + return parse(z, chunkName, strings, firstChar, null); + } + + /** + * 带 {@link LuaState} 的入口:{@code enterlevel} 要用它的 {@code nCcalls}。 + *

+ * 这一层不是可有可无的花样:C 里解析器的深度和调用深度是同一个 + * 计数器,于是 {@code require} 循环依赖时,第 200 层不是在 + * {@code lua_call} 上炸,而是在某次 {@code luaL_loadfile} 的解析中途炸—— + * 错误被 {@code luaD_protectedparser} 兜住,{@code loadfile} 返回 + * 非 {@code LUA_OK},最后由 {@code checkload} 包成 + * {@code error loading module 'x' from file 'y':\n\tC stack overflow}。 + * 各数各的就会拿到裸的 {@code C stack overflow}。 + */ + public static Proto parse(Zio z, byte[] chunkName, StringPool strings, int firstChar, + LuaState L) { + LuaString src = (chunkName == null) ? LuaString.latin1("?") : LuaString.of(chunkName); + Lexer ls = new Lexer(z, chunkName, strings, firstChar); + DynData dyd = new DynData(); + Parser p = new Parser(ls, dyd, src, L); + Proto f = new Proto(); + f.source = src; + FuncState funcstate = new FuncState(f, null, ls, dyd); + p.fs = funcstate; + p.mainFunc(funcstate); + return f; + } + + /** + * 按「从文件加载」的语义解析:先吃掉 BOM 与首行的 {@code #!...}。 + *

+ * 这一步 {@code lauxlib.c: skipcomment} 才做,不属于词法分析—— + * 用 {@code load} 加载字符串时首行写 {@code #} 照样报 + * {@code unexpected symbol}。所以单独开一个入口,别混进 {@link #parse}。 + *

+ * 容易被忽略的一点:跳过首行之后要补一个换行,否则后面所有行的 + * 行号都会少 1,调试信息全错。 + */ + public static Proto parseFile(byte[] data, byte[] chunkName, StringPool strings) { + return parse(skipFilePreamble(data), chunkName, strings); + } + + /** {@code lauxlib.c: skipBOM + skipcomment} */ + static byte[] skipFilePreamble(byte[] data) { + int off = 0; + if (data.length >= 3 && (data[0] & 0xFF) == 0xEF && (data[1] & 0xFF) == 0xBB + && (data[2] & 0xFF) == 0xBF) { + off = 3; // UTF-8 BOM 由加载层吃掉,不属于源码 + } + if (off < data.length && data[off] == '#') { + int i = off; + while (i < data.length && data[i] != '\n') { + i++; + } + if (i < data.length) { + i++; // 吃掉换行本身 + } + byte[] out = new byte[1 + data.length - i]; + out[0] = '\n'; // 补回被跳过的那一行,保住行号 + System.arraycopy(data, i, out, 1, data.length - i); + return out; + } + if (off == 0) { + return data; + } + byte[] noBom = new byte[data.length - off]; + System.arraycopy(data, off, noBom, 0, noBom.length); + return noBom; + } + + public static Proto parse(String utf8Source, String chunkName) { + return parse(utf8Source.getBytes(java.nio.charset.StandardCharsets.UTF_8), + chunkName == null ? null : chunkName.getBytes(java.nio.charset.StandardCharsets.UTF_8), + new StringPool()); + } + + /* ============================================================ */ + /* 块结构 */ + /* ============================================================ */ + + /** + * {@code BlockCnt}:活跃块链表的节点。 + *

+ * C 里它是栈上的局部结构体,靠 {@code fs->bl} 串起来;这里只能堆分配。 + */ + static final class BlockCnt { + BlockCnt previous; + int firstlabel; + int firstgoto; + int nactvar; + boolean upval; + boolean isloop; + boolean insidetbc; + } + + /** 多重赋值左侧的变量链({@code struct LHS_assign}) */ + private static final class LhsAssign { + LhsAssign prev; + final ExpDesc v = new ExpDesc(); + } + + /** 表构造器的控制结构({@code ConsControl}) */ + private static final class ConsControl { + final ExpDesc v = new ExpDesc(); + ExpDesc t; + int nh; + int na; + int tostore; + } + + /* ============================================================ */ + /* 小工具 */ + /* ============================================================ */ + + /** {@code eqstr}:驻留之后指针相等即内容相等,这里两者都认 */ + private static boolean eqstr(LuaString a, LuaString b) { + return a == b || (a != null && a.equals(b)); + } + + private static boolean hasMultRet(int k) { + return k == ExpDesc.VCALL || k == ExpDesc.VVARARG; + } + + /** {@code luaX_newstring} 的便捷形式:把一个 ASCII 字面量驻留进词法层的池 */ + private LuaString literal(String s) { + return ls.newString(LuaString.latin1(s).bytes, 0, s.length()); + } + + private LuaError errorExpected(int token) { + return ls.syntaxError(Lexer.tokenName(token) + " expected"); + } + + /** {@code errorlimit} */ + private LuaError errorLimit(FuncState fs, int limit, String what) { + int line = fs.f.linedefined; + String where = (line == 0) ? "main function" + : String.format(Locale.ROOT, "function at line %d", line); + String msg = String.format(Locale.ROOT, "too many %s (limit is %d) in %s", what, limit, where); + return ls.syntaxError(msg); + } + + private void checkLimit(FuncState fs, int v, int l, String what) { + if (v > l) { + throw errorLimit(fs, l, what); + } + } + + /** {@code testnext} */ + private boolean testNext(int c) { + if (ls.t.token == c) { + ls.next(); + return true; + } + return false; + } + + /** {@code check} */ + private void check(int c) { + if (ls.t.token != c) { + throw errorExpected(c); + } + } + + /** {@code checknext} */ + private void checkNext(int c) { + check(c); + ls.next(); + } + + /** {@code check_match} */ + private void checkMatch(int what, int who, int where) { + if (!testNext(what)) { + if (where == ls.linenumber) { + throw errorExpected(what); + } + throw ls.syntaxError(String.format(Locale.ROOT, "%s expected (to close %s at line %d)", + Lexer.tokenName(what), Lexer.tokenName(who), where)); + } + } + + /** {@code str_checkname} */ + private LuaString strCheckName() { + check(Lexer.TK_NAME); + LuaString ts = ls.t.s; + ls.next(); + return ts; + } + + /** {@code init_exp} */ + private static void initExp(ExpDesc e, int k, int i) { + e.f = e.t = Codegen.NO_JUMP; + e.k = k; + e.info = i; + } + + /** {@code codestring} */ + private static void codeString(ExpDesc e, LuaString s) { + e.f = e.t = Codegen.NO_JUMP; + e.k = ExpDesc.VKSTR; + e.strval = s; + } + + /** {@code codename} */ + private void codeName(ExpDesc e) { + codeString(e, strCheckName()); + } + + /** {@code enterlevel} / {@code leavelevel}({@code luaE_incCstack}) */ + private void enterLevel() { + if (L != null) { + L.incCstack(); + } else if (++depth >= MAXDEPTH) { + throw new LuaError(LuaValue.ofString(LuaString.latin1("C stack overflow"))); + } + } + + private void leaveLevel() { + if (L != null) { + L.decCstack(); + } else { + depth--; + } + } + + /* ============================================================ */ + /* 局部变量 */ + /* ============================================================ */ + + /** {@code registerlocalvar} */ + private int registerLocalVar(FuncState fs, LuaString varname) { + Proto.LocVar lv = fs.f.allocLocVar(); + lv.varname = varname; + lv.startpc = fs.pc; + return fs.ndebugvars++; + } + + /** {@code new_localvar} */ + private int newLocalVar(LuaString name) { + FuncState fs = this.fs; + checkLimit(fs, dyd.actvarN + 1 - fs.firstlocal, MAXVARS, "local variables"); + checkLimit(fs, dyd.actvarN + 1, SHRT_MAX, "local variables"); + DynData.Vardesc var = new DynData.Vardesc(); + var.kind = DynData.VDKREG; + var.name = name; + dyd.setActvar(dyd.actvarN, var); + return dyd.actvarN++ - fs.firstlocal; + } + + /** {@code new_localvarliteral} */ + private int newLocalVarLiteral(String name) { + return newLocalVar(literal(name)); + } + + /** {@code getlocalvardesc} */ + private static DynData.Vardesc getLocalVarDesc(FuncState fs, int vidx) { + return fs.dyd.actvar.get(fs.firstlocal + vidx); + } + + /** {@code localdebuginfo} */ + private static Proto.LocVar localDebugInfo(FuncState fs, int vidx) { + DynData.Vardesc vd = getLocalVarDesc(fs, vidx); + if (vd.kind == DynData.RDKCTC) { + return null; // 常量没有调试信息 + } + return fs.f.locvars[vd.pidx]; + } + + /** {@code init_var} */ + private static void initVar(FuncState fs, ExpDesc e, int vidx) { + e.f = e.t = Codegen.NO_JUMP; + e.k = ExpDesc.VLOCAL; + e.varVidx = vidx; + e.varRidx = getLocalVarDesc(fs, vidx).ridx; + } + + /** {@code check_readonly} */ + private void checkReadonly(ExpDesc e) { + FuncState fs = this.fs; + LuaString varname = null; + switch (e.k) { + case ExpDesc.VCONST: + varname = dyd.actvar.get(e.info).name; + break; + case ExpDesc.VLOCAL: { + DynData.Vardesc vardesc = getLocalVarDesc(fs, e.varVidx); + if (vardesc.kind != DynData.VDKREG) { + varname = vardesc.name; + } + break; + } + case ExpDesc.VUPVAL: { + Proto.Upvaldesc up = fs.f.upvalues[e.info]; + if (up.kind != DynData.VDKREG) { + varname = up.name; + } + break; + } + default: + return; // 其余情况不可能是只读的 + } + if (varname != null) { + String msg = String.format(Locale.ROOT, + "attempt to assign to const variable '%s'", varname.toJavaString()); + throw Codegen.semError(fs, msg); + } + } + + /** {@code adjustlocalvars} */ + private void adjustLocalVars(int nvars) { + FuncState fs = this.fs; + int reglevel = fs.nvarstack(); + for (int i = 0; i < nvars; i++) { + int vidx = fs.nactvar++; + DynData.Vardesc var = getLocalVarDesc(fs, vidx); + var.ridx = reglevel++; + var.pidx = registerLocalVar(fs, var.name); + } + } + + /** {@code removevars} */ + private void removeVars(FuncState fs, int tolevel) { + // 只动逻辑长度;槽位本身留着,下面的循环还要读它们去补 endpc + dyd.actvarN -= (fs.nactvar - tolevel); + while (fs.nactvar > tolevel) { + Proto.LocVar var = localDebugInfo(fs, --fs.nactvar); + if (var != null) { + var.endpc = fs.pc; + } + } + } + + /* ---------- upvalue ---------- */ + + /** {@code searchupvalue} */ + private int searchUpvalue(FuncState fs, LuaString name) { + for (int i = 0; i < fs.nups; i++) { + if (eqstr(fs.f.upvalues[i].name, name)) { + return i; + } + } + return -1; + } + + /** {@code allocupvalue} */ + private Proto.Upvaldesc allocUpvalue(FuncState fs) { + checkLimit(fs, fs.nups + 1, MAXUPVAL, "upvalues"); + Proto.Upvaldesc up = fs.f.allocUpvalue(); + fs.nups++; + return up; + } + + /** {@code newupvalue} */ + private int newUpvalue(FuncState fs, LuaString name, ExpDesc v) { + Proto.Upvaldesc up = allocUpvalue(fs); + FuncState prev = fs.prev; + if (v.k == ExpDesc.VLOCAL) { + up.instack = true; + up.idx = v.varRidx; + up.kind = getLocalVarDesc(prev, v.varVidx).kind; + } else { + up.instack = false; + up.idx = v.info; + up.kind = prev.f.upvalues[v.info].kind; + } + up.name = name; + return fs.nups - 1; + } + + /** {@code searchvar} */ + private int searchVar(FuncState fs, LuaString n, ExpDesc var) { + for (int i = fs.nactvar - 1; i >= 0; i--) { + DynData.Vardesc vd = getLocalVarDesc(fs, i); + if (eqstr(n, vd.name)) { + if (vd.kind == DynData.RDKCTC) { + initExp(var, ExpDesc.VCONST, fs.firstlocal + i); + } else { + initVar(fs, var, i); + } + return var.k; + } + } + return -1; + } + + /** {@code markupval} */ + private void markUpval(FuncState fs, int level) { + BlockCnt bl = fs.bl; + while (bl.nactvar > level) { + bl = bl.previous; + } + bl.upval = true; + fs.needclose = true; + } + + /** {@code marktobeclosed} */ + private void markToBeClosed(FuncState fs) { + BlockCnt bl = fs.bl; + bl.upval = true; + bl.insidetbc = true; + fs.needclose = true; + } + + /** {@code singlevaraux} */ + private void singlevarAux(FuncState fs, LuaString n, ExpDesc var, boolean base) { + if (fs == null) { + initExp(var, ExpDesc.VVOID, 0); // 到底了:是全局变量 + } else { + int v = searchVar(fs, n, var); + if (v >= 0) { + if (v == ExpDesc.VLOCAL && !base) { + markUpval(fs, var.varVidx); // 会被内层当 upvalue 用 + } + } else { + int idx = searchUpvalue(fs, n); + if (idx < 0) { + singlevarAux(fs.prev, n, var, false); + if (var.k == ExpDesc.VLOCAL || var.k == ExpDesc.VUPVAL) { + idx = newUpvalue(fs, n, var); + } else { + return; // 全局或常量,本层不用做什么 + } + } + initExp(var, ExpDesc.VUPVAL, idx); + } + } + } + + /** {@code singlevar} */ + private void singlevar(ExpDesc var) { + LuaString varname = strCheckName(); + FuncState fs = this.fs; + singlevarAux(fs, varname, var, true); + if (var.k == ExpDesc.VVOID) { // 是全局名 + ExpDesc key = new ExpDesc(); + singlevarAux(fs, envn, var, true); // 取 _ENV + Codegen.exp2AnyRegUp(fs, var); + codeString(key, varname); + Codegen.indexed(fs, var, key); // _ENV[varname] + } + } + + /** {@code adjust_assign} */ + private void adjustAssign(int nvars, int nexps, ExpDesc e) { + FuncState fs = this.fs; + int needed = nvars - nexps; + if (hasMultRet(e.k)) { + int extra = needed + 1; + if (extra < 0) { + extra = 0; + } + Codegen.setReturns(fs, e, extra); + } else { + if (e.k != ExpDesc.VVOID) { + Codegen.exp2NextReg(fs, e); + } + if (needed > 0) { + Codegen.codeNil(fs, fs.freereg, needed); + } + } + if (needed > 0) { + Codegen.reserveRegs(fs, needed); + } else { + fs.freereg += needed; // needed 为负,这里是收掉多余的值 + } + } + + /* ============================================================ */ + /* goto / label */ + /* ============================================================ */ + + /** {@code jumpscopeerror} */ + private void jumpScopeError(DynData.Labeldesc gt) { + LuaString varname = getLocalVarDesc(fs, gt.nactvar).name; + String msg = String.format(Locale.ROOT, + " at line %d jumps into the scope of local '%s'", + gt.name.toJavaString(), gt.line, varname.toJavaString()); + throw Codegen.semError(fs, msg); + } + + /** {@code solvegoto} */ + private void solveGoto(int g, DynData.Labeldesc label) { + DynData.Labeldesc gt = dyd.gt.get(g); + if (gt.nactvar < label.nactvar) { // 跳进了某个变量的作用域 + jumpScopeError(gt); + } + Codegen.patchList(fs, gt.pc, label.pc); + dyd.gt.remove(g); + } + + /** {@code findlabel} */ + private DynData.Labeldesc findLabel(LuaString name) { + for (int i = fs.firstlabel; i < dyd.label.size(); i++) { + DynData.Labeldesc lb = dyd.label.get(i); + if (eqstr(lb.name, name)) { + return lb; + } + } + return null; + } + + /** {@code newlabelentry} */ + private int newLabelEntry(boolean isGoto, LuaString name, int line, int pc) { + DynData.Labeldesc l = new DynData.Labeldesc(); + l.name = name; + l.line = line; + l.nactvar = fs.nactvar; + l.close = false; + l.pc = pc; + if (isGoto) { + checkLimit(fs, dyd.gt.size() + 1, SHRT_MAX, "labels/gotos"); + dyd.gt.add(l); + return dyd.gt.size() - 1; + } + checkLimit(fs, dyd.label.size() + 1, SHRT_MAX, "labels/gotos"); + dyd.label.add(l); + return dyd.label.size() - 1; + } + + /** {@code solvegotos} */ + private boolean solveGotos(DynData.Labeldesc lb) { + int i = fs.bl.firstgoto; + boolean needsclose = false; + while (i < dyd.gt.size()) { + if (eqstr(dyd.gt.get(i).name, lb.name)) { + if (dyd.gt.get(i).close) { + needsclose = true; + } + solveGoto(i, lb); // 会把 i 位置上的项删掉 + } else { + i++; + } + } + return needsclose; + } + + /** {@code createlabel} */ + private boolean createLabel(LuaString name, int line, boolean last) { + FuncState fs = this.fs; + int l = newLabelEntry(false, name, line, Codegen.getLabel(fs)); + if (last) { // 标签是块里最后一条空语句 + dyd.label.get(l).nactvar = fs.bl.nactvar; // 认为局部变量已出作用域 + } + if (solveGotos(dyd.label.get(l))) { + Codegen.codeABC(fs, OpCode.OP_CLOSE, fs.nvarstack(), 0, 0); + return true; + } + return false; + } + + /** {@code movegotosout} */ + private void moveGotosOut(FuncState fs, BlockCnt bl) { + for (int i = bl.firstgoto; i < dyd.gt.size(); i++) { + DynData.Labeldesc gt = dyd.gt.get(i); + if (FuncState.reglevel(fs, gt.nactvar) > FuncState.reglevel(fs, bl.nactvar)) { + if (bl.upval) { + gt.close = true; // 跳转可能需要 close + } + } + gt.nactvar = bl.nactvar; + } + } + + /** {@code enterblock} */ + private void enterBlock(FuncState fs, BlockCnt bl, boolean isloop) { + bl.isloop = isloop; + bl.nactvar = fs.nactvar; + bl.firstlabel = dyd.label.size(); + bl.firstgoto = dyd.gt.size(); + bl.upval = false; + bl.insidetbc = (fs.bl != null && fs.bl.insidetbc); + bl.previous = fs.bl; + fs.bl = bl; + } + + /** {@code undefgoto} */ + private void undefGoto(DynData.Labeldesc gt) { + String msg; + if (eqstr(gt.name, breakName)) { + msg = String.format(Locale.ROOT, "break outside loop at line %d", gt.line); + } else { + msg = String.format(Locale.ROOT, "no visible label '%s' for at line %d", + gt.name.toJavaString(), gt.line); + } + throw Codegen.semError(fs, msg); + } + + /** {@code leaveblock} */ + private void leaveBlock(FuncState fs) { + BlockCnt bl = fs.bl; + boolean hasclose = false; + int stklevel = FuncState.reglevel(fs, bl.nactvar); + removeVars(fs, bl.nactvar); + if (bl.isloop) { // 有未解决的 break? + hasclose = createLabel(breakName, 0, false); + } + if (!hasclose && bl.previous != null && bl.upval) { + Codegen.codeABC(fs, OpCode.OP_CLOSE, stklevel, 0, 0); + } + fs.freereg = stklevel; + while (dyd.label.size() > bl.firstlabel) { + dyd.label.remove(dyd.label.size() - 1); + } + fs.bl = bl.previous; + if (bl.previous != null) { + moveGotosOut(fs, bl); + } else if (bl.firstgoto < dyd.gt.size()) { + undefGoto(dyd.gt.get(bl.firstgoto)); + } + } + + /* ============================================================ */ + /* 函数边界 */ + /* ============================================================ */ + + /** {@code addprototype} */ + private static Proto addPrototype(FuncState fs) { + Proto clp = new Proto(); + fs.f.addNested(clp); + return clp; + } + + /** {@code codeclosure} */ + private void codeClosure(ExpDesc v) { + FuncState fs = this.fs.prev; + int np = fs.f.sizep - 1; + initExp(v, ExpDesc.VRELOC, Codegen.codeABx(fs, OpCode.OP_CLOSURE, 0, np)); + Codegen.exp2NextReg(fs, v); + } + + /** {@code open_func} */ + private void openFunc(FuncState fs, BlockCnt bl) { + Proto f = fs.f; + fs.pc = 0; + fs.previousline = f.linedefined; + fs.iwthabs = 0; + fs.lasttarget = 0; + fs.freereg = 0; + fs.nups = 0; + fs.ndebugvars = 0; + fs.nactvar = 0; + fs.needclose = false; + fs.firstlocal = dyd.actvarN; + fs.firstlabel = dyd.label.size(); + fs.bl = null; + f.source = source; + f.maxstacksize = 2; // 寄存器 0/1 永远可用 + enterBlock(fs, bl, false); + } + + /** {@code close_func} */ + private void closeFunc() { + FuncState fs = this.fs; + Proto f = fs.f; + Codegen.ret(fs, fs.nvarstack(), 0); // 末尾的 return + leaveBlock(fs); + Codegen.finish(fs); + f.truncateCode(fs.pc); + this.fs = fs.prev; + } + + /* ============================================================ */ + /* 语法规则 */ + /* ============================================================ */ + + /** {@code block_follow} */ + private boolean blockFollow(boolean withuntil) { + switch (ls.t.token) { + case Lexer.TK_ELSE: + case Lexer.TK_ELSEIF: + case Lexer.TK_END: + case Lexer.TK_EOS: + return true; + case Lexer.TK_UNTIL: + return withuntil; + default: + return false; + } + } + + /** {@code statlist} */ + private void statList() { + while (!blockFollow(true)) { + if (ls.t.token == Lexer.TK_RETURN) { + statement(); + return; // return 必须是最后一条语句 + } + statement(); + } + } + + /** {@code fieldsel} */ + private void fieldSel(ExpDesc v) { + FuncState fs = this.fs; + Codegen.exp2AnyRegUp(fs, v); + ls.next(); // 跳过 '.' 或 ':' + ExpDesc key = new ExpDesc(); + codeName(key); + Codegen.indexed(fs, v, key); + } + + /** {@code yindex} */ + private void yindex(ExpDesc v) { + ls.next(); // 跳过 '[' + expr(v); + Codegen.exp2Val(fs, v); + checkNext(']'); + } + + /* ---------- 表构造 ---------- */ + + /** {@code recfield} */ + private void recField(ConsControl cc) { + FuncState fs = this.fs; + int reg = fs.freereg; + ExpDesc tab = new ExpDesc(cc.t); + ExpDesc key = new ExpDesc(); + if (ls.t.token == Lexer.TK_NAME) { + checkLimit(fs, cc.nh, Integer.MAX_VALUE, "items in a constructor"); + codeName(key); + } else { + yindex(key); + } + cc.nh++; + checkNext('='); + Codegen.indexed(fs, tab, key); + ExpDesc val = new ExpDesc(); + expr(val); + Codegen.storeVar(fs, tab, val); + fs.freereg = reg; + } + + /** {@code closelistfield} */ + private void closeListField(ConsControl cc) { + if (cc.v.k == ExpDesc.VVOID) { + return; // 没有待存的元素 + } + Codegen.exp2NextReg(fs, cc.v); + cc.v.k = ExpDesc.VVOID; + if (cc.tostore == OpCode.LFIELDS_PER_FLUSH) { + Codegen.setList(fs, cc.t.info, cc.na, cc.tostore); + cc.na += cc.tostore; + cc.tostore = 0; + } + } + + /** {@code lastlistfield} */ + private void lastListField(ConsControl cc) { + if (cc.tostore == 0) { + return; + } + if (hasMultRet(cc.v.k)) { + Codegen.setMultiRet(fs, cc.v); + Codegen.setList(fs, cc.t.info, cc.na, Codegen.MULTRET); + cc.na--; // 最后一个表达式的元素数未知,不计数 + } else { + if (cc.v.k != ExpDesc.VVOID) { + Codegen.exp2NextReg(fs, cc.v); + } + Codegen.setList(fs, cc.t.info, cc.na, cc.tostore); + } + cc.na += cc.tostore; + } + + /** {@code constructor} */ + private void constructor(ExpDesc t) { + FuncState fs = this.fs; + int line = ls.linenumber; + int pc = Codegen.codeABC(fs, OpCode.OP_NEWTABLE, 0, 0, 0); + Codegen.code(fs, 0); // 给 EXTRAARG 留位置 + ConsControl cc = new ConsControl(); + cc.na = cc.nh = cc.tostore = 0; + cc.t = t; + initExp(t, ExpDesc.VNONRELOC, fs.freereg); + Codegen.reserveRegs(fs, 1); + initExp(cc.v, ExpDesc.VVOID, 0); + checkNext('{'); + do { + if (ls.t.token == '}') { + break; + } + closeListField(cc); + field(cc); + } while (testNext(',') || testNext(';')); + checkMatch('}', '{', line); + lastListField(cc); + Codegen.setTableSize(fs, pc, t.info, cc.na, cc.nh); + } + + /** {@code field} */ + private void field(ConsControl cc) { + switch (ls.t.token) { + case Lexer.TK_NAME: // 可能是 listfield 也可能是 recfield + if (ls.lookahead() != '=') { + listField(cc); + } else { + recField(cc); + } + break; + case '[': + recField(cc); + break; + default: + listField(cc); + break; + } + } + + /** {@code listfield} */ + private void listField(ConsControl cc) { + expr(cc.v); + cc.tostore++; + } + + /* ---------- 函数定义与调用 ---------- */ + + /** {@code setvararg} */ + private static void setVararg(FuncState fs, int nparams) { + fs.f.isVararg = true; + Codegen.codeABC(fs, OpCode.OP_VARARGPREP, nparams, 0, 0); + } + + /** {@code parlist} */ + private void parList() { + FuncState fs = this.fs; + Proto f = fs.f; + int nparams = 0; + boolean isvararg = false; + if (ls.t.token != ')') { + do { + switch (ls.t.token) { + case Lexer.TK_NAME: + newLocalVar(strCheckName()); + nparams++; + break; + case Lexer.TK_DOTS: + ls.next(); + isvararg = true; + break; + default: + throw ls.syntaxError(" or '...' expected"); + } + } while (!isvararg && testNext(',')); + } + adjustLocalVars(nparams); + f.numparams = fs.nactvar; + if (isvararg) { + setVararg(fs, f.numparams); + } + Codegen.reserveRegs(fs, fs.nactvar); + } + + /** {@code body} */ + private void body(ExpDesc e, boolean ismethod, int line) { + FuncState newFs = new FuncState(addPrototype(fs), fs, ls, dyd); + BlockCnt bl = new BlockCnt(); + newFs.f.linedefined = line; + FuncState saved = this.fs; + this.fs = newFs; + openFunc(newFs, bl); + checkNext('('); + if (ismethod) { + newLocalVarLiteral("self"); + adjustLocalVars(1); + } + parList(); + checkNext(')'); + statList(); + newFs.f.lastlinedefined = ls.linenumber; + checkMatch(Lexer.TK_END, Lexer.TK_FUNCTION, line); + codeClosure(e); + closeFunc(); + this.fs = saved; + } + + /** {@code explist} */ + private int expList(ExpDesc v) { + int n = 1; + expr(v); + while (testNext(',')) { + Codegen.exp2NextReg(fs, v); + expr(v); + n++; + } + return n; + } + + /** {@code funcargs} */ + private void funcArgs(ExpDesc f) { + FuncState fs = this.fs; + ExpDesc args = new ExpDesc(); + int base; + int nparams; + int line = ls.linenumber; + switch (ls.t.token) { + case '(': + ls.next(); + if (ls.t.token == ')') { + args.k = ExpDesc.VVOID; + } else { + expList(args); + if (hasMultRet(args.k)) { + Codegen.setMultiRet(fs, args); + } + } + checkMatch(')', '(', line); + break; + case '{': + constructor(args); + break; + case Lexer.TK_STRING: + codeString(args, ls.t.s); + ls.next(); // 必须在 next 之前取 seminfo + break; + default: + throw ls.syntaxError("function arguments expected"); + } + base = f.info; // 调用的基址寄存器 + if (hasMultRet(args.k)) { + nparams = Codegen.MULTRET; + } else { + if (args.k != ExpDesc.VVOID) { + Codegen.exp2NextReg(fs, args); + } + nparams = fs.freereg - (base + 1); + } + initExp(f, ExpDesc.VCALL, Codegen.codeABC(fs, OpCode.OP_CALL, base, nparams + 1, 2)); + Codegen.fixLine(fs, line); + fs.freereg = base + 1; // 调用吃掉函数与实参,留下一个结果 + } + + /* ---------- 表达式 ---------- */ + + /** {@code primaryexp} */ + private void primaryExp(ExpDesc v) { + switch (ls.t.token) { + case '(': { + int line = ls.linenumber; + ls.next(); + expr(v); + checkMatch(')', '(', line); + Codegen.dischargeVars(fs, v); + return; + } + case Lexer.TK_NAME: + singlevar(v); + return; + default: + throw ls.syntaxError("unexpected symbol"); + } + } + + /** {@code suffixedexp} */ + private void suffixedExp(ExpDesc v) { + FuncState fs = this.fs; + primaryExp(v); + for (;;) { + switch (ls.t.token) { + case '.': + fieldSel(v); + break; + case '[': { + ExpDesc key = new ExpDesc(); + Codegen.exp2AnyRegUp(fs, v); + yindex(key); + Codegen.indexed(fs, v, key); + break; + } + case ':': { + ExpDesc key = new ExpDesc(); + ls.next(); + codeName(key); + Codegen.self(fs, v, key); + funcArgs(v); + break; + } + case '(': + case Lexer.TK_STRING: + case '{': + Codegen.exp2NextReg(fs, v); + funcArgs(v); + break; + default: + return; + } + } + } + + /** {@code simpleexp} */ + private void simpleExp(ExpDesc v) { + switch (ls.t.token) { + case Lexer.TK_FLT: + initExp(v, ExpDesc.VKFLT, 0); + v.nval = ls.t.r; + break; + case Lexer.TK_INT: + initExp(v, ExpDesc.VKINT, 0); + v.ival = ls.t.i; + break; + case Lexer.TK_STRING: + codeString(v, ls.t.s); + break; + case Lexer.TK_NIL: + initExp(v, ExpDesc.VNIL, 0); + break; + case Lexer.TK_TRUE: + initExp(v, ExpDesc.VTRUE, 0); + break; + case Lexer.TK_FALSE: + initExp(v, ExpDesc.VFALSE, 0); + break; + case Lexer.TK_DOTS: { + FuncState fs = this.fs; + if (!fs.f.isVararg) { + throw ls.syntaxError("cannot use '...' outside a vararg function"); + } + initExp(v, ExpDesc.VVARARG, Codegen.codeABC(fs, OpCode.OP_VARARG, 0, 0, 1)); + break; + } + case '{': + constructor(v); + return; + case Lexer.TK_FUNCTION: + ls.next(); + body(v, false, ls.linenumber); + return; + default: + suffixedExp(v); + return; + } + ls.next(); + } + + /** {@code getunopr} */ + private static int getUnOpr(int op) { + switch (op) { + case Lexer.TK_NOT: + return Codegen.UNOPR_NOT; + case '-': + return Codegen.UNOPR_MINUS; + case '~': + return Codegen.UNOPR_BNOT; + case '#': + return Codegen.UNOPR_LEN; + default: + return Codegen.UNOPR_NOUNOPR; + } + } + + /** {@code getbinopr} */ + private static int getBinOpr(int op) { + switch (op) { + case '+': + return Codegen.OPR_ADD; + case '-': + return Codegen.OPR_SUB; + case '*': + return Codegen.OPR_MUL; + case '%': + return Codegen.OPR_MOD; + case '^': + return Codegen.OPR_POW; + case '/': + return Codegen.OPR_DIV; + case Lexer.TK_IDIV: + return Codegen.OPR_IDIV; + case '&': + return Codegen.OPR_BAND; + case '|': + return Codegen.OPR_BOR; + case '~': + return Codegen.OPR_BXOR; + case Lexer.TK_SHL: + return Codegen.OPR_SHL; + case Lexer.TK_SHR: + return Codegen.OPR_SHR; + case Lexer.TK_CONCAT: + return Codegen.OPR_CONCAT; + case Lexer.TK_NE: + return Codegen.OPR_NE; + case Lexer.TK_EQ: + return Codegen.OPR_EQ; + case '<': + return Codegen.OPR_LT; + case Lexer.TK_LE: + return Codegen.OPR_LE; + case '>': + return Codegen.OPR_GT; + case Lexer.TK_GE: + return Codegen.OPR_GE; + case Lexer.TK_AND: + return Codegen.OPR_AND; + case Lexer.TK_OR: + return Codegen.OPR_OR; + default: + return Codegen.OPR_NOBINOPR; + } + } + + /** 二元运算符优先级表({@code priority[]},ORDER OPR) */ + private static final int[] PRIO_LEFT = { + 10, 10, /* '+' '-' */ + 11, 11, /* '*' '%' */ + 14, /* '^' 右结合 */ + 11, 11, /* '/' '//' */ + 6, 4, 5, /* '&' '|' '~' */ + 7, 7, /* '<<' '>>' */ + 9, /* '..' 右结合 */ + 3, 3, 3, /* ==, <, <= */ + 3, 3, 3, /* ~=, >, >= */ + 2, 1 /* and, or */ + }; + + private static final int[] PRIO_RIGHT = { + 10, 10, + 11, 11, + 13, + 11, 11, + 6, 4, 5, + 7, 7, + 8, + 3, 3, 3, + 3, 3, 3, + 2, 1 + }; + + private static final int UNARY_PRIORITY = 12; + + /** + * {@code subexpr}。 + * + * @return 第一个没被吃掉的运算符(供外层继续展开) + */ + private int subExpr(ExpDesc v, int limit) { + int op; + int uop; + enterLevel(); + uop = getUnOpr(ls.t.token); + if (uop != Codegen.UNOPR_NOUNOPR) { + int line = ls.linenumber; + ls.next(); + subExpr(v, UNARY_PRIORITY); + Codegen.prefix(fs, uop, v, line); + } else { + simpleExp(v); + } + op = getBinOpr(ls.t.token); + while (op != Codegen.OPR_NOBINOPR && PRIO_LEFT[op] > limit) { + ExpDesc v2 = new ExpDesc(); + int nextop; + int line = ls.linenumber; + ls.next(); + Codegen.infix(fs, op, v); + nextop = subExpr(v2, PRIO_RIGHT[op]); + Codegen.posfix(fs, op, v, v2, line); + op = nextop; + } + leaveLevel(); + return op; + } + + private void expr(ExpDesc v) { + subExpr(v, 0); + } + + /* ---------- 语句 ---------- */ + + /** {@code block} */ + private void block() { + FuncState fs = this.fs; + BlockCnt bl = new BlockCnt(); + enterBlock(fs, bl, false); + statList(); + leaveBlock(fs); + } + + /** + * {@code check_conflict}:{@code a[i], a = 1, 2} 这类赋值里,前面的索引要用 + * 后面被赋值的那个变量的旧值,所以先把旧值拷到一个临时寄存器。 + */ + private void checkConflict(LhsAssign lh, ExpDesc v) { + FuncState fs = this.fs; + int extra = fs.freereg; + boolean conflict = false; + for (; lh != null; lh = lh.prev) { + if (lh.v.isIndexed()) { + if (lh.v.k == ExpDesc.VINDEXUP) { // 表是 upvalue + if (v.k == ExpDesc.VUPVAL && lh.v.indT == v.info) { + conflict = true; + lh.v.k = ExpDesc.VINDEXSTR; + lh.v.indT = extra; + } + } else { // 表在寄存器里 + if (v.k == ExpDesc.VLOCAL && lh.v.indT == v.varRidx) { + conflict = true; + lh.v.indT = extra; + } + if (lh.v.k == ExpDesc.VINDEXED && v.k == ExpDesc.VLOCAL + && lh.v.indIdx == v.varRidx) { + conflict = true; + lh.v.indIdx = extra; + } + } + } + } + if (conflict) { + if (v.k == ExpDesc.VLOCAL) { + Codegen.codeABC(fs, OpCode.OP_MOVE, extra, v.varRidx, 0); + } else { + Codegen.codeABC(fs, OpCode.OP_GETUPVAL, extra, v.info, 0); + } + Codegen.reserveRegs(fs, 1); + } + } + + /** {@code restassign} */ + private void restAssign(LhsAssign lh, int nvars) { + ExpDesc e = new ExpDesc(); + if (!lh.v.isVar()) { + throw ls.syntaxError("syntax error"); + } + checkReadonly(lh.v); + if (testNext(',')) { + LhsAssign nv = new LhsAssign(); + nv.prev = lh; + suffixedExp(nv.v); + if (!nv.v.isIndexed()) { + checkConflict(lh, nv.v); + } + enterLevel(); + restAssign(nv, nvars + 1); + leaveLevel(); + } else { + int nexps; + checkNext('='); + nexps = expList(e); + if (nexps != nvars) { + adjustAssign(nvars, nexps, e); + } else { + Codegen.setOneRet(fs, e); + Codegen.storeVar(fs, lh.v, e); + return; + } + } + initExp(e, ExpDesc.VNONRELOC, fs.freereg - 1); // 缺省赋值 + Codegen.storeVar(fs, lh.v, e); + } + + /** {@code cond} */ + private int cond() { + ExpDesc v = new ExpDesc(); + expr(v); + if (v.k == ExpDesc.VNIL) { + v.k = ExpDesc.VFALSE; // 这里所有 falsy 等价 + } + Codegen.goIfTrue(fs, v); + return v.f; + } + + /** {@code gotostat} */ + private void gotoStat() { + FuncState fs = this.fs; + int line = ls.linenumber; + LuaString name = strCheckName(); + DynData.Labeldesc lb = findLabel(name); + if (lb == null) { + // 向前跳,等标签声明时再解决 + newLabelEntry(true, name, line, Codegen.jump(fs)); + } else { + int lblevel = FuncState.reglevel(fs, lb.nactvar); + if (fs.nvarstack() > lblevel) { // 离开了某个变量的作用域 + Codegen.codeABC(fs, OpCode.OP_CLOSE, lblevel, 0, 0); + } + Codegen.patchList(fs, Codegen.jump(fs), lb.pc); + } + } + + /** {@code breakstat} */ + private void breakStat() { + int line = ls.linenumber; + ls.next(); + newLabelEntry(true, breakName, line, Codegen.jump(fs)); + } + + /** {@code checkrepeated} */ + private void checkRepeated(LuaString name) { + DynData.Labeldesc lb = findLabel(name); + if (lb != null) { + String msg = String.format(Locale.ROOT, "label '%s' already defined on line %d", + name.toJavaString(), lb.line); + throw Codegen.semError(fs, msg); + } + } + + /** {@code labelstat} */ + private void labelStat(LuaString name, int line) { + checkNext(Lexer.TK_DBCOLON); + while (ls.t.token == ';' || ls.t.token == Lexer.TK_DBCOLON) { + statement(); // 跳过其它空语句 + } + checkRepeated(name); + createLabel(name, line, blockFollow(false)); + } + + /** {@code whilestat} */ + private void whileStat(int line) { + FuncState fs = this.fs; + int whileinit; + int condexit; + BlockCnt bl = new BlockCnt(); + ls.next(); // 跳过 WHILE + whileinit = Codegen.getLabel(fs); + condexit = cond(); + enterBlock(fs, bl, true); + checkNext(Lexer.TK_DO); + block(); + Codegen.jumpTo(fs, whileinit); + checkMatch(Lexer.TK_END, Lexer.TK_WHILE, line); + leaveBlock(fs); + Codegen.patchToHere(fs, condexit); + } + + /** {@code repeatstat} */ + private void repeatStat(int line) { + int condexit; + FuncState fs = this.fs; + int repeatInit = Codegen.getLabel(fs); + BlockCnt bl1 = new BlockCnt(); + BlockCnt bl2 = new BlockCnt(); + enterBlock(fs, bl1, true); // 循环块 + enterBlock(fs, bl2, false); // 作用域块 + ls.next(); // 跳过 REPEAT + statList(); + checkMatch(Lexer.TK_UNTIL, Lexer.TK_REPEAT, line); + condexit = cond(); // 条件在作用域块内读 + leaveBlock(fs); // 结束作用域 + if (bl2.upval) { + int exit = Codegen.jump(fs); // 正常出口要跳过 fix + Codegen.patchToHere(fs, condexit); + Codegen.codeABC(fs, OpCode.OP_CLOSE, FuncState.reglevel(fs, bl2.nactvar), 0, 0); + condexit = Codegen.jump(fs); + Codegen.patchToHere(fs, exit); + } + Codegen.patchList(fs, condexit, repeatInit); + leaveBlock(fs); + } + + /** {@code exp1} */ + private void exp1() { + ExpDesc e = new ExpDesc(); + expr(e); + Codegen.exp2NextReg(fs, e); + } + + /** {@code fixforjump} */ + private void fixForJump(int pc, int dest, boolean back) { + int offset = dest - (pc + 1); + if (back) { + offset = -offset; + } + if (offset > OpCode.MAXARG_BX) { + throw ls.syntaxError("control structure too long"); + } + fs.f.code[pc] = OpCode.setArgBx(fs.f.code[pc], offset); + } + + /** {@code forbody} */ + private void forBody(int base, int line, int nvars, int isgen) { + BlockCnt bl = new BlockCnt(); + FuncState fs = this.fs; + int prep; + int endfor; + checkNext(Lexer.TK_DO); + prep = Codegen.codeABx(fs, (isgen == 1) ? OpCode.OP_TFORPREP : OpCode.OP_FORPREP, base, 0); + enterBlock(fs, bl, false); + adjustLocalVars(nvars); + Codegen.reserveRegs(fs, nvars); + block(); + leaveBlock(fs); + fixForJump(prep, Codegen.getLabel(fs), false); + if (isgen == 1) { + Codegen.codeABC(fs, OpCode.OP_TFORCALL, base, 0, nvars); + Codegen.fixLine(fs, line); + } + endfor = Codegen.codeABx(fs, (isgen == 1) ? OpCode.OP_TFORLOOP : OpCode.OP_FORLOOP, base, 0); + fixForJump(endfor, prep + 1, true); + Codegen.fixLine(fs, line); + } + + /** {@code fornum} */ + private void forNum(LuaString varname, int line) { + FuncState fs = this.fs; + int base = fs.freereg; + newLocalVarLiteral("(for state)"); + newLocalVarLiteral("(for state)"); + newLocalVarLiteral("(for state)"); + newLocalVar(varname); + checkNext('='); + exp1(); // 初值 + checkNext(','); + exp1(); // 上限 + if (testNext(',')) { + exp1(); // 可选步长 + } else { + Codegen.codeInt(fs, fs.freereg, 1); + Codegen.reserveRegs(fs, 1); + } + adjustLocalVars(3); // 控制变量 + forBody(base, line, 1, 0); + } + + /** {@code forlist} */ + private void forList(LuaString indexname) { + FuncState fs = this.fs; + ExpDesc e = new ExpDesc(); + int nvars = 5; // gen, state, control, toclose, indexname + int line; + int base = fs.freereg; + newLocalVarLiteral("(for state)"); + newLocalVarLiteral("(for state)"); + newLocalVarLiteral("(for state)"); + newLocalVarLiteral("(for state)"); + newLocalVar(indexname); + while (testNext(',')) { + newLocalVar(strCheckName()); + nvars++; + } + checkNext(Lexer.TK_IN); + line = ls.linenumber; + adjustAssign(4, expList(e), e); + adjustLocalVars(4); // 控制变量 + markToBeClosed(fs); // 最后一个控制变量必须被关闭 + Codegen.checkStack(fs, 3); // 调用迭代器要额外 3 个寄存器 + forBody(base, line, nvars - 4, 1); + } + + /** {@code forstat} */ + private void forStat(int line) { + LuaString varname; + BlockCnt bl = new BlockCnt(); + enterBlock(fs, bl, true); + ls.next(); // 跳过 'for' + varname = strCheckName(); + switch (ls.t.token) { + case '=': + forNum(varname, line); + break; + case ',': + case Lexer.TK_IN: + forList(varname); + break; + default: + throw ls.syntaxError("'=' or 'in' expected"); + } + checkMatch(Lexer.TK_END, Lexer.TK_FOR, line); + leaveBlock(fs); + } + + /** {@code test_then_block} */ + private void testThenBlock(int[] escapelist) { + BlockCnt bl = new BlockCnt(); + FuncState fs = this.fs; + ExpDesc v = new ExpDesc(); + int jf; + ls.next(); // 跳过 IF / ELSEIF + expr(v); + checkNext(Lexer.TK_THEN); + if (ls.t.token == Lexer.TK_BREAK) { // 'if x then break' ? + int line = ls.linenumber; + Codegen.goIfFalse(fs, v); + ls.next(); + enterBlock(fs, bl, false); + newLabelEntry(true, breakName, line, v.t); + while (testNext(';')) { + // 跳过分号 + } + if (blockFollow(false)) { // 整个块就是这条跳转 + leaveBlock(fs); + return; + } + jf = Codegen.jump(fs); // 条件为假时要跳过 then 部分 + } else { + Codegen.goIfTrue(fs, v); + enterBlock(fs, bl, false); + jf = v.f; + } + statList(); + leaveBlock(fs); + if (ls.t.token == Lexer.TK_ELSE || ls.t.token == Lexer.TK_ELSEIF) { + Codegen.concat(fs, escapelist, Codegen.jump(fs)); + } + Codegen.patchToHere(fs, jf); + } + + /** {@code ifstat} */ + private void ifStat(int line) { + FuncState fs = this.fs; + int[] escapelist = new int[] { Codegen.NO_JUMP }; + testThenBlock(escapelist); + while (ls.t.token == Lexer.TK_ELSEIF) { + testThenBlock(escapelist); + } + if (testNext(Lexer.TK_ELSE)) { + block(); + } + checkMatch(Lexer.TK_END, Lexer.TK_IF, line); + Codegen.patchToHere(fs, escapelist[0]); + } + + /** {@code localfunc} */ + private void localFunc() { + ExpDesc b = new ExpDesc(); + FuncState fs = this.fs; + int fvar = fs.nactvar; + newLocalVar(strCheckName()); + adjustLocalVars(1); + body(b, false, ls.linenumber); + // 调试信息只从这一点开始才看得到这个变量 + Proto.LocVar lv = localDebugInfo(fs, fvar); + if (lv != null) { + lv.startpc = fs.pc; + } + } + + /** {@code getlocalattribute} */ + private int getLocalAttribute() { + if (testNext('<')) { + LuaString attr = strCheckName(); + checkNext('>'); + String a = attr.toJavaString(); + if ("const".equals(a)) { + return DynData.RDKCONST; + } + if ("close".equals(a)) { + return DynData.RDKTOCLOSE; + } + throw Codegen.semError(fs, + String.format(Locale.ROOT, "unknown attribute '%s'", a)); + } + return DynData.VDKREG; + } + + /** {@code checktoclose} */ + private void checkToClose(int level) { + if (level != -1) { + FuncState fs = this.fs; + markToBeClosed(fs); + Codegen.codeABC(fs, OpCode.OP_TBC, FuncState.reglevel(fs, level), 0, 0); + } + } + + /** {@code localstat} */ + private void localStat() { + FuncState fs = this.fs; + int toclose = -1; + int vidx = 0; + int nvars = 0; + int nexps; + ExpDesc e = new ExpDesc(); + do { + vidx = newLocalVar(strCheckName()); + int kind = getLocalAttribute(); + getLocalVarDesc(fs, vidx).kind = kind; + if (kind == DynData.RDKTOCLOSE) { + if (toclose != -1) { + throw Codegen.semError(fs, "multiple to-be-closed variables in local list"); + } + toclose = fs.nactvar + nvars; + } + nvars++; + } while (testNext(',')); + if (testNext('=')) { + nexps = expList(e); + } else { + e.k = ExpDesc.VVOID; + nexps = 0; + } + DynData.Vardesc var = getLocalVarDesc(fs, vidx); + LuaValue cv = new LuaValue(); + if (nvars == nexps && var.kind == DynData.RDKCONST && Codegen.exp2Const(fs, e, cv)) { + var.kind = DynData.RDKCTC; // 编译期常量:不占寄存器 + var.k = new LuaValue(cv); + adjustLocalVars(nvars - 1); + fs.nactvar++; + } else { + adjustAssign(nvars, nexps, e); + adjustLocalVars(nvars); + } + checkToClose(toclose); + } + + /** {@code funcname} */ + private boolean funcName(ExpDesc v) { + boolean ismethod = false; + singlevar(v); + while (ls.t.token == '.') { + fieldSel(v); + } + if (ls.t.token == ':') { + ismethod = true; + fieldSel(v); + } + return ismethod; + } + + /** {@code funcstat} */ + private void funcStat(int line) { + boolean ismethod; + ExpDesc v = new ExpDesc(); + ExpDesc b = new ExpDesc(); + ls.next(); // 跳过 FUNCTION + ismethod = funcName(v); + body(b, ismethod, line); + checkReadonly(v); + Codegen.storeVar(fs, v, b); + Codegen.fixLine(fs, line); // 定义「发生」在第一行 + } + + /** {@code exprstat} */ + private void exprStat() { + FuncState fs = this.fs; + LhsAssign v = new LhsAssign(); + suffixedExp(v.v); + if (ls.t.token == '=' || ls.t.token == ',') { + v.prev = null; + restAssign(v, 1); + } else { + if (v.v.k != ExpDesc.VCALL) { + throw ls.syntaxError("syntax error"); + } + // 作为语句的调用不需要结果 + fs.f.code[v.v.info] = OpCode.setArgC(fs.f.code[v.v.info], 1); + } + } + + /** {@code retstat} */ + private void retStat() { + FuncState fs = this.fs; + ExpDesc e = new ExpDesc(); + int nret; + int first = fs.nvarstack(); + if (blockFollow(true) || ls.t.token == ';') { + nret = 0; + } else { + nret = expList(e); + if (hasMultRet(e.k)) { + Codegen.setMultiRet(fs, e); + if (e.k == ExpDesc.VCALL && nret == 1 && !fs.bl.insidetbc) { + // 尾调用 + fs.f.code[e.info] = OpCode.setOpCode(fs.f.code[e.info], OpCode.OP_TAILCALL); + } + nret = Codegen.MULTRET; + } else { + if (nret == 1) { + first = Codegen.exp2AnyReg(fs, e); + } else { + Codegen.exp2NextReg(fs, e); + } + } + } + Codegen.ret(fs, first, nret); + testNext(';'); + } + + /** {@code statement} */ + private void statement() { + int line = ls.linenumber; + enterLevel(); + switch (ls.t.token) { + case ';': + ls.next(); + break; + case Lexer.TK_IF: + ifStat(line); + break; + case Lexer.TK_WHILE: + whileStat(line); + break; + case Lexer.TK_DO: + ls.next(); + block(); + checkMatch(Lexer.TK_END, Lexer.TK_DO, line); + break; + case Lexer.TK_FOR: + forStat(line); + break; + case Lexer.TK_REPEAT: + repeatStat(line); + break; + case Lexer.TK_FUNCTION: + funcStat(line); + break; + case Lexer.TK_LOCAL: + ls.next(); + if (testNext(Lexer.TK_FUNCTION)) { + localFunc(); + } else { + localStat(); + } + break; + case Lexer.TK_DBCOLON: + ls.next(); + labelStat(strCheckName(), line); + break; + case Lexer.TK_RETURN: + ls.next(); + retStat(); + break; + case Lexer.TK_BREAK: + breakStat(); + break; + case Lexer.TK_GOTO: + ls.next(); + gotoStat(); + break; + default: + exprStat(); + break; + } + fs.freereg = fs.nvarstack(); // 释放临时寄存器 + leaveLevel(); + } + + /* ---------- 入口 ---------- */ + + /** {@code mainfunc} */ + private void mainFunc(FuncState funcstate) { + BlockCnt bl = new BlockCnt(); + openFunc(funcstate, bl); + setVararg(funcstate, 0); // 主函数总是 vararg + Proto.Upvaldesc env = allocUpvalue(funcstate); + env.instack = true; + env.idx = 0; + env.kind = DynData.VDKREG; + env.name = envn; + ls.next(); // 读第一个 token + statList(); + check(Lexer.TK_EOS); + closeFunc(); + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/Proto.java b/src/main/java/com/github/esrrhs/fakescript/lua/Proto.java new file mode 100644 index 0000000..c7b7830 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/Proto.java @@ -0,0 +1,248 @@ +package com.github.esrrhs.fakescript.lua; + +import java.util.Arrays; + +/** + * 函数原型 —— 对应 {@code lobject.h} 的 {@code Proto},以及 {@code lfunc.c} 里的 + * 一部分访问函数。 + *

+ * 一个 Proto 就是一段编译产物:指令、常量表、内嵌函数、upvalue 描述、局部变量 + * 描述、以及行号信息。运行期 {@code LClosure} 引用它,同一个 Proto 可以被多个 + * 闭包共享({@code local f = function() end} 在循环里就会共享)。 + *

+ * 没有 {@code CommonHeader} 与 {@code gclist}——那是给 {@code lgc.c} 用的链表字段, + * 这里由 JVM 管生命周期,不需要。 + */ +public final class Proto { + + /** + * {@code ABSLINEINFO}:写在 {@code lineinfo} 里的哨兵值,表示「这一条的行号 + * 在 abslineinfo 里另存」,因为相对差值塞不进一个字节。 + */ + public static final int ABSLINEINFO = -0x80; + + /** {@code MAXIWTHABS}:每隔这么多条指令就存一次绝对行号,便于二分查找 */ + public static final int MAXIWTHABS = 128; + + /** + * upvalue 描述({@code Upvaldesc})。 + *

+ * {@code kind} 用的是 {@code RDKREG/RDKCONST/RDKTOCLOSE/RDKCTC},运行期据此 + * 决定要不要为它生成 to-be-closed 的清理动作。 + */ + public static final class Upvaldesc { + public LuaString name; + /** 是否在本函数的寄存器栈上(false 表示在外层函数的 upvalue 列表里) */ + public boolean instack; + /** 寄存器号,或外层 upvalue 列表的下标 */ + public int idx; + /** {@code kind}:RDKREG / RDKCONST / RDKTOCLOSE / RDKCTC */ + public int kind; + + public Upvaldesc() { + } + } + + /** 局部变量描述({@code LocVar}),仅供调试信息 */ + public static final class LocVar { + public LuaString varname; + /** 变量生效的第一条指令 */ + public int startpc; + /** 变量失效的第一条指令 */ + public int endpc; + + public LocVar() { + } + } + + /** 绝对行号条目({@code AbsLineInfo}) */ + public static final class AbsLineInfo { + public int pc; + public int line; + + public AbsLineInfo(int pc, int line) { + this.pc = pc; + this.line = line; + } + } + + /** 固定参数个数 */ + public int numparams; + /** 是否是可变参数函数 */ + public boolean isVararg; + /** 本函数需要的寄存器数 */ + public int maxstacksize; + + public int[] code = new int[0]; + public int sizecode; + + /** 常量表。每个元素是一份拷贝,槽位在运行期不变(字节码里按下标引用) */ + public LuaValue[] k = new LuaValue[0]; + public int sizek; + + /** 内嵌的函数原型 */ + public Proto[] p = new Proto[0]; + public int sizep; + + public Upvaldesc[] upvalues = new Upvaldesc[0]; + public int sizeupvalues; + + public LocVar[] locvars = new LocVar[0]; + public int sizelocvars; + + /** 相对行号(与上一条的差值),{@link #ABSLINEINFO} 表示走绝对行号 */ + public byte[] lineinfo = new byte[0]; + public int sizelineinfo; + + public AbsLineInfo[] abslineinfo = new AbsLineInfo[0]; + public int sizeabslineinfo; + + /** 调试信息:函数定义起始行 / 结束行 */ + public int linedefined; + public int lastlinedefined; + + /** 源名(调试信息) */ + public LuaString source; + + /* ---------- 构建期辅助 ---------- */ + + /** + * 写第 {@code pc} 条指令。 + *

+ * 用「按下标写」而不是「追加」:codegen 里 {@code removelastinstruction} + * 会回退 {@code pc} 再重写同一条,追加语义会在那里长出一条多余的指令。 + */ + public void setCode(int pc, int instr) { + if (pc >= code.length) { + int n = Math.max(8, code.length); + while (n <= pc) { + n *= 2; + } + code = Arrays.copyOf(code, n); + } + code[pc] = instr; + if (pc + 1 > sizecode) { + sizecode = pc + 1; + } + } + + /** 写第 {@code pc} 条的行号信息({@code lineinfo} 与 {@code code} 一一对应) */ + public void setLineInfo(int pc, byte v) { + if (pc >= lineinfo.length) { + int n = Math.max(8, lineinfo.length); + while (n <= pc) { + n *= 2; + } + lineinfo = Arrays.copyOf(lineinfo, n); + } + lineinfo[pc] = v; + if (pc + 1 > sizelineinfo) { + sizelineinfo = pc + 1; + } + } + + public void addAbsLineInfo(int pc, int line) { + if (sizeabslineinfo >= abslineinfo.length) { + abslineinfo = Arrays.copyOf(abslineinfo, Math.max(8, abslineinfo.length * 2)); + } + abslineinfo[sizeabslineinfo++] = new AbsLineInfo(pc, line); + } + + /** 追加一个常量(不去重,去重在 Codegen 里做),返回下标 */ + public int addConstant(LuaValue v) { + if (sizek >= k.length) { + k = Arrays.copyOf(k, Math.max(8, k.length * 2)); + } + k[sizek] = new LuaValue(v); + return sizek++; + } + + public int addNested(Proto child) { + if (sizep >= p.length) { + p = Arrays.copyOf(p, Math.max(4, p.length * 2)); + } + p[sizep++] = child; + return sizep - 1; + } + + /** 把指令数截断到 {@code n}(配合 {@link #sizecode} 回退使用) */ + public void truncateCode(int n) { + sizecode = n; + } + + /** + * 追加一个空的 upvalue 槽位并返回它({@code lparser.c: allocupvalue})。 + *

+ * 返回的是同一个对象,调用方拿到后直接填字段——不要在别处 + * {@code new} 一个再塞回来,那样 {@code Parser} 里「先分配后填」的顺序 + * 就会被打乱。 + */ + public Upvaldesc allocUpvalue() { + if (sizeupvalues >= upvalues.length) { + upvalues = Arrays.copyOf(upvalues, Math.max(4, upvalues.length * 2)); + } + if (upvalues[sizeupvalues] == null) { + upvalues[sizeupvalues] = new Upvaldesc(); + } + return upvalues[sizeupvalues++]; + } + + /** 追加一个空的局部变量槽位并返回它({@code lparser.c: registerlocalvar}) */ + public LocVar allocLocVar() { + if (sizelocvars >= locvars.length) { + locvars = Arrays.copyOf(locvars, Math.max(8, locvars.length * 2)); + } + if (locvars[sizelocvars] == null) { + locvars[sizelocvars] = new LocVar(); + } + return locvars[sizelocvars++]; + } + + /* ---------- 调试信息 ---------- */ + + /** + * {@code ldebug.c: luaG_getfuncline}:由 pc 反推行号。 + *

+ * 行号是差分存的,所以只能从最近的一个「绝对行号锚点」线性累加过去; + * {@code abslineinfo} 的作用就是让这个锚点不要离得太远。 + */ + public int getFuncline(int pc) { + if (sizelineinfo == 0) { + return -1; // 没有调试信息 + } + int[] basepcHolder = new int[1]; + int baseline = getBaseline(pc, basepcHolder); + int basepc = basepcHolder[0]; + while (basepc++ < pc) { + baseline += lineinfo[basepc]; + } + return baseline; + } + + /** {@code getbaseline} */ + private int getBaseline(int pc, int[] basepcOut) { + if (sizeabslineinfo == 0 || pc < abslineinfo[0].pc) { + basepcOut[0] = -1; // 从头开始 + return linedefined; + } + int i = pc / MAXIWTHABS - 1; // 估算(必须是正确锚点的下界) + while (i + 1 < sizeabslineinfo && pc >= abslineinfo[i + 1].pc) { + i++; + } + basepcOut[0] = abslineinfo[i].pc; + return abslineinfo[i].line; + } + + /** {@code lfunc.c: luaF_getlocalname} */ + public LuaString getLocalName(int localNumber, int pc) { + for (int i = 0; i < sizelocvars && locvars[i].startpc <= pc; i++) { + if (pc < locvars[i].endpc) { // 变量在这一点生效? + localNumber--; + if (localNumber == 0) { + return locvars[i].varname; + } + } + } + return null; + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/StringLib.java b/src/main/java/com/github/esrrhs/fakescript/lua/StringLib.java new file mode 100644 index 0000000..3a227e4 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/StringLib.java @@ -0,0 +1,1788 @@ +package com.github.esrrhs.fakescript.lua; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; + +/** + * 字符串库 —— 对应 {@code lstrlib.c}。 + *

+ * 这是阶段 7 里最大的一块,也是「不作废任何一行」最容易翻车的一块: + * 模式匹配、{@code string.format} 都是逐字节算法,而 Java 的 + * {@code String} 是 UTF-16 并且拒绝非法序列。所以这里没有一处用 Java + * String 处理 Lua 字符串内容——全部走 {@code byte[]}。 + *

+ * 三个刻意的缺口,都留到后面的阶段补(见 README 的「已知差异」): + *

    + *
  • {@code string.dump}:要等阶段 8 的二进制 chunk 导出({@code ldump.c}); + * 现在一律报「unable to dump given function」。
  • + *
  • {@code string.pack} / {@code unpack} / {@code packsize}:约五百行机械代码, + * 尚未移植。
  • + *
  • {@code string.format} 的 {@code %p}:打印的是 Java 的 identity hash, + * 不可能是 C 的指针值。
  • + *
+ */ +public final class StringLib { + + private StringLib() { + } + + /** {@code LUA_MAXCAPTURES} */ + private static final int MAXCAPTURES = 32; + /** {@code MAXCCALLS}:{@code match} 的递归深度上限 */ + private static final int MAXCCALLS = 200; + private static final int L_ESC = '%'; + private static final String SPECIALS = "^$*+?.([%-"; + /** {@code MAXSIZE}:C 版是 {@code INT_MAX}(size_t 比 int 宽时的取值) */ + private static final long MAXSIZE = Integer.MAX_VALUE; + /** 非 {@code %f} 的格式项都放得下(见 {@code lstrlib.c} 的注释) */ + private static final int MAX_ITEM = 120; + /** {@code MAX_ITEMF = 110 + MAX_10_EXP} */ + private static final int MAX_ITEMF = 110 + 308; + /** {@code MAX_FORMAT} */ + private static final int MAX_FORMAT = 32; + + private static final int CAP_UNFINISHED = -1; + private static final int CAP_POSITION = -2; + + /* ==================== 开库 ==================== */ + + /** + * {@code luaopen_string}(外加 {@code createmetatable} 里那句 + * {@code metatable.__index = string})。 + *

+ * 必须在 {@link BaseLib#open} 之后跑:字符串元表是 {@code BaseLib} 建的, + * 这里只往里补 {@code __index}。少了这一句 {@code ("x"):upper()} 就会报 + * 「attempt to index a string value」,而真实脚本里方法式调用是常态。 + */ + public static void open(LuaState L) { + LuaTable s = new LuaTable(); + LuaAux.setFn(s, "byte", StringLib::byteFn); + LuaAux.setFn(s, "char", StringLib::charFn); + LuaAux.setFn(s, "dump", StringLib::dump); + LuaAux.setFn(s, "find", StringLib::find); + LuaAux.setFn(s, "format", StringLib::format); + LuaAux.setFn(s, "gmatch", StringLib::gmatch); + LuaAux.setFn(s, "gsub", StringLib::gsub); + LuaAux.setFn(s, "len", StringLib::len); + LuaAux.setFn(s, "lower", StringLib::lower); + LuaAux.setFn(s, "match", StringLib::matchFn); + LuaAux.setFn(s, "pack", StringPack::pack); + LuaAux.setFn(s, "packsize", StringPack::packsize); + LuaAux.setFn(s, "rep", StringLib::rep); + LuaAux.setFn(s, "reverse", StringLib::reverse); + LuaAux.setFn(s, "sub", StringLib::sub); + LuaAux.setFn(s, "unpack", StringPack::unpack); + LuaAux.setFn(s, "upper", StringLib::upper); + LuaAux.registerLib(L, "string", s); + + LuaTable mt = L.typeMetatable(Lua.TSTRING); + if (mt != null) { + mt.set("__index", LuaValue.ofTable(s)); + } + } + + /* ==================== 简单函数 ==================== */ + + /** {@code str_len} */ + private static int len(LuaState L) { + L.push(LuaValue.ofInteger(LuaAux.checkString(L, 1).length())); + return 1; + } + + /** + * {@code lstrlib.c: posrelatI}:把相对位置翻成正的,并夹到 {@code [1, ...]}。 + *

+ * 返回 {@code long} 而不是 {@code int}:{@code ("abc"):sub(2^40)} 这种 + * 越界下标在 C 里是 {@code size_t},截断成 int 会把「很远」变成「很近」, + * 进而切出一个根本不该存在的子串。 + */ + static long posrelatI(long pos, int len) { + if (pos > 0) { + return pos; + } + if (pos == 0) { + return 1; + } + if (pos < -(long) len) { + return 1; + } + return len + pos + 1; + } + + /** {@code getendpos}:可选的结束位置,默认 {@code def},夹到 {@code [0, len]} */ + private static long getEndPos(LuaState L, int arg, long def, int len) { + long pos = LuaAux.optInteger(L, arg, def); + if (pos > (long) len) { + return len; + } + if (pos >= 0) { + return pos; + } + if (pos < -(long) len) { + return 0; + } + return len + pos + 1; + } + + /** {@code str_sub} */ + private static int sub(LuaState L) { + LuaString s = LuaAux.checkString(L, 1); + int l = s.length(); + long start = posrelatI(LuaAux.checkInteger(L, 2), l); + long end = getEndPos(L, 3, -1, l); + if (start <= end) { + L.push(LuaValue.ofString(s.substring((int) (start - 1), (int) end))); + } else { + L.push(LuaValue.ofString(LuaString.EMPTY)); + } + return 1; + } + + /** {@code str_reverse} */ + private static int reverse(LuaState L) { + LuaString s = LuaAux.checkString(L, 1); + byte[] b = s.bytes; + byte[] out = new byte[b.length]; + for (int i = 0; i < b.length; i++) { + out[i] = b[b.length - i - 1]; + } + L.push(LuaValue.ofString(L.newString(out))); + return 1; + } + + /** {@code str_lower}:按 ASCII 转,不用 Java 的 {@code toLowerCase} */ + private static int lower(LuaState L) { + LuaString s = LuaAux.checkString(L, 1); + byte[] out = new byte[s.length()]; + for (int i = 0; i < out.length; i++) { + int c = s.byteAt(i); + out[i] = (byte) ((c >= 'A' && c <= 'Z') ? c + 32 : c); + } + L.push(LuaValue.ofString(L.newString(out))); + return 1; + } + + private static int upper(LuaState L) { + LuaString s = LuaAux.checkString(L, 1); + byte[] out = new byte[s.length()]; + for (int i = 0; i < out.length; i++) { + int c = s.byteAt(i); + out[i] = (byte) ((c >= 'a' && c <= 'z') ? c - 32 : c); + } + L.push(LuaValue.ofString(L.newString(out))); + return 1; + } + + /** + * {@code str_rep}。 + *

+ * 溢出检查照抄 C:{@code l + lsep > MAXSIZE / n}。写成 + * {@code n * (l + lsep) > MAXSIZE} 会在 {@code n} 很大时自己先溢出—— + * 正是这里要防的那件事。 + */ + private static int rep(LuaState L) { + LuaString s = LuaAux.checkString(L, 1); + long n = LuaAux.checkInteger(L, 2); + LuaString sep = LuaAux.optString(L, 3, LuaString.EMPTY); + int l = s.length(); + int lsep = sep.length(); + if (n <= 0) { + L.push(LuaValue.ofString(LuaString.EMPTY)); + return 1; + } + if ((long) l + lsep > MAXSIZE / n) { + throw L.libError("resulting string too large"); + } + long total = (long) n * l + (long) (n - 1) * lsep; + byte[] out = new byte[(int) total]; + int p = 0; + while (n-- > 1) { + System.arraycopy(s.bytes, 0, out, p, l); + p += l; + if (lsep > 0) { + System.arraycopy(sep.bytes, 0, out, p, lsep); + p += lsep; + } + } + System.arraycopy(s.bytes, 0, out, p, l); + L.push(LuaValue.ofString(L.newString(out))); + return 1; + } + + /** {@code str_byte}:一次可以返回很多个字节值 */ + private static int byteFn(LuaState L) { + LuaString s = LuaAux.checkString(L, 1); + int l = s.length(); + long pi = LuaAux.optInteger(L, 2, 1); + long posi = posrelatI(pi, l); + long pose = getEndPos(L, 3, pi, l); + if (posi > pose) { + return 0; // 空区间,一个值也不返回 + } + if (pose - posi >= Integer.MAX_VALUE) { + throw L.libError("string slice too long"); + } + int n = (int) (pose - posi) + 1; + LuaAux.checkStack(L, n, "string slice too long"); + for (int i = 0; i < n; i++) { + L.push(LuaValue.ofInteger(s.byteAt((int) (posi + i - 1)))); + } + return n; + } + + /** + * {@code str_char}。 + *

+ * 范围检查是 {@code 0..255},负数一样要拦:C 版先把值转成 + * {@code lua_Unsigned} 再比 {@code UCHAR_MAX},负数会变成一个巨大的无符号数, + * 于是 {@code string.char(-1)} 也是「value out of range」。 + */ + private static int charFn(LuaState L) { + int n = L.argCount(); + byte[] out = new byte[n]; + for (int i = 0; i < n; i++) { + long c = LuaAux.checkInteger(L, i + 1); + if (c < 0 || c > 255) { + throw L.argError(i + 1, "value out of range"); + } + out[i] = (byte) c; + } + L.push(LuaValue.ofString(L.newString(out))); + return 1; + } + + /** + * {@code str_dump}:把一个 Lua 函数导出成二进制 chunk。 + *

+ * 只接受 Lua 函数:C 函数没有 {@code Proto} 可 dump,报的是 + * 「Lua function expected」而不是笼统的「function expected」。 + *

+ * {@code strip} 为真时剥掉调试信息(行号、局部变量名、upvalue 名、源名)。 + * 剥完之后仍能执行,只是报错时没有位置前缀。 + */ + private static int dump(LuaState L) { + /* + * 两步检查,顺序不能换,消息也不同: + * {@code luaL_checktype} 先挡「根本不是函数」——报 + * {@code function expected, got X}(连「没给参数」也是这一条); + * 然后 {@code lua_dump} 对非 Lua 函数直接返回 1,于是 C 函数落到 + * {@code luaL_error("unable to dump given function")}。 + * 合成一步会把 C 函数也报成「Lua function expected」。 + */ + LuaAux.checkType(L, 1, Lua.TFUNCTION); + LuaValue v = L.arg1(1); + boolean strip = !L.isNoneArg(2) && L.arg1(2).toBoolean(); + if (v.tt != Lua.VLIF) { + throw L.libError("unable to dump given function"); + } + L.setTop(1); + LClosure cl = (LClosure) v.o; + byte[] bytes = Dump.dumpBytes(cl.p, strip); + L.push(LuaValue.ofString(L.newString(bytes))); + return 1; + } + + /* ==================== 模式匹配 ==================== */ + + /** + * {@code MatchState}。 + *

+ * C 版用 {@code const char*} 在源串与模式串里游走,这里换成下标: + * {@code s} / {@code p} 是偏移量,{@code -1} 表示 C 里的 {@code NULL}(匹配失败)。 + *

+ * 越界读要按 C 的语义返回 0:Lua 字符串在 C 里总有一个结尾的 {@code \0}, + * 于是 {@code *s} 在 {@code s == src_end} 时读到的是 0 而不是越界。 + * {@code %b\0\0} 这类以 NUL 为分隔符的模式正是靠这个成立的。 + */ + private static final class MatchState { + LuaState L; + byte[] src; + int srcEnd; + byte[] pat; + int pStart; + int pEnd; + int matchdepth; + int level; + final int[] capInit = new int[MAXCAPTURES]; + final int[] capLen = new int[MAXCAPTURES]; + + int sAt(int i) { + return (i >= 0 && i < srcEnd) ? (src[i] & 0xFF) : 0; + } + + int pAt(int i) { + return (i >= 0 && i < pEnd) ? (pat[i] & 0xFF) : 0; + } + } + + private static void prepState(MatchState ms, LuaState L, LuaString s, LuaString pat, + int pStart, int lp) { + ms.L = L; + ms.matchdepth = MAXCCALLS; + ms.src = s.bytes; + ms.srcEnd = s.length(); + ms.pat = pat.bytes; + ms.pStart = pStart; + ms.pEnd = pStart + lp; + } + + private static void reprepState(MatchState ms) { + ms.level = 0; + ms.matchdepth = MAXCCALLS; + } + + /** {@code classend}:跳过一个「单字符匹配项」(含 {@code %x}、{@code [...]}) */ + private static int classEnd(MatchState ms, int p) { + int c = ms.pAt(p++); + switch (c) { + case L_ESC: + if (p == ms.pEnd) { + throw ms.L.libError("malformed pattern (ends with '%')"); + } + return p + 1; + case '[': + if (ms.pAt(p) == '^') { + p++; + } + do { + if (p == ms.pEnd) { + throw ms.L.libError("malformed pattern (missing ']')"); + } + if (ms.pAt(p++) == L_ESC && p < ms.pEnd) { + p++; + } + } while (ms.pAt(p) != ']'); + return p + 1; + default: + return p; + } + } + + /** C locale 下的 {@code isalpha} 等,一律按 ASCII 判 */ + private static boolean isClass(int c, int cl) { + boolean res; + switch (Character.toLowerCase(cl)) { + case 'a': + res = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); + break; + case 'c': + res = c < 32 || c == 127; + break; + case 'd': + res = c >= '0' && c <= '9'; + break; + case 'g': + res = c > 32 && c < 127; + break; + case 'l': + res = c >= 'a' && c <= 'z'; + break; + case 'p': + res = (c > 32 && c < 127) && !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') + || (c >= '0' && c <= '9')); + break; + case 's': + res = c == ' ' || c == '\t' || c == '\n' || c == 11 || c == 12 || c == '\r'; + break; + case 'u': + res = c >= 'A' && c <= 'Z'; + break; + case 'w': + res = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'); + break; + case 'x': + res = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); + break; + case 'z': + res = (c == 0); + break; + default: + return cl == c; + } + return (cl >= 'a' && cl <= 'z') ? res : !res; + } + + /** {@code matchbracketclass}:{@code p} 指向 {@code [},{@code ec} 指向 {@code ]} */ + private static boolean matchBracketClass(MatchState ms, int c, int p, int ec) { + int sig = 1; + if (ms.pAt(p + 1) == '^') { + sig = 0; + p++; + } + while (++p < ec) { + if (ms.pAt(p) == L_ESC) { + p++; + if (isClass(c, ms.pAt(p))) { + return sig != 0; + } + } else if (ms.pAt(p + 1) == '-' && p + 2 < ec) { + p += 2; + if (ms.pAt(p - 2) <= c && c <= ms.pAt(p)) { + return sig != 0; + } + } else if (ms.pAt(p) == c) { + return sig != 0; + } + } + return sig == 0; + } + + /** {@code singlematch} */ + private static boolean singleMatch(MatchState ms, int s, int p, int ep) { + if (s >= ms.srcEnd) { + return false; + } + int c = ms.sAt(s); + int pc = ms.pAt(p); + if (pc == '.') { + return true; + } + if (pc == L_ESC) { + return isClass(c, ms.pAt(p + 1)); + } + if (pc == '[') { + return matchBracketClass(ms, c, p, ep - 1); + } + return pc == c; + } + + /** {@code matchbalance}({@code %b}) */ + private static int matchBalance(MatchState ms, int s, int p) { + if (p >= ms.pEnd - 1) { + throw ms.L.libError("malformed pattern (missing arguments to '%b')"); + } + if (ms.sAt(s) != ms.pAt(p)) { + return -1; + } + int b = ms.pAt(p); + int e = ms.pAt(p + 1); + int cont = 1; + while (++s < ms.srcEnd) { + int sc = ms.sAt(s); + if (sc == e) { + if (--cont == 0) { + return s + 1; + } + } else if (sc == b) { + cont++; + } + } + return -1; + } + + private static int maxExpand(MatchState ms, int s, int p, int ep) { + int i = 0; + while (singleMatch(ms, s + i, p, ep)) { + i++; + } + while (i >= 0) { + int res = match(ms, s + i, ep + 1); + if (res >= 0) { + return res; + } + i--; + } + return -1; + } + + private static int minExpand(MatchState ms, int s, int p, int ep) { + while (true) { + int res = match(ms, s, ep + 1); + if (res >= 0) { + return res; + } + if (singleMatch(ms, s, p, ep)) { + s++; + } else { + return -1; + } + } + } + + private static int startCapture(MatchState ms, int s, int p, int what) { + int level = ms.level; + if (level >= MAXCAPTURES) { + throw ms.L.libError("too many captures"); + } + ms.capInit[level] = s; + ms.capLen[level] = what; + ms.level = level + 1; + int res = match(ms, s, p); + if (res < 0) { + ms.level--; + } + return res; + } + + private static int endCapture(MatchState ms, int s, int p) { + int l = captureToClose(ms); + ms.capLen[l] = s - ms.capInit[l]; + int res = match(ms, s, p); + if (res < 0) { + ms.capLen[l] = CAP_UNFINISHED; + } + return res; + } + + private static int captureToClose(MatchState ms) { + for (int level = ms.level - 1; level >= 0; level--) { + if (ms.capLen[level] == CAP_UNFINISHED) { + return level; + } + } + throw ms.L.libError("invalid pattern capture"); + } + + /** {@code check_capture}:把 {@code '1'..'9'} 换成下标,顺手校验 */ + private static int checkCapture(MatchState ms, int l) { + l -= '1'; + if (l < 0 || l >= ms.level || ms.capLen[l] == CAP_UNFINISHED) { + throw ms.L.libError("invalid capture index %" + (l + 1)); + } + return l; + } + + /** {@code match_capture}({@code %1} 这类回溯引用) */ + private static int matchCapture(MatchState ms, int s, int l) { + l = checkCapture(ms, l); + int len = ms.capLen[l]; + if (len < 0) { + // 位置捕获(CAP_POSITION):C 里转成 size_t 是个天文数字,必然不匹配 + return -1; + } + if (ms.srcEnd - s >= len && regionEquals(ms.src, ms.capInit[l], s, len)) { + return s + len; + } + return -1; + } + + private static boolean regionEquals(byte[] a, int aOff, int bOff, int len) { + if (len <= 0) { + return true; + } + for (int i = 0; i < len; i++) { + if (a[aOff + i] != a[bOff + i]) { + return false; + } + } + return true; + } + + /** + * {@code match}:匹配主循环,返回匹配结束的位置,{@code -1} 表示失败。 + *

+ * C 版用 {@code goto init} 做尾递归优化;这里换成 + * 「{@code continue} 重新跑一遍循环体」,语义等价——尤其 + * {@code matchdepth} 的减与加仍然各发生一次。 + */ + private static int match(MatchState ms, int s, int p) { + if (ms.matchdepth-- == 0) { + throw ms.L.libError("pattern too complex"); + } + while (true) { + boolean dflt = false; + if (p != ms.pEnd) { + int c = ms.pAt(p); + switch (c) { + case '(': + if (ms.pAt(p + 1) == ')') { + s = startCapture(ms, s, p + 2, CAP_POSITION); + } else { + s = startCapture(ms, s, p + 1, CAP_UNFINISHED); + } + break; + case ')': + s = endCapture(ms, s, p + 1); + break; + case '$': + if (p + 1 != ms.pEnd) { + dflt = true; // 不是最后一个字符,就当普通字符处理 + } else { + s = (s == ms.srcEnd) ? s : -1; + } + break; + case L_ESC: { + int c1 = ms.pAt(p + 1); + if (c1 == 'b') { + s = matchBalance(ms, s, p + 2); + if (s >= 0) { + p += 4; + continue; + } + } else if (c1 == 'f') { + p += 2; + if (ms.pAt(p) != '[') { + throw ms.L.libError("missing '[' after '%f' in pattern"); + } + int ep = classEnd(ms, p); + int previous = (s == 0) ? 0 : ms.sAt(s - 1); + if (!matchBracketClass(ms, previous, p, ep - 1) + && matchBracketClass(ms, ms.sAt(s), p, ep - 1)) { + p = ep; + continue; + } + s = -1; + } else if (c1 >= '0' && c1 <= '9') { + s = matchCapture(ms, s, c1); + if (s >= 0) { + p += 2; + continue; + } + } else { + dflt = true; + } + break; + } + default: + dflt = true; + break; + } + if (dflt) { + int ep = classEnd(ms, p); + if (!singleMatch(ms, s, p, ep)) { + int suf = ms.pAt(ep); + if (suf == '*' || suf == '?' || suf == '-') { + p = ep + 1; + continue; // 接受空匹配 + } + s = -1; // '+' 或无后缀:至少要匹配一次 + } else { + int suf = ms.pAt(ep); + if (suf == '?') { + int res = match(ms, s + 1, ep + 1); + if (res >= 0) { + s = res; + } else { + p = ep + 1; + continue; + } + } else if (suf == '+' || suf == '*') { + if (suf == '+') { + s++; + } + s = maxExpand(ms, s, p, ep); + } else if (suf == '-') { + s = minExpand(ms, s, p, ep); + } else { + s++; + p = ep; + continue; + } + } + } + } + break; + } + ms.matchdepth++; + return s; + } + + /** {@code lmemfind}:朴素子串查找 */ + private static int lmemfind(byte[] s, int from, int len, byte[] p) { + if (p.length == 0) { + return from; + } + if (p.length > len) { + return -1; + } + for (int i = from; i + p.length <= from + len; i++) { + int j = 0; + while (j < p.length && s[i + j] == p[j]) { + j++; + } + if (j == p.length) { + return i; + } + } + return -1; + } + + /** {@code nospecials}:模式里有没有特殊字符 */ + private static boolean noSpecials(LuaString p) { + for (int i = 0; i < p.length(); i++) { + if (SPECIALS.indexOf(p.byteAt(i)) >= 0) { + return false; + } + } + return true; + } + + /** {@code get_onecapture}:{@code capOut[0]} 收下标;返回长度,或 {@code CAP_POSITION} */ + private static int getOneCapture(MatchState ms, int i, int s, int e, int[] capOut) { + if (i >= ms.level) { + if (i != 0) { + throw ms.L.libError("invalid capture index %" + (i + 1)); + } + capOut[0] = s; + return e - s; + } + int capl = ms.capLen[i]; + capOut[0] = ms.capInit[i]; + if (capl == CAP_UNFINISHED) { + throw ms.L.libError("unfinished capture"); + } + if (capl == CAP_POSITION) { + ms.L.push(LuaValue.ofInteger(ms.capInit[i] + 1)); + return CAP_POSITION; + } + return capl; + } + + private static void pushOneCapture(MatchState ms, int i, int s, int e) { + int[] cap = new int[1]; + int l = getOneCapture(ms, i, s, e, cap); + if (l != CAP_POSITION) { + LuaString out = (l <= 0 || cap[0] < 0) ? LuaString.EMPTY + : ms.L.newString(ms.src, cap[0], l); + ms.L.push(LuaValue.ofString(out)); + } + } + + private static int pushCaptures(MatchState ms, int s, int e) { + int nlevels = (ms.level == 0 && s >= 0) ? 1 : ms.level; + LuaAux.checkStack(ms.L, nlevels, "too many captures"); + for (int i = 0; i < nlevels; i++) { + pushOneCapture(ms, i, s, e); + } + return nlevels; + } + + /** {@code str_find_aux}:{@code find} 与 {@code match} 共用 */ + private static int findAux(LuaState L, boolean find) { + LuaString s = LuaAux.checkString(L, 1); + LuaString pat = LuaAux.checkString(L, 2); + int ls = s.length(); + long init = posrelatI(LuaAux.optInteger(L, 3, 1), ls) - 1; + if (init > ls) { + LuaAux.pushFail(L); + return 1; + } + if (find && ((!L.isNoneArg(4) && L.arg1(4).toBoolean()) || noSpecials(pat))) { + int s2 = lmemfind(s.bytes, (int) init, ls - (int) init, pat.bytes); + if (s2 >= 0) { + L.push(LuaValue.ofInteger(s2 + 1)); + L.push(LuaValue.ofInteger(s2 + pat.length())); + return 2; + } + } else { + MatchState ms = new MatchState(); + int s1 = (int) init; + int pStart = 0; + int lp = pat.length(); + boolean anchor = (pat.length() > 0 && pat.byteAt(0) == '^'); + if (anchor) { + pStart = 1; + lp--; + } + prepState(ms, L, s, pat, pStart, lp); + do { + reprepState(ms); + int res = match(ms, s1, ms.pStart); + if (res >= 0) { + if (find) { + L.push(LuaValue.ofInteger(s1 + 1)); + L.push(LuaValue.ofInteger(res)); + return pushCaptures(ms, -1, 0) + 2; + } + return pushCaptures(ms, s1, res); + } + } while (s1++ < ms.srcEnd && !anchor); + } + LuaAux.pushFail(L); + return 1; + } + + private static int find(LuaState L) { + return findAux(L, true); + } + + private static int matchFn(LuaState L) { + return findAux(L, false); + } + + /* ==================== gmatch ==================== */ + + /** {@code GMatchState}:迭代之间要记住「上次匹配到哪儿」 */ + private static final class GMatchState { + MatchState ms; + int src; + int p; + int lastmatch = -1; + } + + /** + * {@code gmatch}。 + *

+ * C 版把源串、模式串、状态结构三个都塞进 C 闭包的 upvalue + * (注释写得很清楚:为了不让那两个字符串被回收)。这里只要一个就够—— + * Java 的引用本身就是强引用,状态结构拿着那两个 {@code LuaString}, + * 它们就不会消失。 + */ + private static int gmatch(LuaState L) { + LuaString s = LuaAux.checkString(L, 1); + LuaString pat = LuaAux.checkString(L, 2); + long init = posrelatI(LuaAux.optInteger(L, 3, 1), s.length()) - 1; + L.top = L.ci.func + 3; // lua_settop(L, 2) + if (init > s.length()) { + init = s.length() + 1; // 避免 s + init 溢出 + } + GMatchState gm = new GMatchState(); + gm.ms = new MatchState(); + prepState(gm.ms, L, s, pat, 0, pat.length()); + gm.src = (int) init; + gm.p = gm.ms.pStart; + CClosure cl = new CClosure(StringLib::gmatchAux, 1); + cl.upvalue[0].set(LuaValue.ofUserdata(gm)); + L.push(LuaValue.ofCClosure(cl)); + return 1; + } + + private static int gmatchAux(LuaState L) { + GMatchState gm = (GMatchState) LuaUserdata.dataOf(L.upvalue(0)); + gm.ms.L = L; + for (int src = gm.src; src <= gm.ms.srcEnd; src++) { + reprepState(gm.ms); + int e = match(gm.ms, src, gm.p); + if (e >= 0 && e != gm.lastmatch) { + gm.src = gm.lastmatch = e; + return pushCaptures(gm.ms, src, e); + } + } + return 0; + } + + /* ==================== gsub ==================== */ + + private static final int TR_NUMBER = 0; + private static final int TR_STRING = 1; + private static final int TR_FUNCTION = 2; + private static final int TR_TABLE = 3; + + private static int trType(LuaState L) { + if (L.isNoneArg(3)) { + return -1; + } + LuaValue v = L.arg1(3); + if (v.isNumber()) { + return TR_NUMBER; + } + if (v.isString()) { + return TR_STRING; + } + if (v.isFunction()) { + return TR_FUNCTION; + } + if (v.isTable()) { + return TR_TABLE; + } + return -1; + } + + /** {@code add_s}:替换串里的 {@code %0} / {@code %n} / {@code %%} */ + private static void addS(MatchState ms, LuaAux.Buf b, int s, int e) { + LuaString rep = LuaAux.checkString(ms.L, 3); + byte[] news = rep.bytes; + int off = 0; + while (true) { + int idx = -1; + for (int i = off; i < news.length; i++) { + if ((news[i] & 0xFF) == L_ESC) { + idx = i; + break; + } + } + if (idx < 0) { + b.addBytes(news, off, news.length - off); + return; + } + b.addBytes(news, off, idx - off); + int p = idx + 1; + int pc = (p < news.length) ? (news[p] & 0xFF) : 0; + if (pc == L_ESC) { + b.addChar(L_ESC); + } else if (pc == '0') { + b.addBytes(ms.src, s, e - s); + } else if (pc >= '0' && pc <= '9') { + int[] cap = new int[1]; + int resl = getOneCapture(ms, pc - '1', s, e, cap); + if (resl == CAP_POSITION) { + LuaAux.addTop(ms.L, b); // 位置捕获:值已经压在栈顶了 + } else { + b.addBytes(ms.src, cap[0], resl); + } + } else { + throw ms.L.libError("invalid use of '%' in replacement string"); + } + off = p + 1; + } + } + + /** {@code add_value}:返回「原文有没有被改动」 */ + private static boolean addValue(MatchState ms, LuaAux.Buf b, int s, int e, int tr) { + LuaState L = ms.L; + switch (tr) { + case TR_FUNCTION: { + int fIdx = L.top; + L.push(L.arg1(3)); + int n = pushCaptures(ms, s, e); + L.callNoYield(fIdx, 1); /* lstrlib.c: lua_call —— 不可 yield */ + break; + } + case TR_TABLE: { + pushOneCapture(ms, 0, s, e); + LuaValue key = new LuaValue(L.stack[L.top - 1]); + L.pushNil(); + int idx = L.top - 1; + Vm.getTable(L, L.arg1(3), key, idx); + break; + } + default: + addS(ms, b, s, e); + return true; + } + LuaValue res = L.stack[L.top - 1]; + if (!res.toBoolean()) { + L.top--; // nil / false:保留原文 + b.addBytes(ms.src, s, e - s); + return false; + } + if (!res.isString() && !res.isNumber()) { + throw L.libError("invalid replacement value (a " + res.typeName() + ")"); + } + LuaAux.addTop(L, b); + return true; + } + + /** {@code str_gsub} */ + private static int gsub(LuaState L) { + LuaString src = LuaAux.checkString(L, 1); + LuaString pat = LuaAux.checkString(L, 2); + int tr = trType(L); + /* + * 顺序照抄 C:先 {@code luaL_optinteger(L, 4, ...)},再 + * 校验第三个参数的类型。所以 {@code gsub(s, p, nil, "x")} 报的是 + * {@code bad argument #4 (number expected, got string)},而不是 + * 「第三个参数不是 string/function/table」——那个判断在后面。 + * 写反了就会在「repl 不对、同时 max 也不对」时报错的那一条上偏。 + */ + long maxS = LuaAux.optInteger(L, 4, (long) src.length() + 1); + LuaAux.argExpected(L, tr >= 0, 3, "string/function/table"); + MatchState ms = new MatchState(); + LuaAux.Buf b = new LuaAux.Buf(); + int pStart = 0; + int lp = pat.length(); + boolean anchor = (pat.length() > 0 && pat.byteAt(0) == '^'); + if (anchor) { + pStart = 1; + lp--; + } + prepState(ms, L, src, pat, pStart, lp); + long n = 0; + boolean changed = false; + int cur = 0; + int lastmatch = -1; + while (n < maxS) { + reprepState(ms); + int e = match(ms, cur, ms.pStart); + /* + * {@code e != lastmatch} 这一条是 {@code ("abc"):gsub("", "-")} + * 能给出 {@code -a-b-c-} 而不是 {@code ----abc} 的原因:空匹配 + * 在同一个位置会原地重复命中,只靠 {@code max_s} 挡住的话 + * 得到的是「替换次数够了但一个字符都没推进」——语法上跑完了, + * 语义上完全错。 + */ + if (e >= 0 && e != lastmatch) { + n++; + if (addValue(ms, b, cur, e, tr)) { + changed = true; + } + cur = lastmatch = e; + } else if (cur < ms.srcEnd) { + b.addChar(ms.src[cur++]); + } else { + break; + } + if (anchor) { + break; + } + } + if (!changed) { + L.push(LuaValue.ofString(src)); + } else { + b.addBytes(src.bytes, cur, ms.srcEnd - cur); + L.push(LuaValue.ofString(b.result())); + } + L.push(LuaValue.ofInteger(n)); + return 2; + } + /* ==================== string.format ==================== */ + + /** + * {@code str_format}。 + *

+ * C 版把格式项拼成一小段 {@code "%-+#0..."} 再交给系统的 {@code snprintf}; + * 这里不能直接转手给 Java 的 {@code String.format},两家的规则在 + * 好几个地方不一样: + *

    + *
  • {@code %g} 默认会去掉尾部的零(Java 不会);
  • + *
  • {@code %c} 写的是一个字节,Java 会把 200 编成 UTF-8 的两字节;
  • + *
  • {@code %s} 的精度数的是字节不是字符;
  • + *
  • inf / NaN 分别写成 {@code inf} / {@code nan},Java 写 {@code Infinity}。
  • + *
+ * 所以这里自己解析格式项、自己生成数字串,只在最后一步复用 C printf 的 + * 「符号 / 前缀 / 宽度」组装规则。 + */ + private static int format(LuaState L) { + LuaString fmt = LuaAux.checkString(L, 1); + byte[] f = fmt.bytes; + int sfl = f.length; + int arg = 1; // 1 号参数是格式串本身,下一个可用的参数从 2 开始 + LuaAux.Buf out = new LuaAux.Buf(); + int i = 0; + while (i < sfl) { + if (f[i] != L_ESC) { + out.addChar(f[i] & 0xFF); + i++; + continue; + } + i++; + if (i < sfl && f[i] == L_ESC) { + out.addChar(L_ESC); // %% + i++; + continue; + } + arg++; + if (arg > L.getTop()) { + // 注意顺序:参数不够比「格式串本身不合法」先报 + throw L.argError(arg, "no value"); + } + int start = i; + int len = 0; + while (start + len < sfl && "-+#0 123456789.".indexOf(f[start + len] & 0xFF) >= 0) { + len++; + } + len++; // 加上后面的转换字符 + if (len >= MAX_FORMAT - 10) { + throw L.libError("invalid format (too long)"); + } + int take = Math.min(len, sfl - start); + String form = "%" + ascii(f, start, take); + int convIdx = start + len - 1; + int conv = (convIdx >= 0 && convIdx < sfl) ? (f[convIdx] & 0xFF) : 0; + i = start + len; + Spec sp = parseSpec(form); + switch (conv) { + case 'c': { + checkFormat(L, form, form.substring(1), "-", false); + long n = LuaAux.checkInteger(L, arg); + out.addAscii(assemble(sp, "", "", Character.toString((char) (int) n), false)); + break; + } + case 'd': + case 'i': { + // C 的 intcase 是「先 checkinteger,再 checkformat」,顺序别反 + long n = LuaAux.checkInteger(L, arg); + checkFormat(L, form, form.substring(1), "-+0 ", true); + emitInt(out, sp, conv, n, false); + break; + } + case 'u': { + long n = LuaAux.checkInteger(L, arg); + checkFormat(L, form, form.substring(1), "-0", true); + emitInt(out, sp, conv, n, false); + break; + } + case 'o': + case 'x': + case 'X': { + long n = LuaAux.checkInteger(L, arg); + checkFormat(L, form, form.substring(1), "-#0", true); + emitInt(out, sp, conv, n, false); + break; + } + case 'a': + case 'A': { + checkFormat(L, form, form.substring(1), "-+#0 ", true); + double x = LuaAux.checkNumber(L, arg); + emitHexFloat(out, sp, x, conv == 'A'); + break; + } + case 'f': { + double x = LuaAux.checkNumber(L, arg); + checkFormat(L, form, form.substring(1), "-+#0 ", true); + int prec = sp.hasPrec ? sp.prec : 6; + String d = fixedDigits(Math.abs(x), prec); + emitFloat(out, sp, isNeg(x), keepPoint(d, sp.hash), conv == 'F'); + break; + } + case 'e': + case 'E': { + double x = LuaAux.checkNumber(L, arg); + checkFormat(L, form, form.substring(1), "-+#0 ", true); + int prec = sp.hasPrec ? sp.prec : 6; + int[] exp = new int[1]; + String m = expDigits(Math.abs(x), prec, exp); + if (!isSpecial(m)) { + m = keepPoint(m, sp.hash) + "e" + signedExp(exp[0]); + } + emitFloat(out, sp, isNeg(x), m, conv == 'E'); + break; + } + case 'g': + case 'G': { + double x = LuaAux.checkNumber(L, arg); + checkFormat(L, form, form.substring(1), "-+#0 ", true); + int prec = sp.hasPrec ? sp.prec : 6; + emitFloat(out, sp, isNeg(x), generalDigits(Math.abs(x), prec, sp.hash), conv == 'G'); + break; + } + case 'p': { + checkFormat(L, form, form.substring(1), "-", false); + LuaValue v = L.arg1(arg); + /* + * {@code lua_topointer} 只对「可回收对象」有地址,数字、布尔、 + * nil 一律返回 {@code NULL}。C 版拿到 NULL 就把转换符改成 + * {@code %s}、拿 {@code "(null)"} 当字符串格式化——所以 + * {@code string.format("%p", 4)} 是 {@code "(null)"}, + * 不是 {@code 0x0}(上游 {@code strings.lua} 有这一条)。 + */ + if (v.isNil() || v.isBoolean() || v.isNumber()) { + String digits = "(null)"; + if (sp.hasPrec) { + digits = digits.substring(0, Math.min(sp.prec, digits.length())); + } + out.addAscii(assemble(sp, "", "", digits, false)); + break; + } + /* + * 与参考实现必然不同:C 打印的是真实地址,我们只有 Java 的 + * identity hash。硬编一个假地址不如如实给一个稳定的数—— + * 至少同一个值在同一次运行里得到同一个串。 + */ + String hex = "0x" + Long.toHexString(System.identityHashCode(v.o)); + out.addAscii(assemble(sp, "", "", hex, false)); + break; + } + case 'q': { + if (form.length() > 2) { + throw L.libError("specifier '%q' cannot have modifiers"); + } + addLiteral(L, out, arg); + break; + } + case 's': { + LuaString s = LuaAux.toLString(L, L.arg1(arg)); + if (form.length() == 2) { // 没有修饰符 + out.addString(s); + break; + } + for (int k = 0; k < s.length(); k++) { + if (s.byteAt(k) == 0) { + throw L.argError(arg, "string contains zeros"); + } + } + checkFormat(L, form, form.substring(1), "-", true); + if (form.indexOf('.') < 0 && s.length() >= 100) { + out.addString(s); // 太长,不进 buff + break; + } + String digits = ascii(s.bytes, 0, s.length()); + if (sp.hasPrec) { + digits = digits.substring(0, Math.min(sp.prec, digits.length())); + } + out.addAscii(assemble(sp, "", "", digits, false)); + break; + } + default: + throw L.libError("invalid conversion '" + form + "' to 'format'"); + } + } + L.push(LuaValue.ofString(out.result())); + return 1; + } + + private static String ascii(byte[] b, int off, int len) { + char[] c = new char[len]; + for (int k = 0; k < len; k++) { + c[k] = (char) (b[off + k] & 0xFF); + } + return new String(c); + } + + /** + * {@code lauxlib.c: checkformat}:格式项里除了 flags / width / precision, + * 最后必须落到一个字母(转换字符)上。 + */ + private static void checkFormat(LuaState L, String form, String spec, String flags, + boolean precision) { + int p = 0; + while (p < spec.length() && flags.indexOf(spec.charAt(p)) >= 0) { + p++; + } + if (p < spec.length() && spec.charAt(p) != '0') { + p = twoDigits(spec, p); + if (p < spec.length() && spec.charAt(p) == '.' && precision) { + p++; + p = twoDigits(spec, p); + } + } + if (p >= spec.length() || !isAlpha(spec.charAt(p))) { + throw L.libError("invalid conversion specification: '" + form + "'"); + } + } + + /** {@code get2digits}:宽度与精度最多读两位——所以 {@code %.200d} 是不合法的 */ + private static int twoDigits(String s, int p) { + for (int k = 0; k < 2 && p < s.length() && s.charAt(p) >= '0' && s.charAt(p) <= '9'; k++) { + p++; + } + return p; + } + + private static boolean isAlpha(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); + } + + /** 解析出的格式项 */ + private static final class Spec { + boolean left; + boolean plus; + boolean space; + boolean hash; + boolean zero; + int width; + boolean hasPrec; + int prec; + } + + private static Spec parseSpec(String form) { + Spec sp = new Spec(); + int p = 1; // 跳过 '%' + while (p < form.length() && "-+ #0".indexOf(form.charAt(p)) >= 0) { + switch (form.charAt(p)) { + case '-': + sp.left = true; + break; + case '+': + sp.plus = true; + break; + case ' ': + sp.space = true; + break; + case '#': + sp.hash = true; + break; + default: + sp.zero = true; + break; + } + p++; + } + while (p < form.length() && form.charAt(p) >= '0' && form.charAt(p) <= '9') { + sp.width = sp.width * 10 + (form.charAt(p) - '0'); + p++; + } + if (p < form.length() && form.charAt(p) == '.') { + p++; + sp.hasPrec = true; + while (p < form.length() && form.charAt(p) >= '0' && form.charAt(p) <= '9') { + sp.prec = sp.prec * 10 + (form.charAt(p) - '0'); + p++; + } + } + return sp; + } + + /** 符号:{@code '-'} 优先,其次 {@code '+'},再其次空格 */ + private static String signOf(boolean neg, Spec sp) { + if (neg) { + return "-"; + } + if (sp.plus) { + return "+"; + } + if (sp.space) { + return " "; + } + return ""; + } + + /** + * C printf 的「组装」:把符号、前缀、数字串按 flags / width 摆好。 + *

+ * 零填充的位置是唯一容易写错的地方:在符号与 {@code 0x} 前缀之后、数字之前。 + * {@code ("%010a"):format(1.5)} 要给 {@code 0x001.8p+0}——填在 {@code 0x} + * 后面,不是前面。 + */ + private static String assemble(Spec sp, String sign, String prefix, String digits, + boolean upper) { + String body = prefix + digits; + if (upper) { + body = body.toUpperCase(); + } + String lead = sign + body; + if (sp.width <= lead.length()) { + return lead; + } + int pad = sp.width - lead.length(); + StringBuilder sb = new StringBuilder(); + if (sp.left) { + sb.append(lead); + repeat(sb, ' ', pad); + } else if (sp.zero && !isSpecial(digits)) { + /* + * inf / nan 不参与零填充:C printf 把它们当「没有数字可言」, + * {@code 0} 标志被忽略,宽度改用空格补。 + * {@code ("%020.5a"):format(math.huge)} 因此是 17 个空格加 + * {@code inf}(共 20 列),而 {@code ("%020d"):format(42)} 照旧是 + * {@code 00000000000000000042}——整数动词不受影响。 + */ + sb.append(sign).append(prefix); + repeat(sb, '0', pad); + sb.append(upper ? digits.toUpperCase() : digits); + } else { + repeat(sb, ' ', pad); + sb.append(lead); + } + return sb.toString(); + } + + /** 去掉 {@code 0} 标志的副本(整数动词给了精度时用) */ + private static Spec copyWithoutZero(Spec s) { + Spec c = new Spec(); + c.left = s.left; + c.plus = s.plus; + c.space = s.space; + c.hash = s.hash; + c.zero = false; + c.width = s.width; + c.hasPrec = s.hasPrec; + c.prec = s.prec; + return c; + } + + private static void repeat(StringBuilder sb, char c, int n) { + for (int k = 0; k < n; k++) { + sb.append(c); + } + } + + /** {@code %.0d} 对 0 得到空串——C 的行为,别「顺手」补一个 0 */ + private static String applyPrecision(String digits, boolean hasPrec, int prec) { + if (!hasPrec) { + return digits; + } + if (prec == 0 && digits.equals("0")) { + return ""; + } + if (digits.length() >= prec) { + return digits; + } + StringBuilder sb = new StringBuilder(); + repeat(sb, '0', prec - digits.length()); + sb.append(digits); + return sb.toString(); + } + + private static void emitInt(LuaAux.Buf out, Spec sp, int conv, long n, boolean unused) { + String digits; + String prefix = ""; + boolean neg = false; + switch (conv) { + case 'u': + digits = Long.toUnsignedString(n); + break; + case 'o': + digits = Long.toUnsignedString(n, 8); + break; + case 'x': + case 'X': + digits = Long.toHexString(n); + if (sp.hash && n != 0) { + prefix = (conv == 'X') ? "0X" : "0x"; + } + break; + default: + neg = n < 0; + digits = (n == Long.MIN_VALUE) ? "9223372036854775808" : Long.toString(Math.abs(n)); + break; + } + digits = applyPrecision(digits, sp.hasPrec, sp.prec); + /* + * C 的规则:整数转换一旦指定了精度,{@code 0} 标志就被忽略 + * (C99 7.19.6.1:「For d, i, o, u, x, and X conversions, if a + * precision is specified, the 0 flag is ignored」)。 + * {@code ("%05.3d"):format(42)} 因此是 {@code " 042"}——精度先把值补成 + * {@code "042"},宽度 5 再用空格补,不是 {@code "00042"}。 + * 实测矩阵里这一类错了 220 行,全是「{@code 0} 标志 + 精度 + 整数动词」。 + * 浮点动词没有这条规则({@code %08.3f} 照旧补零),所以只在这里改。 + */ + if (sp.hasPrec) { + sp = copyWithoutZero(sp); + } + if (conv == 'o' && sp.hash && !digits.startsWith("0")) { + /* + * {@code #} 对 {@code %o} 的作用是「逼出前导 0」。必须在补完精度 + * 之后再补:{@code ("%#.0o"):format(0)} 的精度把 {@code "0"} + * 削成了空串,而 C 的规则是「值与精度都是 0 时要打印一个 0」—— + * 顺序反了就会给出空串。 + */ + digits = "0" + digits; + } + out.addAscii(assemble(sp, signOf(neg, sp), prefix, digits, conv == 'X')); + } + + /* ---------- 浮点 ---------- */ + + /** 负号取自符号位,所以 {@code -0.0} 也算负——{@code %f} 要给 {@code -0.000000} */ + private static boolean isNeg(double x) { + return !Double.isNaN(x) && Double.doubleToRawLongBits(x) < 0; + } + + private static void emitFloat(LuaAux.Buf out, Spec sp, boolean neg, String digits, + boolean upper) { + out.addAscii(assemble(sp, signOf(neg, sp), "", digits, upper)); + } + + /** + * {@code %f}:定点表示。 + *

+ * 用 {@link BigDecimal} 而不是自己写十进制展开,是因为 C printf 的舍入是 + * round-half-to-even,而 {@code RoundingMode.HALF_EVEN} 正好就是它。 + * 顺带也白拿了任意精度:{@code ("%.99f"):format(-maxfloat)} 有 410 个字符。 + */ + private static String fixedDigits(double ax, int prec) { + if (Double.isInfinite(ax)) { + return "inf"; + } + if (Double.isNaN(ax)) { + return "nan"; + } + return new BigDecimal(ax).setScale(prec, RoundingMode.HALF_EVEN).toPlainString(); + } + + /** {@code %e} 的尾数部分,指数从 {@code expOut} 带出来 */ + private static String expDigits(double ax, int prec, int[] expOut) { + if (Double.isInfinite(ax)) { + expOut[0] = 0; + return "inf"; + } + if (Double.isNaN(ax)) { + expOut[0] = 0; + return "nan"; + } + BigDecimal r = new BigDecimal(ax).round(new MathContext(prec + 1, RoundingMode.HALF_EVEN)); + int exp = r.precision() - r.scale() - 1; + expOut[0] = exp; + return r.movePointLeft(exp).setScale(prec, RoundingMode.HALF_EVEN).toPlainString(); + } + + /** + * {@code #} 对浮点转换的作用是「一定保留小数点」。 + *

+ * {@code ("%#.0f"):format(1)} 是 {@code 1.} 而不是 {@code 1}——小数点留在 + * 末尾,且必须在拼指数之前加,{@code %#.0e} 给的是 {@code 0.e+00}。 + */ + private static String keepPoint(String digits, boolean hash) { + if (!hash || digits.indexOf('.') >= 0) { + return digits; + } + return digits + "."; + } + + /** inf / nan 的“数字串”后面不再拼指数 */ + private static boolean isSpecial(String digits) { + return digits.equals("inf") || digits.equals("nan"); + } + + /** {@code %a} 的指数:与 {@code %e} 不同,不补足两位 */ + private static String signedHexExp(int exp) { + return (exp < 0 ? "-" : "+") + Math.abs(exp); + } + + private static String signedExp(int exp) { + StringBuilder sb = new StringBuilder(); + sb.append(exp < 0 ? '-' : '+'); + int a = Math.abs(exp); + if (a < 10) { + sb.append('0'); // 指数至少两位 + } + sb.append(a); + return sb.toString(); + } + + /** + * {@code %g}:先按 P 位有效数字定出十进制指数 X,再决定走 {@code f} 还是 {@code e}。 + *

+ * 规则照抄 C:{@code P > X >= -4} 走 {@code f}(精度 {@code P-(X+1)}), + * 否则走 {@code e}(精度 {@code P-1});最后去掉尾部的零, + * 除非有 {@code #}。{@code ("%g"):format(1000000)} 得到 {@code 1e+06} + * 就是这条规则,不是什么「看起来像整数就换记数法」。 + *

+ * 包内可见,因为 {@code luaO_tostring} 的 {@code %.14g} 与这里的 {@code %g} + * 是同一个 C 转换的两处调用,必须共用一份实现 —— 各写一份正是它们分家的 + * 原因({@link Vm#formatG} 曾经用 {@code String.format} 自己写了一遍)。 + * 传进来的 {@code ax} 是绝对值:正负号由调用方处理,{@code -0.0} 也靠它保住。 + */ + static String generalDigits(double ax, int p, boolean hash) { + int bigP = (p == 0) ? 1 : p; + if (Double.isInfinite(ax) || Double.isNaN(ax)) { + return fixedDigits(ax, 0); + } + BigDecimal r = new BigDecimal(ax).round(new MathContext(bigP, RoundingMode.HALF_EVEN)); + int x = (ax == 0) ? 0 : r.precision() - r.scale() - 1; + if (bigP > x && x >= -4) { + String s = fixedDigits(ax, bigP - (x + 1)); + return hash ? keepPoint(s, true) : stripZeros(s); + } + int[] exp = new int[1]; + String m = expDigits(ax, bigP - 1, exp); + // 先剥尾零再拼指数:指数本身以 "e-05" 结尾,直接对整个串剥零是剥不到的 + if (!isSpecial(m) && !hash) { + m = stripZeros(m); + } + return isSpecial(m) ? m : keepPoint(m, hash) + "e" + signedExp(exp[0]); + } + + /** 去掉小数部分尾部的零;都去掉了就连小数点一起去掉 */ + private static String stripZeros(String s) { + int dot = s.indexOf('.'); + if (dot < 0) { + return s; + } + int end = s.length(); + while (end > dot + 1 && s.charAt(end - 1) == '0') { + end--; + } + if (end == dot + 1) { + end = dot; + } + return s.substring(0, end); + } + + /** + * {@code %a}:十六进制浮点。 + *

+ * 形状是 {@code 0xh.p±dd},指数不补两位({@code p+0} 不是 {@code p+00})。 + */ + private static void emitHexFloat(LuaAux.Buf out, Spec sp, double x, boolean upper) { + boolean neg = isNeg(x); + double ax = Math.abs(x); + boolean special = Double.isInfinite(ax) || Double.isNaN(ax); + out.addAscii(assemble(sp, signOf(neg, sp), special ? "" : "0x", + hexFloatDigits(ax, sp), upper)); + } + + /** + * {@code %a} 的「尾数 + {@code p±dd}」部分,不含 {@code 0x} 前缀与符号。 + *

+ * {@code %q} 与 {@code %a} 在 C 里是同一个转换(约定 #16): + * {@code quotefloat} 用的就是 {@code snprintf("%" LA_FRMLEN "a")}。 + * 之前 {@code %q} 另写了一份直接吃 {@code Double.toHexString} 的实现, + * 于是和 {@code %a} 那条路径在次正规数上给出两种答案 + * ({@code ("%q"):format(5e-324)} 与 {@code ("%a"):format(5e-324)} 不一致)。 + * 合并到这一处之后两边同源。 + */ + private static String hexFloatDigits(double ax, Spec sp) { + if (Double.isInfinite(ax) || Double.isNaN(ax)) { + return Double.isNaN(ax) ? "nan" : "inf"; + } + if (ax == 0) { + String frac = sp.hasPrec ? zeros(sp.prec) : ""; + return "0" + dotFrac(frac, sp) + "p" + signedHexExp(0); + } + /* + * 小数点左边那一位直接取自 {@code Double.toHexString}:正规数是 + * {@code 1},次正规数是 {@code 0}。 + *

+ * 这里刻意不把次正规规格化。C99 只规定「小数点左边恰好一位」, + * 并且「这个数字只在数次正规时为 0,其它情况取值由实现定义」—— + * 次正规怎么写属于 unspecified,两个 libc 正好相反: + * glibc(CI 的 ubuntu)忠实于机器编码,{@code 5e-324} 给 + * {@code 0x0.0000000000001p-1022};Apple libc / musl 先规格化,给 + * {@code 0x1p-1074}。取 glibc:CI 的一致性闸门才是主闸门(约定 #52)。 + *

+ * 之前这里有一段「剥掉前导 0 再减指数」的规格化逻辑,于是次正规被 + * 规格化成了 Apple 那种写法,而 {@code %q} 走的另一份实现保留机器 + * 编码——同一个 {@code %a} 转换在两处给出两种答案。删掉它之后两边同源, + * 且 {@link StringLibTest#subnormalQuoteFollowsGlibc} 钉的就是这份。 + */ + String h = Double.toHexString(ax); // 形如 "0x1.8p0" / "0x0.0000000000001p-1022" + int pIdx = h.indexOf('p'); + String mant = h.substring(2, pIdx); + int exp = Integer.parseInt(h.substring(pIdx + 1)); + int dot = mant.indexOf('.'); + String intPart = (dot < 0) ? mant : mant.substring(0, dot); + String frac = (dot < 0) ? "" : mant.substring(dot + 1); + if (sp.hasPrec) { + frac = roundFrac(intPart, frac, sp.prec); + intPart = frac.substring(0, frac.length() - sp.prec); + frac = frac.substring(frac.length() - sp.prec); + } else { + frac = stripZerosFrac(frac); + } + return intPart + dotFrac(frac, sp) + "p" + signedHexExp(exp); + } + + /** + * 小数点什么时候留。 + *

+ * C99:{@code #} 标志下 {@code a}/{@code A} 的结果必须带小数点,哪怕 + * 后面一位数字都没有。实测(参考实现):{@code ("%#a"):format(0.0)} 是 + * {@code 0x0.p+0}、{@code ("%#a"):format(1.0)} 是 {@code 0x1.p+0}、 + * {@code ("%#.0a"):format(1.0)} 是 {@code 0x1.p+0},而去掉 {@code #} 之后 + * 三种都是 {@code 0x0p+0} / {@code 0x1p+0} / {@code 0x1p+0}。 + * 曾经只有非零分支照这条写,零值分支漏了 —— 于是 + * {@code ("%#a"):format(0.0)} 给 {@code 0x0p+0}。 + */ + private static String dotFrac(String frac, Spec sp) { + return (frac.isEmpty() && !sp.hash) ? "" : "." + frac; + } + + private static String zeros(int n) { + StringBuilder sb = new StringBuilder(); + repeat(sb, '0', n); + return sb.toString(); + } + + private static String stripZerosFrac(String frac) { + int end = frac.length(); + while (end > 0 && frac.charAt(end - 1) == '0') { + end--; + } + return frac.substring(0, end); + } + + /** + * 把 {@code intPart.frac} 舍到 {@code prec} 位十六进制小数。返回合并后的串。 + *

+ * 判据逐字照抄 glibc 的 {@code stdio-common/printf_fphex.c}: + *

+     * bool more_bits = ((next_digit_value & 7) != 0
+     *                   || precision + 1 < numend - numstr);
+     * do_round_away = round_away (negative, last_digit_value & 1,
+     *                             next_digit_value >= 8, more_bits, mode);
+     * 
+ * 而 {@code round_away} 在 {@code FE_TONEAREST} 下就是 + * {@code half_bit && (last_digit_odd || more_bits)}——即 + * 就近舍入、平局取偶。 + *

+ * 平局(舍弃的第一位恰好是 {@code 8}、后面全是 0)时看保留的最后一位 + * 的奇偶:{@code ("%.0a"):format(1.5)} 保留的最后一位是整数位 {@code 1} + * (奇),进位成 {@code 0x2p+0};{@code ("%.1a"):format(0x1.08p0)} 保留的最 + * 后一位是 {@code 0}(偶),不进位,给 {@code 0x1.0p+0}。 + *

+ * Apple libc 在这里是错的,错得可以量化:{@code ("%.4a")} 作用在 + * {@code 0x1.500082p+0} 上给 {@code 0x1.5000p+0}——舍弃的 {@code 0x82} + * 明明大于半 {@code 0x80} 却舍了下去;而 {@code 0x1.500081p+0} 反倒进上来。 + * 它的判据成了「第二位舍弃数字的奇偶」。这两条在 glibc 上都该进位。 + * 所以带精度的 {@code %a} 里「舍弃的第一位恰为 8」的那一批是平台相关的, + * 不进差分表(同一族问题见 {@code subnormalQuoteFollowsGlibc})。 + */ + private static String roundFrac(String intPart, String frac, int prec) { + if (frac.length() <= prec) { + StringBuilder sb = new StringBuilder(intPart); + sb.append(frac); + repeat(sb, '0', prec - frac.length()); + return sb.toString(); + } + int next = Character.digit(frac.charAt(prec), 16); + boolean moreBits = (next & 7) != 0; + if (!moreBits) { + for (int i = prec + 1; i < frac.length(); i++) { + if (frac.charAt(i) != '0') { + moreBits = true; + break; + } + } + } + /* prec == 0 时「保留的最后一位」是小数点左边那一位 */ + int last = Character.digit((prec > 0) ? frac.charAt(prec - 1) + : intPart.charAt(0), 16); + boolean up = (next >= 8) && (moreBits || (last % 2 == 1)); + StringBuilder sb = new StringBuilder(intPart); + sb.append(frac, 0, prec); + if (up) { + for (int i = sb.length() - 1; i >= 0; i--) { + int d = Character.digit(sb.charAt(i), 16); + if (d < 15) { + sb.setCharAt(i, "0123456789abcdef".charAt(d + 1)); + break; + } + sb.setCharAt(i, '0'); + } + } + return sb.toString(); + } + + /* ---------- %q ---------- */ + + /** {@code addliteral} */ + private static void addLiteral(LuaState L, LuaAux.Buf b, int arg) { + LuaValue v = L.arg1(arg); + if (v.isString()) { + addQuoted(b, v.asString()); + } else if (v.isNumber()) { + if (v.isInteger()) { + long n = v.i; + // MIN_INTEGER 走十六进制:写成十进制再取负会溢出 + b.addAscii((n == Long.MIN_VALUE) ? ("0x" + Long.toHexString(n)) + : Long.toString(n)); + } else { + b.addAscii(quoteFloat(v.n)); + } + } else if (v.isNil() || v.isBoolean()) { + b.addString(LuaAux.toLString(L, v)); + } else { + throw L.argError(arg, "value has no literal form"); + } + } + + /** {@code quotefloat}:十六进制保精度,三个非有限值各有一套固定写法 */ + private static String quoteFloat(double n) { + if (n == Double.POSITIVE_INFINITY) { + return "1e9999"; + } + if (n == Double.NEGATIVE_INFINITY) { + return "-1e9999"; + } + if (Double.isNaN(n)) { + return "(0/0)"; + } + StringBuilder sb = new StringBuilder(); + if (isNeg(n)) { + sb.append('-'); + } + // 与 %a 同源(约定 #16):C 的 quotefloat 就是 snprintf("%a"), + // 无标志、无宽度、无精度。 + sb.append("0x").append(hexFloatDigits(Math.abs(n), new Spec())); + return sb.toString(); + } + + /** + * {@code addquoted}。 + *

+ * 控制字符写成 {@code \d} 或 {@code \ddd}——后面那一位是数字时才补成三位, + * 否则 {@code \1} 后面紧跟 {@code '2'} 会被读成 {@code \12}。这条不是风格问题, + * 是 {@code %q} 要能被 Lua 原样读回的前提。 + */ + private static void addQuoted(LuaAux.Buf b, LuaString s) { + b.addChar('"'); + for (int i = 0; i < s.length(); i++) { + int c = s.byteAt(i); + if (c == '"' || c == '\\' || c == '\n') { + b.addChar('\\'); + b.addChar(c); + } else if (c < 32 || c == 127) { + int next = (i + 1 < s.length()) ? s.byteAt(i + 1) : 0; + boolean nextDigit = next >= '0' && next <= '9'; + b.addChar('\\'); + b.addAscii(nextDigit ? String.format("%03d", c) : Integer.toString(c)); + } else { + b.addChar(c); + } + } + b.addChar('"'); + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/StringPack.java b/src/main/java/com/github/esrrhs/fakescript/lua/StringPack.java new file mode 100644 index 0000000..86a1554 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/StringPack.java @@ -0,0 +1,514 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * {@code string.pack} / {@code string.unpack} / {@code string.packsize} —— + * {@code lstrlib.c} 里「pack/unpack stuff」那一段。 + *

+ * 单独成一个文件只是因为 StringLib 已经太长了;它属于 {@code lstrlib.c}, + * 不是新的一层。 + * + *

这是唯一一处「结果取决于宿主平台」的标准库

+ * 格式里的 {@code l} / {@code L} 是 C 的 {@code long},{@code T} 是 + * {@code size_t},{@code !} 的默认最大对齐是 {@code offsetof(struct{char; union + * {lua_Number; double; void*; lua_Integer; long;};}, u)},而默认字节序是 + * 宿主的字节序。这些在 C 里由编译器和 CPU 决定。 + *

+ * 这里按参考二进制所在的平台(64 位 macOS / Linux,LP64 + 小端)钉死: + * {@code long}=8、{@code size_t}=8、最大对齐=8、小端。只有这一份选择, + * 差分比对才有意义——换到 {@code long} 是 4 字节的平台(Windows)上, + * 参考实现自己也会给出不同的字节。 + * + *

一个必须照抄的怪异之处

+ * {@code getdetails} 处理 {@code X}(按后一项对齐)时会真的把后一项吃掉: + * 它调 {@code getoption} 是为了拿到后一项的对齐要求,而那次调用同时把格式串 + * 往前推了一格。于是 {@code string.packsize("Xi4")} 是 0 而不是 4, + * {@code string.pack("Xi4", 7)} 得到空串。「先看一下再决定」的实现会给出 4, + * 而且看起来更合理——这正是必须先量再写的原因。 + */ +final class StringPack { + + private StringPack() { + } + + /* ---------- 平台尺寸(见类注释) ---------- */ + + private static final int SZ_CHAR = 1; + private static final int SZ_SHORT = 2; + private static final int SZ_INT = 4; // C 的 int + private static final int SZ_LONG = 8; // C 的 long(LP64) + private static final int SZ_SIZE_T = 8; + private static final int SZ_FLOAT = 4; + private static final int SZ_DOUBLE = 8; + + /** {@code SZINT = sizeof(lua_Integer)} */ + private static final int SZINT = 8; + + /** {@code MAXINTSIZE} */ + private static final int MAXINTSIZE = 16; + + /** {@code NB = CHAR_BIT} */ + private static final int NB = 8; + + /** {@code MC = (1 << NB) - 1} */ + private static final int MC = 0xFF; + + /** {@code MAXSIZE}:{@code size_t} 不比 {@code int} 小时就是 {@code INT_MAX} */ + private static final int MAXSIZE = Integer.MAX_VALUE; + + /** {@code LUAL_PACKPADBYTE} */ + private static final byte PAD = 0; + + private static final boolean NATIVE_LITTLE = true; + private static final int NATIVE_MAXALIGN = 8; + + /* ---------- KOption ---------- */ + + private static final int KINT = 0; + private static final int KUINT = 1; + private static final int KFLOAT = 2; + private static final int KNUMBER = 3; + private static final int KDOUBLE = 4; + private static final int KCHAR = 5; + private static final int KSTRING = 6; + private static final int KZSTR = 7; + private static final int KPADDING = 8; + private static final int KPADDALIGN = 9; + private static final int KNOP = 10; + + /** {@code Header} */ + private static final class Header { + final LuaState L; + boolean islittle; + int maxalign; + + Header(LuaState L) { + this.L = L; + } + } + + /* ---------- 格式串 ---------- */ + + private static boolean digit(int c) { + return '0' <= c && c <= '9'; + } + + /** {@code getnum}:读一串十进制数字,没有就用默认值 */ + private static int getnum(LuaState L, byte[] fmt, int[] p, int df) { + if (p[0] >= fmt.length || !digit(fmt[p[0]] & 0xFF)) { + return df; + } + int a = 0; + do { + a = a * 10 + ((fmt[p[0]++] & 0xFF) - '0'); + } while (p[0] < fmt.length && digit(fmt[p[0]] & 0xFF) && a <= (MAXSIZE - 9) / 10); + return a; + } + + /** {@code getnumlimit}:读完还要卡在 {@code [1, MAXINTSIZE]} 里 */ + private static int getnumlimit(Header h, byte[] fmt, int[] p, int df) { + int sz = getnum(h.L, fmt, p, df); + if (sz > MAXINTSIZE || sz <= 0) { + throw h.L.libError("integral size (" + sz + ") out of limits [1," + MAXINTSIZE + "]"); + } + return sz; + } + + /** + * {@code getoption}:读并分类下一项,顺便把尺寸写进 {@code sizeOut}。 + *

+ * 配置类选项({@code < > = !} 与空格)不消耗参数,返回 {@link #KNOP}。 + */ + private static int getoption(Header h, byte[] fmt, int[] p, int[] sizeOut) { + int opt = (p[0] < fmt.length) ? (fmt[p[0]++] & 0xFF) : 0; + sizeOut[0] = 0; + switch (opt) { + case 'b': + sizeOut[0] = SZ_CHAR; + return KINT; + case 'B': + sizeOut[0] = SZ_CHAR; + return KUINT; + case 'h': + sizeOut[0] = SZ_SHORT; + return KINT; + case 'H': + sizeOut[0] = SZ_SHORT; + return KUINT; + case 'l': + sizeOut[0] = SZ_LONG; + return KINT; + case 'L': + sizeOut[0] = SZ_LONG; + return KUINT; + case 'j': + sizeOut[0] = SZINT; + return KINT; + case 'J': + sizeOut[0] = SZINT; + return KUINT; + case 'T': + sizeOut[0] = SZ_SIZE_T; + return KUINT; + case 'f': + sizeOut[0] = SZ_FLOAT; + return KFLOAT; + case 'n': + sizeOut[0] = SZ_DOUBLE; + return KNUMBER; + case 'd': + sizeOut[0] = SZ_DOUBLE; + return KDOUBLE; + case 'i': + sizeOut[0] = getnumlimit(h, fmt, p, SZ_INT); + return KINT; + case 'I': + sizeOut[0] = getnumlimit(h, fmt, p, SZ_INT); + return KUINT; + case 's': + sizeOut[0] = getnumlimit(h, fmt, p, SZ_SIZE_T); + return KSTRING; + case 'c': + sizeOut[0] = getnum(h.L, fmt, p, -1); + if (sizeOut[0] == -1) { + throw h.L.libError("missing size for format option 'c'"); + } + return KCHAR; + case 'z': + return KZSTR; + case 'x': + sizeOut[0] = 1; + return KPADDING; + case 'X': + return KPADDALIGN; + case ' ': + break; + case '<': + h.islittle = true; + break; + case '>': + h.islittle = false; + break; + case '=': + h.islittle = NATIVE_LITTLE; + break; + case '!': + h.maxalign = getnumlimit(h, fmt, p, NATIVE_MAXALIGN); + break; + default: + throw h.L.libError("invalid format option '" + (char) opt + "'"); + } + return KNOP; + } + + /** + * {@code getdetails}。 + *

+ * {@code X} 那一段连格式串一起推进(见类注释),不要「优化」成只看不动。 + */ + private static int getdetails(Header h, long totalsize, byte[] fmt, int[] p, + int[] psize, int[] ntoalign) { + int opt = getoption(h, fmt, p, psize); + int[] alignHolder = new int[] { psize[0] }; + if (opt == KPADDALIGN) { + if (p[0] >= fmt.length || getoption(h, fmt, p, alignHolder) == KCHAR + || alignHolder[0] == 0) { + throw h.L.argError(1, "invalid next option for option 'X'"); + } + } + int align = alignHolder[0]; + if (align <= 1 || opt == KCHAR) { + ntoalign[0] = 0; + } else { + if (align > h.maxalign) { + align = h.maxalign; + } + if ((align & (align - 1)) != 0) { // 不是 2 的幂 + throw h.L.argError(1, "format asks for alignment not power of 2"); + } + ntoalign[0] = (int) (align - (totalsize & (align - 1))) & (align - 1); + } + return opt; + } + + /* ---------- 字节读写 ---------- */ + + /** {@code packint}:把 {@code n} 按 {@code size} 字节、指定字节序写进 {@code out} */ + private static void packint(byte[] out, int off, long n, boolean islittle, int size, boolean neg) { + out[off + (islittle ? 0 : size - 1)] = (byte) (n & MC); + for (int i = 1; i < size; i++) { + n >>>= NB; + out[off + (islittle ? i : size - 1 - i)] = (byte) (n & MC); + } + if (neg && size > SZINT) { + // size 比 lua_Integer 还宽时,多出来的高位要补成符号位,不能留 0 + for (int i = SZINT; i < size; i++) { + out[off + (islittle ? i : size - 1 - i)] = (byte) MC; + } + } + } + + /** {@code unpackint}:反向操作,窄的要做符号扩展,超宽的要检查未读字节 */ + private static long unpackint(LuaState L, byte[] str, int off, boolean islittle, + int size, boolean issigned) { + long res = 0; + int limit = (size <= SZINT) ? size : SZINT; + for (int i = limit - 1; i >= 0; i--) { + res <<= NB; + res |= (str[off + (islittle ? i : size - 1 - i)] & 0xFF); + } + if (size < SZINT) { + if (issigned) { + long mask = 1L << (size * NB - 1); + res = ((res ^ mask) - mask); // 符号扩展 + } + } else if (size > SZINT) { + int mask = (!issigned || res >= 0) ? 0 : MC; + for (int i = limit; i < size; i++) { + if ((str[off + (islittle ? i : size - 1 - i)] & 0xFF) != mask) { + throw L.libError(size + "-byte integer does not fit into Lua Integer"); + } + } + } + return res; + } + + /* ---------- string.pack ---------- */ + + static int pack(LuaState L) { + // 格式串在 C 里是 const char*,主循环是 while (*fmt != '\0'): + // \0 之后的内容不是「非法选项」,是根本不存在。 + LuaString fmtStr = LuaAux.checkString(L, 1); + byte[] fmt = fmtStr.toCString().bytes; + Header h = new Header(L); + h.islittle = NATIVE_LITTLE; + h.maxalign = 1; + int[] p = new int[1]; + long totalsize = 0; + int arg = 1; + /* + * C 在这里 {@code lua_pushnil} 是「把参数与字符串缓冲隔开」的标记, + * 但它是可观测的:只传了格式串时,第 2 号参数位置站的就是 + * 这个 nil,于是 {@code string.pack("c2")} 报的是 + * {@code string expected, got nil} 而不是 {@code got no value}。 + * 不压这个 nil,缺参数的报错就会少一个「nil」。 + */ + L.push(LuaValue.ofNil()); + java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); + int[] size = new int[1]; + int[] ntoalign = new int[1]; + while (p[0] < fmt.length) { + int opt = getdetails(h, totalsize, fmt, p, size, ntoalign); + totalsize += ntoalign[0] + size[0]; + for (int i = 0; i < ntoalign[0]; i++) { + out.write(PAD); + } + arg++; + switch (opt) { + case KINT: + case KUINT: { + long n = LuaAux.checkInteger(L, arg); + boolean signed = opt == KINT; + if (size[0] < SZINT) { + if (signed) { + long lim = 1L << (size[0] * NB - 1); + LuaAux.argCheck(L, -lim <= n && n < lim, arg, "integer overflow"); + } else { + LuaAux.argCheck(L, (n >= 0 && n < (1L << (size[0] * NB))) + || (n < 0 && size[0] * NB >= 64), arg, "unsigned overflow"); + } + } + byte[] b = new byte[size[0]]; + packint(b, 0, n, h.islittle, size[0], signed && n < 0); + out.write(b, 0, b.length); + break; + } + case KFLOAT: + case KNUMBER: + case KDOUBLE: { + double d = LuaAux.checkNumber(L, arg); + byte[] b; + if (opt == KFLOAT) { + int bits = Float.floatToRawIntBits((float) d); + b = new byte[SZ_FLOAT]; + putInt(b, 0, bits, h.islittle, SZ_FLOAT); + } else { + long bits = Double.doubleToRawLongBits(d); + b = new byte[SZ_DOUBLE]; + putLong(b, 0, bits, h.islittle, SZ_DOUBLE); + } + out.write(b, 0, b.length); + break; + } + case KCHAR: { + LuaString s = LuaAux.checkString(L, arg); + LuaAux.argCheck(L, s.length() <= size[0], arg, "string longer than given size"); + out.write(s.bytes, 0, s.length()); + for (int i = s.length(); i < size[0]; i++) { + out.write(PAD); + } + break; + } + case KSTRING: { + LuaString s = LuaAux.checkString(L, arg); + long len = s.length(); + // size 装得下 size_t 就不用查;否则按位宽查 + LuaAux.argCheck(L, size[0] >= SZ_SIZE_T || len < (1L << (size[0] * NB)), + arg, "string length does not fit in given size"); + byte[] lb = new byte[size[0]]; + packint(lb, 0, len, h.islittle, size[0], false); + out.write(lb, 0, lb.length); + out.write(s.bytes, 0, s.length()); + totalsize += len; + break; + } + case KZSTR: { + LuaString s = LuaAux.checkString(L, arg); + if (indexOfZero(s.bytes) >= 0) { + LuaAux.argCheck(L, false, arg, "string contains zeros"); + } + out.write(s.bytes, 0, s.length()); + out.write(0); + totalsize += s.length() + 1; + break; + } + case KPADDING: + out.write(PAD); + arg--; + break; + case KPADDALIGN: + case KNOP: + arg--; + break; + default: + break; + } + } + L.push(LuaValue.ofString(L.newString(out.toByteArray()))); + return 1; + } + + private static int indexOfZero(byte[] b) { + for (int i = 0; i < b.length; i++) { + if (b[i] == 0) { + return i; + } + } + return -1; + } + + private static void putInt(byte[] b, int off, int v, boolean islittle, int size) { + for (int j = 0; j < size; j++) { + int shift = 8 * (islittle ? j : size - 1 - j); + b[off + j] = (byte) ((v >>> shift) & 0xFF); + } + } + + private static void putLong(byte[] b, int off, long v, boolean islittle, int size) { + for (int j = 0; j < size; j++) { + int shift = 8 * (islittle ? j : size - 1 - j); + b[off + j] = (byte) ((v >>> shift) & 0xFF); + } + } + + /* ---------- string.packsize ---------- */ + + static int packsize(LuaState L) { + byte[] fmt = LuaAux.checkString(L, 1).toCString().bytes; + Header h = new Header(L); + h.islittle = NATIVE_LITTLE; + h.maxalign = 1; + int[] p = new int[1]; + long totalsize = 0; + int[] size = new int[1]; + int[] ntoalign = new int[1]; + while (p[0] < fmt.length) { + int opt = getdetails(h, totalsize, fmt, p, size, ntoalign); + LuaAux.argCheck(L, opt != KSTRING && opt != KZSTR, 1, "variable-length format"); + long used = (long) size[0] + ntoalign[0]; + LuaAux.argCheck(L, totalsize <= MAXSIZE - used, 1, "format result too large"); + totalsize += used; + } + L.push(LuaValue.ofInteger(totalsize)); + return 1; + } + + /* ---------- string.unpack ---------- */ + + static int unpack(LuaState L) { + byte[] fmt = LuaAux.checkString(L, 1).toCString().bytes; + LuaString dataStr = LuaAux.checkString(L, 2); + byte[] data = dataStr.bytes; + int ld = data.length; + long pos = StringLib.posrelatI(LuaAux.optInteger(L, 3, 1), ld) - 1; + LuaAux.argCheck(L, pos <= ld, 3, "initial position out of string"); + Header h = new Header(L); + h.islittle = NATIVE_LITTLE; + h.maxalign = 1; + int[] p = new int[1]; + int n = 0; + int[] size = new int[1]; + int[] ntoalign = new int[1]; + while (p[0] < fmt.length) { + int opt = getdetails(h, pos, fmt, p, size, ntoalign); + LuaAux.argCheck(L, (long) ntoalign[0] + size[0] <= ld - pos, 2, "data string too short"); + pos += ntoalign[0]; + LuaAux.checkStack(L, 2, "too many results"); + n++; + int at = (int) pos; + switch (opt) { + case KINT: + case KUINT: + L.push(LuaValue.ofInteger( + unpackint(L, data, at, h.islittle, size[0], opt == KINT))); + break; + case KFLOAT: + L.push(LuaValue.ofFloat( + Float.intBitsToFloat((int) getLong(data, at, h.islittle, SZ_FLOAT)))); + break; + case KNUMBER: + case KDOUBLE: + L.push(LuaValue.ofFloat( + Double.longBitsToDouble(getLong(data, at, h.islittle, SZ_DOUBLE)))); + break; + case KCHAR: + L.push(LuaValue.ofString(L.newString(data, at, size[0]))); + break; + case KSTRING: { + long len = unpackint(L, data, at, h.islittle, size[0], false); + LuaAux.argCheck(L, len <= ld - pos - size[0], 2, "data string too short"); + L.push(LuaValue.ofString(L.newString(data, at + size[0], (int) len))); + pos += len; + break; + } + case KZSTR: { + int len = 0; + while (at + len < ld && data[at + len] != 0) { + len++; + } + LuaAux.argCheck(L, pos + len < ld, 2, "unfinished string for format 'z'"); + L.push(LuaValue.ofString(L.newString(data, at, len))); + pos += len + 1; + break; + } + case KPADDING: + case KPADDALIGN: + case KNOP: + n--; + break; + default: + break; + } + pos += size[0]; + } + L.push(LuaValue.ofInteger(pos + 1)); + return n + 1; + } + + private static long getLong(byte[] b, int off, boolean islittle, int size) { + long v = 0; + for (int i = size - 1; i >= 0; i--) { + v = (v << 8) | (b[off + (islittle ? i : size - 1 - i)] & 0xFF); + } + return v; + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/StringPool.java b/src/main/java/com/github/esrrhs/fakescript/lua/StringPool.java new file mode 100644 index 0000000..35a9f4a --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/StringPool.java @@ -0,0 +1,51 @@ +package com.github.esrrhs.fakescript.lua; + +import java.lang.ref.WeakReference; +import java.util.Map; +import java.util.WeakHashMap; + +/** + * 短字符串驻留池 —— 对应 {@code lstring.c} 的 {@code strt}(string table)。 + *

+ * 驻留是 Lua 的性能基石:短字符串全局唯一,相等比较可以退化成引用比较, + * 表查找也能省掉逐字节比对。语义上不可观测,但少了它 {@code t.x = t.x} 这类 + * 代码会慢一个量级。 + *

+ * 为什么要弱引用。C 版里驻留表是由 {@code lgc.c} 显式管理的:GC 周期中 + * 会清掉没人引用的短字符串({@code iscleared} 那套逻辑)。这里没有自己的 GC, + * 如果驻留池用强引用,脚本里出现过的每一个短字符串都会永久驻留——跑数据处理 + * 脚本时这会实打实地泄漏。所以池用 {@link WeakHashMap} + {@link WeakReference}: + * 键弱、值也弱,脚本不再引用某个字符串时它自然被 JVM 回收。 + *

+ * 注意不能用 {@code WeakHashMap}:值就是键本身的话, + * 值对键形成强引用,条目永远不会被清掉,池就退化成泄漏源。 + */ +public final class StringPool { + + private final Map> shortStrings = new WeakHashMap<>(); + + /** + * 返回与 {@code s} 内容相同的规范实例。 + *

+ * 长字符串不驻留(与上游一致:{@code LUAI_MAXSHORTLEN} 以上的字符串每次都新建)。 + */ + public LuaString intern(LuaString s) { + if (!s.isShort()) { + return s; + } + synchronized (this) { + WeakReference ref = shortStrings.get(s); + LuaString canonical = (ref == null) ? null : ref.get(); + if (canonical != null) { + return canonical; + } + shortStrings.put(s, new WeakReference<>(s)); + return s; + } + } + + /** 池中条目数(含尚未被清理的失效条目),仅用于测试与诊断 */ + public synchronized int size() { + return shortStrings.size(); + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/TableLib.java b/src/main/java/com/github/esrrhs/fakescript/lua/TableLib.java new file mode 100644 index 0000000..88d7ab9 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/TableLib.java @@ -0,0 +1,494 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * 表库 —— 对应 {@code ltablib.c}。 + *

+ * 这一块最值得说的不是快排,而是「像表一样的东西」也算表: + * {@code ltablib.c} 开头那组 {@code TAB_R / TAB_W / TAB_L} 检查允许传一个 + * 带 {@code __index} / {@code __newindex} / {@code __len} 的对象进来。少了它, + * 凡是拿 userdata 当数组用的代码在 {@code table.insert} 上就直接报错。 + *

+ * 排序的不稳定性是刻意保留的:参考实现用的是原地快排,相等元素的相对 + * 顺序没有保证。这里照抄同一套划分逻辑(连「order function 不自洽」的报错 + * 都一致),不去做稳定排序——稳定了反而是另一种可观测行为。 + */ +public final class TableLib { + + private TableLib() { + } + + public static void open(LuaState L) { + LuaTable t = new LuaTable(); + LuaAux.setFn(t, "concat", TableLib::concat); + LuaAux.setFn(t, "insert", TableLib::insert); + LuaAux.setFn(t, "move", TableLib::move); + LuaAux.setFn(t, "pack", TableLib::pack); + LuaAux.setFn(t, "remove", TableLib::remove); + LuaAux.setFn(t, "unpack", TableLib::unpack); + LuaAux.setFn(t, "sort", TableLib::sort); + LuaAux.registerLib(L, "table", t); + } + + /* ==================== 「像表」判定 ==================== */ + + private static final int TAB_R = 1; + private static final int TAB_W = 2; + private static final int TAB_L = 4; + + /** + * {@code ltablib.c: checktab}。 + *

+ * 不是表的时候,要求它同时具备本操作需要的那几个元方法,缺一个就 + * 报错——只查 {@code __index} 会让 {@code table.insert} 在一个只有 + * {@code __index} 的对象上走到一半才炸。 + */ + private static void checkTab(LuaState L, int arg, int what) { + LuaValue v = L.arg1(arg); + if (v.isTable()) { + return; + } + LuaTable mt = (v.baseType() == Lua.TTABLE) ? v.asTable().getMetatable() + : L.typeMetatable(v.baseType()); + if (mt != null) { + boolean ok = true; + if ((what & TAB_R) != 0) { + ok = hasField(mt, "__index"); + } + if (ok && (what & TAB_W) != 0) { + ok = hasField(mt, "__newindex"); + } + if (ok && (what & TAB_L) != 0) { + ok = hasField(mt, "__len"); + } + if (ok) { + return; + } + } + LuaAux.checkType(L, arg, Lua.TTABLE); + } + + private static boolean hasField(LuaTable mt, String name) { + LuaValue r = mt.get(LuaString.latin1(name)); + return r != null && !r.isNil(); + } + + /** {@code luaL_len}:走 {@code __len},且结果必须是整数 */ + private static long objLen(LuaState L, LuaValue v) { + LuaValue tm = null; + if (v.isTable()) { + tm = LuaTm.fastTm(v.asTable().getMetatable(), Tm.TM_LEN, L); + if (tm == null) { + return v.asTable().length(); + } + } else if (v.isString()) { + return v.asString().length(); + } else { + tm = LuaTm.getTmByObj(L, v, Tm.TM_LEN); + if (tm == null) { + throw L.typeError(v, "get length of"); + } + } + int f = L.top; + L.ensureSlots(f + 2); + L.stack[f].set(tm); + L.stack[f + 1].set(v); + L.top = f + 2; + L.callTmMightYield(f, 1); + LuaValue r = L.stack[L.top - 1]; + Long i = Vm.toInteger(r, Arith.F2I_EQ); + L.top = f; + if (i == null) { + throw L.libError("object length is not an integer"); + } + return i; + } + + private static long auxGetN(LuaState L, int arg, int what) { + checkTab(L, arg, what | TAB_L); + return objLen(L, L.arg1(arg)); + } + + /* ==================== 各函数 ==================== */ + + /** + * {@code lua_geti} / {@code lua_seti}:会走 {@code __index} / + * {@code __newindex}。 + *

+ * 一开始这里写的是裸读写({@code luaH_setint} 语义),结果 + * {@code table.insert} 落在带 {@code __newindex} 的对象上静默失败: + * 元方法没被调用,值也没进表。C 版用的是 {@code lua_seti},它走 + * {@code luaV_fastset + luaV_finishset},元方法是会触发的。 + *

+ * 取值的实现细节:{@link Vm#getTable} 把结果写进一个栈槽,所以这里借 + * {@code L.top} 当临时槽,用完不推进 {@code top}——回调元方法时 + * {@code L.top} 会被改动,但返回时 {@code callTmRes} 已经把它复位了。 + */ + private static LuaValue getI(LuaState L, LuaValue t, long i) { + int f = L.top; + L.ensureSlots(f + 1); + Vm.getTable(L, t, LuaValue.ofInteger(i), f); + return new LuaValue(L.stack[f]); + } + + private static void setI(LuaState L, LuaValue t, long i, LuaValue v) { + Vm.setTable(L, t, LuaValue.ofInteger(i), v); + } + + /** {@code tinsert} */ + private static int insert(LuaState L) { + long e = auxGetN(L, 1, TAB_R | TAB_W); + e = e + 1; // 第一个空位 + int n = L.getTop(); + long pos; + if (n == 2) { + pos = e; + } else if (n == 3) { + pos = LuaAux.checkInteger(L, 2); + // (pos-1) 当作无符号数比较,等价于 1 <= pos <= e + if (!(pos >= 1 && pos <= e)) { + throw L.argError(2, "position out of bounds"); + } + for (long i = e; i > pos; i--) { + setI(L, L.arg1(1), i, getI(L, L.arg1(1), i - 1)); + } + } else { + throw L.libError("wrong number of arguments to 'insert'"); + } + setI(L, L.arg1(1), pos, new LuaValue(L.arg1(n))); + return 0; + } + + /** {@code tremove} */ + private static int remove(LuaState L) { + long size = auxGetN(L, 1, TAB_R | TAB_W); + long pos = LuaAux.optInteger(L, 2, size); + if (pos != size) { + if (!(pos >= 1 && pos <= size + 1)) { + throw L.argError(2, "position out of bounds"); + } + } + LuaValue res = getI(L, L.arg1(1), pos); + for (; pos < size; pos++) { + setI(L, L.arg1(1), pos, getI(L, L.arg1(1), pos + 1)); + } + setI(L, L.arg1(1), pos, LuaValue.ofNil()); + L.push(res); + return 1; + } + + /** {@code tmove} */ + private static int move(LuaState L) { + long f = LuaAux.checkInteger(L, 2); + long e = LuaAux.checkInteger(L, 3); + long t = LuaAux.checkInteger(L, 4); + int tt = (!L.isNoneArg(5) && !L.arg1(5).isNil()) ? 5 : 1; + checkTab(L, 1, TAB_R); + checkTab(L, tt, TAB_W); + if (e >= f) { + /* + * 必须是 {@code e < Long.MAX_VALUE + f},不能化简成 + * {@code e - f < Long.MAX_VALUE}。两者在无溢出时等价,但这里就是 + * 要判溢出:{@code f} 为负得厉害时 {@code e - f} 先回绕成负数, + * 化简后的式子反而成立,判断就漏过去了。 + *

+ * 实测(上游 {@code sort.lua:185}): + * {@code table.move({}, math.mininteger, math.maxinteger, 1)} + * 参考实现报 {@code bad argument #3 ... (too many elements to move)}, + * 照化简的写法会走到下面那条,报 + * {@code bad argument #4 ... (destination wrap around)}。 + *

+ * {@code f > 0} 短路之后 {@code Long.MAX_VALUE + f} 的 {@code f} + * 必为非正,因此这边的加法本身不会再溢出——与 C 版一致。 + */ + if (!(f > 0 || e < Long.MAX_VALUE + f)) { + throw L.argError(3, "too many elements to move"); + } + long n = e - f + 1; + if (t > Long.MAX_VALUE - n + 1) { + throw L.argError(4, "destination wrap around"); + } + LuaValue src = L.arg1(1); + LuaValue dst = L.arg1(tt); + /* + * C 版第三个条件 {@code tt != 1 && !lua_compare(1, tt, OPEQ)} 别省: + * 源和目标是两张不同的表时一律正向拷贝,跟 {@code t}、{@code f} + * 的相对位置无关。省掉它,本该正向的情形会走成逆向。 + *

+ * 内容完全相同,但插入次序反了,表内部的数组/哈希布局就不同, + * 于是 {@code #t} 跟着变——实测 + * {@code table.move({1}, -2, 1, -1, {1})} 之后目标表只有 {@code [2]=1}, + * 参考实现 {@code #t == 0}(正向先挖空 {@code [1]}), + * 逆向先塞 {@code [2]} 会把 alimit 顶到 2,得到 {@code #t == 2}。 + *

+ * 比较走 {@code lua_compare} 而不是引用比较,所以两张 {@code __eq} + * 相等的表仍按同表处理。 + */ + boolean forward = t > e || t <= f + || (tt != 1 && !Vm.equalObj(L, src, dst)); + if (forward) { + for (long i = 0; i < n; i++) { + setI(L, dst, t + i, getI(L, src, f + i)); + } + } else { + for (long i = n - 1; i >= 0; i--) { + setI(L, dst, t + i, getI(L, src, f + i)); + } + } + } + L.push(L.arg1(tt)); + return 1; + } + + /** {@code tconcat} */ + private static int concat(LuaState L) { + long last = auxGetN(L, 1, TAB_R); + LuaString sep = LuaAux.optString(L, 2, LuaString.EMPTY); + long i = LuaAux.optInteger(L, 3, 1); + last = LuaAux.optInteger(L, 4, last); + LuaAux.Buf b = new LuaAux.Buf(); + for (; i < last; i++) { + addField(L, b, i); + b.addString(sep); + } + if (i == last) { + addField(L, b, i); + } + L.push(LuaValue.ofString(b.result())); + return 1; + } + + private static void addField(LuaState L, LuaAux.Buf b, long i) { + LuaValue v = getI(L, L.arg1(1), i); + if (!(v.isString() || v.isNumber())) { + throw L.libError("invalid value (" + v.typeName() + ") at index " + i + + " in table for 'concat'"); + } + b.addString(LuaAux.asString(v)); + } + + /** {@code tpack} */ + private static int pack(LuaState L) { + int n = L.getTop(); + LuaTable t = new LuaTable(n, 1); + for (int i = 0; i < n; i++) { + t.set(LuaValue.ofInteger(i + 1), new LuaValue(L.arg1(i + 1))); + } + t.set("n", LuaValue.ofInteger(n)); + L.push(LuaValue.ofTable(t)); + return 1; + } + + /** {@code tunpack} */ + private static int unpack(LuaState L) { + long i = LuaAux.optInteger(L, 2, 1); + long e = L.isNoneArg(3) || L.arg1(3).isNil() ? objLen(L, L.arg1(1)) + : LuaAux.checkInteger(L, 3); + if (i > e) { + return 0; // 空区间 + } + long n = e - i; // 元素个数减一 + /* + * {@code n} 必须按无符号比。C 版它是 {@code lua_Unsigned}, + * {@code e - i} 溢出时按无符号回绕成一个巨大的数,于是 + * {@code unpack({}, minI, maxI)} 直接落进下面那句 + * {@code "too many results to unpack"}。 + *

+ * Java 的 {@code long} 回绕成负数,照有符号比就漏过去了: + * {@code count} 于是变成 0,循环从 {@code minI} 一路 push 到 + * {@code maxI}——上游 {@code sort.lua} 第一节就是这么把 + * {@code table.unpack} 撑到 {@code OutOfMemoryError} 的。 + */ + if (Long.compareUnsigned(n, Integer.MAX_VALUE) >= 0) { + throw L.libError("too many results to unpack"); + } + int count = (int) n + 1; + /* + * 这里走的是 {@code lua_checkstack} + {@code luaL_error},而不是 + * {@code luaL_checkstack}:后者会拼成 + * {@code "stack overflow (too many results to unpack)"},而参考实现 + * {@code ltablib.c: tunpack} 给的是不带前缀的那句。 + * {@code count} 撑不下时两条路同归一处,所以分两步判。 + */ + if (!L.checkStackOk(count)) { + throw L.libError("too many results to unpack"); + } + LuaValue t = L.arg1(1); + for (long k = i; k < e; k++) { + L.push(getI(L, t, k)); + } + L.push(getI(L, t, e)); + return count; + } + + /* ==================== 快排 ==================== */ + + private static final int RANLIMIT = 100; + + /** {@code lua_compare(LUA_OPLT)} */ + private static boolean lessThan(LuaState L, LuaValue a, LuaValue b) { + if (a.isNumber() && b.isNumber()) { + return Vm.ltNum(a, b); + } + if (a.isString() && b.isString()) { + return Vm.lStrcmp(a.asString(), b.asString()) < 0; + } + return LuaTm.callOrderTm(L, a, b, Tm.TM_LT); + } + + /** {@code sort_comp}:没给比较函数就用 {@code <}(含 {@code __lt}) */ + private static boolean sortComp(LuaState L, LuaValue a, LuaValue b) { + if (L.isNoneArg(2) || L.arg1(2).isNil()) { + return lessThan(L, a, b); + } + int f = L.top; + L.push(L.arg1(2)); + L.push(a); + L.push(b); + /* + * {@code sort_comp} 用的是 {@code lua_call}(= 不可 yield): + * 实测 {@code pcall(table.sort, {1,2,3}, coroutine.yield)} 在参考实现里 + * 得到 {@code false, "attempt to yield across a C-call boundary"}, + * 也就是「根本没 yield 出去」。用可 yield 的 {@code call} 会真的挂起, + * 恢复时 {@code table.sort} 那一帧没有续体,直接 NPE。 + */ + L.callNoYield(f, 1); + boolean r = L.stack[L.top - 1].toBoolean(); + L.top--; + return r; + } + + private static void swap(LuaState L, LuaValue t, int i, int j) { + LuaValue vi = getI(L, t, i); + LuaValue vj = getI(L, t, j); + setI(L, t, i, vj); + setI(L, t, j, vi); + } + + /** + * {@code partition}。 + *

+ * 两处 {@code luaL_error("invalid order function for sorting")} 是「比较函数 + * 不自洽」的哨兵:向中间扫的两条循环本该在中途相遇,谁越过了谁就说明 + * {@code comp} 不满足传递性。这不是防御性检查,是参考实现会报的错误, + * 真实代码里靠它发现写错的比较函数。 + */ + private static int partition(LuaState L, LuaValue t, int lo, int up) { + int i = lo; + int j = up - 1; + LuaValue p = getI(L, t, up - 1); // 枢轴 + while (true) { + while (sortComp(L, getI(L, t, ++i), p)) { + if (i == up - 1) { + throw L.libError("invalid order function for sorting"); + } + } + while (sortComp(L, p, getI(L, t, --j))) { + if (j < i) { + throw L.libError("invalid order function for sorting"); + } + } + if (j < i) { + swap(L, t, up - 1, i); + return i; + } + swap(L, t, i, j); + } + } + + /** + * {@code choosePivot}。 + *

+ * {@code IdxT} 是 {@code unsigned int},这一点必须照搬:C 的 + * {@code rnd % (r4 * 2)} 是无符号取模,结果恒在 {@code [0, r4*2)}。 + * Java 的 {@code int} 有符号,而 {@link #randomizePivot()} 给出的是 + * C 里 {@code unsigned int} 的那 32 个比特——有一半概率落成负数, + * 此时 {@code rnd % m} 也是负的,{@code p} 会掉到 {@code lo} 以下, + * 极端情形 {@code p <= 0}:{@code a[p]} 是 nil,于是 + * {@code sort_comp(nil, number)} 抛出 + * 「attempt to compare nil with number」。上游 {@code sort.lua} 用未播种的 + * {@code math.random()} 造 5 万个元素,这个缺陷就表现成偶发失败。 + */ + private static int choosePivot(int lo, int up, int rnd) { + int r4 = (up - lo) / 4; + int p = Integer.remainderUnsigned(rnd, r4 * 2) + (lo + r4); + return p; + } + + /** + * {@code auxsort}:递归用显式栈展开(C 版也把尾递归改成了循环)。 + *

+ * {@code rnd} 是按值传的:C 里它是 {@code auxsort} 的形参, + * 递归调用里的重新赋值不会漏回外层帧。早先这里用 {@code int[]} 共享, + * 深层递归换掉的随机种子会一路影响外层剩下的分区——虽然只影响枢轴 + * 选择、不改变排序结果,但和参考实现不是同一个算法了。 + */ + private static void auxSort(LuaState L, LuaValue t, int lo, int up, int rnd) { + while (lo < up) { + int p; + int n; + if (sortComp(L, getI(L, t, up), getI(L, t, lo))) { + swap(L, t, lo, up); + } + if (up - lo == 1) { + return; + } + if (up - lo < RANLIMIT || rnd == 0) { + p = (lo + up) / 2; + } else { + p = choosePivot(lo, up, rnd); + } + if (sortComp(L, getI(L, t, p), getI(L, t, lo))) { + swap(L, t, p, lo); + } else if (sortComp(L, getI(L, t, up), getI(L, t, p))) { + swap(L, t, p, up); + } + if (up - lo == 2) { + return; + } + swap(L, t, p, up - 1); // 枢轴换到 up-1 + p = partition(L, t, lo, up); + if (p - lo < up - p) { + auxSort(L, t, lo, p - 1, rnd); + n = p - lo; + lo = p + 1; + } else { + auxSort(L, t, p + 1, up, rnd); + n = up - p; + up = p - 1; + } + if ((up - lo) / 128 > n) { + rnd = randomizePivot(); + } + } + } + + /** + * {@code l_randomizePivot}。 + *

+ * C 版拿 {@code clock()} 与 {@code time()} 的字节当随机源——它只影响枢轴选择, + * 不影响排序结果。这里用 {@code System.nanoTime()} 的低位,作用相同。 + */ + private static int randomizePivot() { + return (int) (System.nanoTime() & 0xFFFFFFFFL); + } + + /** {@code sort} */ + private static int sort(LuaState L) { + long n = auxGetN(L, 1, TAB_R | TAB_W); + if (n > 1) { + if (n >= Integer.MAX_VALUE) { + throw L.argError(1, "array too big"); + } + if (!L.isNoneArg(2) && !L.arg1(2).isNil()) { + LuaAux.checkType(L, 2, Lua.TFUNCTION); + } + L.ensureSlots(L.ci.func + 3); + for (int k = L.top; k < L.ci.func + 3; k++) { + L.stack[k].setNil(); // lua_settop 补出来的槽是 nil,不是上一帧的残值 + } + L.top = L.ci.func + 3; + auxSort(L, L.arg1(1), 1, (int) n, 0); + } + return 0; + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/Tm.java b/src/main/java/com/github/esrrhs/fakescript/lua/Tm.java new file mode 100644 index 0000000..619e218 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/Tm.java @@ -0,0 +1,53 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * 元方法编号 —— 对应 {@code ltm.h} 的 {@code TMS} 与 {@code ltm.c} 的 + * {@code luaT_eventname}。 + *

+ * 顺序不能动:{@code OP_MMBIN} 系列指令把 TMS 编号直接编进 C 字段, + * 而 {@code binopr2TM} 又是靠「ORDER OPR 与 ORDER TM 对齐」做减法算出来的。 + */ +public final class Tm { + + private Tm() { + } + + public static final int TM_INDEX = 0; + public static final int TM_NEWINDEX = 1; + public static final int TM_GC = 2; + public static final int TM_MODE = 3; + public static final int TM_LEN = 4; + /** 最后一个「快速访问」的元方法 */ + public static final int TM_EQ = 5; + public static final int TM_ADD = 6; + public static final int TM_SUB = 7; + public static final int TM_MUL = 8; + public static final int TM_MOD = 9; + public static final int TM_POW = 10; + public static final int TM_DIV = 11; + public static final int TM_IDIV = 12; + public static final int TM_BAND = 13; + public static final int TM_BOR = 14; + public static final int TM_BXOR = 15; + public static final int TM_SHL = 16; + public static final int TM_SHR = 17; + public static final int TM_UNM = 18; + public static final int TM_BNOT = 19; + public static final int TM_LT = 20; + public static final int TM_LE = 21; + public static final int TM_CONCAT = 22; + public static final int TM_CALL = 23; + public static final int TM_CLOSE = 24; + public static final int TM_N = 25; + + /** ORDER TM —— 与上面的编号严格同序 */ + public static final String[] EVENT_NAMES = { + "__index", "__newindex", + "__gc", "__mode", "__len", "__eq", + "__add", "__sub", "__mul", "__mod", "__pow", + "__div", "__idiv", + "__band", "__bor", "__bxor", "__shl", "__shr", + "__unm", "__bnot", "__lt", "__le", + "__concat", "__call", "__close" + }; +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/Undump.java b/src/main/java/com/github/esrrhs/fakescript/lua/Undump.java new file mode 100644 index 0000000..e97a23d --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/Undump.java @@ -0,0 +1,588 @@ +package com.github.esrrhs.fakescript.lua; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +/** + * 读预编译 chunk —— 对应 {@code lundump.c}。 + *

+ * 与 {@link Parser} 是并列的两条加载路径:{@code f_parser} 先看第一个字节, + * 是 {@code LUA_SIGNATURE[0]}({@code \27})就走这里,否则走词法/语法分析。 + * 没有这条路径,{@code string.dump} 的产物就回不来,而 + * {@code luac} 预编译过的发行脚本是相当常见的形态,所以这不是可选项。 + * + *

格式

+ * 头部:签名 {@code \27Lua}(首字节已被 {@code f_parser} 吃掉)、版本、格式号、 + * 6 字节校验串 {@code \x19\x93\r\n\x1a\n}、三个尺寸字节、以及一个整数与一个 + * 浮点用于抓字节序/端序转换错误。之后是一颗函数:源名、行号范围、参数信息、 + * 指令、常量、upvalue、内嵌函数、调试信息。 + *

+ * 长度用的是「7 位一组、最高位置 1 表示最后一组」的变长编码,大端在前; + * 整数与浮点都是 8 字节小端原样铺开。 + * + *

去掉的部分

+ * {@code loadString} 里给长字符串上的 {@code luaC_objbarrier}、以及 + * {@code luai_verifycode},都是给 {@code lgc.c} 用的,这里没有 GC,不需要。 + */ +public final class Undump { + + private Undump() { + } + + /* ---------- 常量 ---------- */ + + /** {@code LUA_SIGNATURE} */ + private static final byte[] SIGNATURE = { 0x1b, 'L', 'u', 'a' }; + + /** {@code LUAC_VERSION}:主版本号放高 4 位、次版本号放低 4 位(504 → 0x54) */ + private static final int LUAC_VERSION = 0x54; + + /** {@code LUAC_FORMAT}:官方格式 */ + private static final int LUAC_FORMAT = 0; + + /** {@code LUAC_DATA}:用来抓文本/二进制转换错误的校验串 */ + private static final byte[] LUAC_DATA = { 0x19, (byte) 0x93, '\r', '\n', 0x1a, '\n' }; + + /** {@code LUAC_INT}:整数格式的探针值 */ + private static final long LUAC_INT = 0x5678L; + + /** {@code LUAC_NUM}:浮点格式的探针值 */ + private static final double LUAC_NUM = 370.5; + + /** {@code sizeof(Instruction)} */ + private static final int SIZE_INSTRUCTION = 4; + + /** {@code sizeof(lua_Integer)} */ + private static final int SIZE_INTEGER = 8; + + /** {@code sizeof(lua_Number)} */ + private static final int SIZE_NUMBER = 8; + + /** {@code LUAI_MAXSHORTLEN} */ + private static final int MAXSHORTLEN = 40; + + /** + * 长字符串声明长度的上限(128 TiB)。 + *

+ * 走到长字符串分支时,C 是 {@code luaS_createlngstrobj(L, size)} 整块 malloc: + * 成了就接着读、读到流末尾报 {@code truncated chunk};失败就抛 + * {@code LUA_ERRMEM},消息是 {@code globalState} 里写死的那份 + * {@code "not enough memory"}(不带位置前缀)。 + *

+ * 「成不成」取决于机器内存与过量提交策略,不是 Lua 的语义,所以这里只能 + * 取一个定值。取 128 TiB 的理由是两头都够远:正常 chunk 里不会有一条上百 + * TB 的字符串常量,而这么大的申请在任何机器上都会失败,于是这一档能 + * 与参考实现对齐。夹在中间的那一档(十几 GB 到上百 TB,取决于内存) + * 观测不到,不进差分表。 + */ + private static final long MAX_LONG_STRING = 1L << 47; + + /* ---------- LoadState ---------- */ + + private static final class LoadState { + final LuaState L; + final Zio z; + /** 错误信息里用的源名(已按 {@code luaU_undump} 的规则整理过) */ + final byte[] name; + + LoadState(LuaState L, Zio z, byte[] name) { + this.L = L; + this.z = z; + this.name = name; + } + } + + /** {@code lundump.c: error}:{@code ": bad binary format ()"} */ + private static LuaError error(LoadState S, String why) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(S.name, 0, S.name.length); + byte[] tail = (": bad binary format (" + why + ")").getBytes(StandardCharsets.UTF_8); + out.write(tail, 0, tail.length); + LuaError e = new LuaError(LuaValue.ofString(LuaString.of(out.toByteArray()))); + // 与 lexerror 同一条路:luaD_throw(LUA_ERRSYNTAX) 直跳,不过 errfunc + e.directThrow = true; + throw e; + } + + /* ---------- 基本读取 ---------- */ + + private static void loadBlock(LoadState S, byte[] b, int off, int size) { + for (int i = 0; i < size; i++) { + int c = S.z.getc(); + if (c == Zio.EOZ) { + error(S, "truncated chunk"); + } + b[off + i] = (byte) c; + } + } + + private static int loadByte(LoadState S) { + int b = S.z.getc(); + if (b == Zio.EOZ) { + error(S, "truncated chunk"); + } + return b; + } + + /** + * 变长无符号数:7 位一组、大端在前,最高位置 1 的那组是最后一组。 + *

+ * {@code limit >>= 7} 之后再比,是 C 版防溢出的写法——照搬,否则 + * 一个恶意 chunk 能用一堆 {@code 0x7f} 把 {@code x} 顶到回绕。 + */ + private static long loadUnsigned(LoadState S, long limit) { + long x = 0; + int b; + /* + * 必须用逻辑右移:limit 传进来的是 C 的 {@code size_t} 上限, + * 而 {@code MAX_SIZET} 是 {@code ~(size_t)0}(全 1),不是 + * {@code Long.MAX_VALUE}。两者 {@code >>7} 之后差一整倍—— + * 用 {@code Long.MAX_VALUE} 会在第 9 组就误报 integer overflow, + * 而参考实现那时要到第 10 组才报(第 9 组之后是截断)。 + */ + limit >>>= 7; + do { + b = loadByte(S); + /* + * C 里 {@code x} 和 {@code limit} 都是 {@code size_t},这个 + * {@code x >= limit} 是无符号比较。这里必须用 + * {@code compareUnsigned}:变长整数被一堆 {@code 0x00} 顶到第 10 + * 组时会跨过 {@code 2^63}、折成负数,有符号比较就认不出溢出 + * ——于是继续往下读,最终报 truncated chunk,而参考实现报 + * integer overflow。这是第 14 条(C 的 {@code unsigned} 一律按 + * 无符号处理)的第四例,前三例是 tunpack / checkStack / sort 枢轴。 + */ + if (Long.compareUnsigned(x, limit) >= 0) { + error(S, "integer overflow"); + } + x = (x << 7) | (b & 0x7f); + } while ((b & 0x80) == 0); + return x; + } + + /** + * C 的 {@code MAX_SIZET}({@code ~(size_t)0},64 位全 1)。 + *

+ * Java 的 {@code long} 没有无符号,全 1 就是 {@code -1};配合 + * {@link #loadUnsigned} 里的 {@code >>>} 正好得到 C 的那个上限。 + */ + private static final long MAX_SIZET = -1L; + + /** + * 返回 {@code size_t}。Java 没有无符号 64 位,用 {@code long} 接, + * 由调用方(只有 {@link #loadStringN})按实际能读到的字节数决定怎么存。 + *

+ * 这里不能对「大到 Java 数组放不下」的长度提前报错。上一版就是这么 + * 写的,注释还断言「C 会去分配并抛 {@code LUA_ERRMEM}」——照着 C 源码读 + * 出来的结论,实测是错的:把源名长度写成 {@code 2^35-1}(变长整数 + * {@code 7f 7f 7f 7f ff}),参考实现给的是 {@code truncated chunk} 而不是 + * 内存错误。64 位上 malloc 34GB 是成功的,C 于是老老实实去读那 34GB, + * 读到流末尾报截断。提前报 {@code integer overflow} 等于凭空多出一个 + * C 里不存在的分支,白白对不上一个用例。 + */ + private static long loadSize(LoadState S) { + return loadUnsigned(S, MAX_SIZET); + } + + private static int loadInt(LoadState S) { + return (int) loadUnsigned(S, Integer.MAX_VALUE); + } + + private static double loadNumber(LoadState S) { + return Double.longBitsToDouble(loadInteger(S)); + } + + private static long loadInteger(LoadState S) { + byte[] b = new byte[SIZE_INTEGER]; + loadBlock(S, b, 0, SIZE_INTEGER); + long v = 0; + for (int i = SIZE_INTEGER - 1; i >= 0; i--) { // 小端 + v = (v << 8) | (b[i] & 0xFF); + } + return v; + } + + /* ---------- 字符串 ---------- */ + + /** {@code loadStringN}:可为 null 的字符串。size==0 表示没有 */ + private static LuaString loadStringN(LoadState S, Proto p) { + long size = loadSize(S); + if (size == 0) { + return null; + } + size--; // 存的是 len+1,好让 0 表示「没有」 + LuaString ts; + // 同上:C 的 size 是 size_t,这个比较也是无符号的 + if (Long.compareUnsigned(size, MAXSHORTLEN) <= 0) { + byte[] buff = new byte[(int) size]; + if (size > 0) { + loadBlock(S, buff, 0, (int) size); + } + ts = LuaString.of(buff); + } else { + /* + * 声明的长度越过 {@link #MAX_LONG_STRING} 时,C 那头的 malloc + * 必然失败,于是报 {@code nil, "not enough memory"};这里照做 + * ——注意消息不带位置前缀,与参考实现一致。 + *

+ * 没越线就边读边攒:攒多少只看流里真有多少,而不是声明了 + * 多少(声明值来自不可信输入,真拿去预分配会 OOM);流一尽照样由 + * {@code loadBlock} 报 truncated chunk。 + */ + if (Long.compareUnsigned(size, MAX_LONG_STRING) > 0) { + // 同上:C 的 malloc 失败是 luaM_error(LUA_ERRMEM),不是普通错误 + LuaError e = new LuaError( + LuaValue.ofString(LuaString.latin1("not enough memory"))); + e.errMem = true; + throw e; + } + ByteArrayOutputStream acc = new ByteArrayOutputStream(); + byte[] block = new byte[8192]; + for (long left = size; left > 0;) { + int n = (int) Math.min(left, block.length); + loadBlock(S, block, 0, n); // 中途流尽 → truncated chunk + acc.write(block, 0, n); + left -= n; + } + ts = LuaString.of(acc.toByteArray()); + } + if (ts.isShort()) { + ts = S.L.strings.intern(ts); + } + return ts; + } + + /** {@code loadString}:不可为 null,取到 null 就是格式错 */ + private static LuaString loadString(LoadState S, Proto p) { + LuaString st = loadStringN(S, p); + if (st == null) { + error(S, "bad format for constant string"); + } + return st; + } + + /* ---------- 惰性增长 ---------- */ + + /** + * 下面几段都要「按流里声明的个数建数组」。畸形 chunk 可以声明 20 亿个 + * upvalue / 常量 / 指令,而 C 那头的 {@code luaM_reallocvectorchecked} + * 是整块 malloc —— Linux/macOS 有过量提交,这种 malloc 往往成功 + * (只虚占地址空间,真正触碰的只有流里那几十个字节),于是接着读、读到流 + * 末尾报 {@code truncated chunk}。 + *

+ * Java 里 {@code new X[2000000000]} 是真的要那么多堆,抛的是 + * {@code OutOfMemoryError}——{@code Error} 不是 {@code LuaError},会穿过 + * {@code pcall} 把宿主进程整个掀掉(第 4 条)。实测就是这样炸的:把源名 + * 长度那个变长整数改坏,参考给 {@code nil, "not enough memory"},我们直接 + * OOM 崩。 + *

+ * 所以一律改成按需增长:实际占用只跟流里真有的字节成正比,正好等于 + * C 真正触碰到的那些页;流一尽照样由 {@code loadBlock} 报 + * {@code truncated chunk},与参考实现一致。 + */ + private static final int MIN_CAP = 8; + + /** 从 {@code cur} 起翻倍,返回能装下 {@code want} 个元素的容量。 */ + private static int grown(int cur, int want) { + int c = cur <= 0 ? MIN_CAP : cur; + while (c < want) { + if (c >= (Integer.MAX_VALUE >> 1)) { + return want; // 到头了:够一个是一个,剩下的交给 loadBlock 报错 + } + c <<= 1; + } + return c; + } + + /* ---------- 各段 ---------- */ + + /** + * 指令逐条读,缓冲复用。 + *

+ * 上一版是先 {@code new byte[n * 4]} 整块读再拆。两个问题: + *

    + *
  • {@code n * 4} 会整数溢出。{@code n} 来自 {@code loadInt}, + * 上限受 {@code loadUnsigned} 约束,但那层约束一松(把 + * {@code limit >>>= 7} 写错一点点就够),{@code n * 4} 立刻变负数, + * {@code new byte[-1073676284]} 抛的是 + * {@code NegativeArraySizeException}——Java 异常, + * {@code pcall} 抓不住,一个畸形 chunk 就能把宿主进程炸掉。
  • + *
  • 多占一倍内存。C 只分配 {@code n} 条指令;这边额外再铺 + * {@code 4n} 字节,等于照声明的尺寸双倍付款,而这个尺寸是 + * 不可信输入给的。
  • + *
+ * 逐条读既不溢出、也只按 {@code n} 分配;流一尽照样由 + * {@code loadBlock} 报 {@code truncated chunk},与参考实现一致。 + */ + private static void loadCode(LoadState S, Proto f) { + int n = loadInt(S); + f.code = new int[Math.min(Math.max(n, 0), MIN_CAP)]; + f.sizecode = n; + byte[] b = new byte[SIZE_INSTRUCTION]; + for (int i = 0; i < n; i++) { + if (i >= f.code.length) { + f.code = Arrays.copyOf(f.code, grown(f.code.length, i + 1)); + } + loadBlock(S, b, 0, SIZE_INSTRUCTION); + int v = 0; + for (int k = SIZE_INSTRUCTION - 1; k >= 0; k--) { // 小端 + v = (v << 8) | (b[k] & 0xFF); + } + f.code[i] = v; + } + if (f.code.length != n) { + f.code = Arrays.copyOf(f.code, Math.max(n, 0)); + } + } + + private static void loadConstants(LoadState S, Proto f) { + int n = loadInt(S); + f.k = new LuaValue[Math.min(Math.max(n, 0), MIN_CAP)]; + f.sizek = n; + // 建一个、立刻解析一个。分成「先铺 nil」「再解析」两趟的话, + // 第一趟不读流,畸形计数会直接把堆吃光(实测 OOM)。 + for (int i = 0; i < n; i++) { + if (i >= f.k.length) { + f.k = Arrays.copyOf(f.k, grown(f.k.length, i + 1)); + } + LuaValue o = LuaValue.ofNil(); + f.k[i] = o; + int t = loadByte(S); + switch (t) { + case Lua.VNIL: + o.setNil(); + break; + case Lua.VFALSE: + o.setBoolean(false); + break; + case Lua.VTRUE: + o.setBoolean(true); + break; + case Lua.VNUMFLT: + o.setFloat(loadNumber(S)); + break; + case Lua.VNUMINT: + o.setInteger(loadInteger(S)); + break; + case Lua.VSHRSTR: + case Lua.VLNGSTR: + o.setString(loadString(S, f)); + break; + default: + /* + * C 这里是 {@code default: lua_assert(0)},而 {@code lua_assert} + * 在默认构建下是 {@code ((void)0)}——也就是说不报错, + * 那个常量就停在循环开头铺好的 nil 上。实测: + * 常量类型字节写 99 的 chunk 能被正常加载成一个函数。 + * 改成报错看着更「安全」,但那是我们自己发明的语义。 + */ + break; + } + } + if (f.k.length != n) { + f.k = Arrays.copyOf(f.k, Math.max(n, 0)); + } + } + + private static void loadProtos(LoadState S, Proto f) { + int n = loadInt(S); + f.p = new Proto[Math.min(Math.max(n, 0), MIN_CAP)]; + f.sizep = n; + /* + * C 是先整块 malloc n 个指针、再逐个加载;这里把两步合一, + * 「建一个、立刻加载一个」。声明的个数不可信,但流里的字节数可信—— + * 所以按需建既能对上 C 的结果(读到流末尾报 truncated chunk), + * 又不会为一个畸形数字去要几十 GB 堆。 + */ + for (int i = 0; i < n; i++) { + if (i >= f.p.length) { + f.p = Arrays.copyOf(f.p, grown(f.p.length, i + 1)); + } + f.p[i] = new Proto(); + loadFunction(S, f.p[i], f.source); + } + if (f.p.length != n) { + f.p = Arrays.copyOf(f.p, Math.max(n, 0)); + } + } + + private static void loadUpvalues(LoadState S, Proto f) { + int n = loadInt(S); + f.upvalues = new Proto.Upvaldesc[Math.min(Math.max(n, 0), MIN_CAP)]; + f.sizeupvalues = n; + // 同上:建一个、读一个,别先照声明的个数全铺开 + for (int i = 0; i < n; i++) { + if (i >= f.upvalues.length) { + f.upvalues = Arrays.copyOf(f.upvalues, grown(f.upvalues.length, i + 1)); + } + Proto.Upvaldesc u = new Proto.Upvaldesc(); + u.instack = loadByte(S) != 0; + u.idx = loadByte(S); + u.kind = loadByte(S); + f.upvalues[i] = u; + } + if (f.upvalues.length != n) { + f.upvalues = Arrays.copyOf(f.upvalues, Math.max(n, 0)); + } + } + + private static void loadDebug(LoadState S, Proto f) { + int n = loadInt(S); + f.lineinfo = new byte[Math.min(Math.max(n, 0), MIN_CAP)]; + f.sizelineinfo = n; + /* + * 行号表是整块 loadBlock 的,不像上面那几段可以「读一个建一个」。 + * 所以换成 8192 一块地攒——声明的长度不可信,攒多少只看流里真有多少; + * 流一尽同样报 truncated chunk。 + */ + int li = 0; + byte[] lblk = new byte[8192]; + for (long left = n; left > 0;) { + int k = (int) Math.min(left, lblk.length); + loadBlock(S, lblk, 0, k); + if (li + k > f.lineinfo.length) { + f.lineinfo = Arrays.copyOf(f.lineinfo, grown(f.lineinfo.length, li + k)); + } + System.arraycopy(lblk, 0, f.lineinfo, li, k); + li += k; + left -= k; + } + n = loadInt(S); + f.abslineinfo = new Proto.AbsLineInfo[Math.min(Math.max(n, 0), MIN_CAP)]; + f.sizeabslineinfo = n; + for (int i = 0; i < n; i++) { + int pc = loadInt(S); + int line = loadInt(S); + if (i >= f.abslineinfo.length) { + f.abslineinfo = Arrays.copyOf(f.abslineinfo, grown(f.abslineinfo.length, i + 1)); + } + f.abslineinfo[i] = new Proto.AbsLineInfo(pc, line); + } + if (f.abslineinfo.length != n) { + f.abslineinfo = Arrays.copyOf(f.abslineinfo, Math.max(n, 0)); + } + n = loadInt(S); + f.locvars = new Proto.LocVar[Math.min(Math.max(n, 0), MIN_CAP)]; + f.sizelocvars = n; + // 同样合成一趟:先建 LocVar 再立刻解析,别分两趟 + for (int i = 0; i < n; i++) { + if (i >= f.locvars.length) { + f.locvars = Arrays.copyOf(f.locvars, grown(f.locvars.length, i + 1)); + } + Proto.LocVar v = new Proto.LocVar(); + f.locvars[i] = v; + v.varname = loadStringN(S, f); + v.startpc = loadInt(S); + v.endpc = loadInt(S); + } + if (f.locvars.length != n) { + f.locvars = Arrays.copyOf(f.locvars, Math.max(n, 0)); + } + n = loadInt(S); + if (n != 0) { // 有调试信息就必须给出全部 upvalue 名 + n = f.sizeupvalues; + } + for (int i = 0; i < n; i++) { + f.upvalues[i].name = loadStringN(S, f); + } + } + + private static void loadFunction(LoadState S, Proto f, LuaString psource) { + f.source = loadStringN(S, f); + if (f.source == null) { // 这份 dump 里没有源名 → 沿用父函数的 + f.source = psource; + } + f.linedefined = loadInt(S); + f.lastlinedefined = loadInt(S); + f.numparams = loadByte(S); + f.isVararg = loadByte(S) != 0; + f.maxstacksize = loadByte(S); + loadCode(S, f); + loadConstants(S, f); + loadUpvalues(S, f); + loadProtos(S, f); + loadDebug(S, f); + } + + /* ---------- 头部校验 ---------- */ + + private static void checkLiteral(LoadState S, byte[] s, String msg) { + byte[] buff = new byte[s.length]; + loadBlock(S, buff, 0, buff.length); + for (int i = 0; i < s.length; i++) { + if (buff[i] != s[i]) { + error(S, msg); + } + } + } + + private static void checkSize(LoadState S, int size, String tname) { + if (loadByte(S) != size) { + error(S, tname + " size mismatch"); + } + } + + private static void checkHeader(LoadState S) { + // 首字节已被 f_parser 吃掉并比对过,这里从第二个字节开始 + byte[] rest = new byte[SIGNATURE.length - 1]; + System.arraycopy(SIGNATURE, 1, rest, 0, rest.length); + checkLiteral(S, rest, "not a binary chunk"); + if (loadByte(S) != LUAC_VERSION) { + error(S, "version mismatch"); + } + if (loadByte(S) != LUAC_FORMAT) { + error(S, "format mismatch"); + } + checkLiteral(S, LUAC_DATA, "corrupted chunk"); + checkSize(S, SIZE_INSTRUCTION, "Instruction"); + checkSize(S, SIZE_INTEGER, "lua_Integer"); + checkSize(S, SIZE_NUMBER, "lua_Number"); + if (loadInteger(S) != LUAC_INT) { + error(S, "integer format mismatch"); + } + if (loadNumber(S) != LUAC_NUM) { + error(S, "float format mismatch"); + } + } + + /* ---------- 入口 ---------- */ + + /** + * {@code luaU_undump}。 + *

+ * 源名的整理规则看着怪,但必须照抄:以 {@code @} 或 {@code =} 开头就去掉那个 + * 前缀字符,以签名首字节开头(也就是源名本身就是那段二进制 chunk, + * 正是 {@code load(s)} 不给第二个参数时的情形)则整段换成 + * {@code "binary string"},其余原样。漏掉最后一条, + * {@code load("\27Lua")} 报出的就会是一串 ESC 开头的乱码。 + */ + public static LClosure undump(LuaState L, Zio z, byte[] name) { + byte[] sname = name; + if (name.length > 0 && (name[0] == '@' || name[0] == '=')) { + sname = new byte[name.length - 1]; + System.arraycopy(name, 1, sname, 0, sname.length); + } else if (name.length > 0 && name[0] == SIGNATURE[0]) { + sname = "binary string".getBytes(StandardCharsets.US_ASCII); + } + LoadState S = new LoadState(L, z, sname); + checkHeader(S); + int nupvals = loadByte(S); + Proto p = new Proto(); + loadFunction(S, p, null); + LClosure cl = new LClosure(p, nupvals); + cl.initUpvals(); + // 第 0 号 upvalue 是 _ENV,与 lua_load 一致:装上全局表并关闭 + if (cl.upvals.length >= 1) { + cl.upvals[0] = UpVal.closed(LuaValue.ofTable(L.globals)); + } + return cl; + } + + /** 把 {@code long} 按小端写进 8 字节({@link #undump} 的对称操作,{@link Dump} 也用) */ + static void putInteger(byte[] b, int off, long v) { + for (int i = 0; i < SIZE_INTEGER; i++) { + b[off + i] = (byte) ((v >>> (8 * i)) & 0xFF); + } + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/UpVal.java b/src/main/java/com/github/esrrhs/fakescript/lua/UpVal.java new file mode 100644 index 0000000..506cc0c --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/UpVal.java @@ -0,0 +1,72 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * upvalue —— 对应 {@code lobject.h} 的 {@code UpVal}。 + *

+ * 两态设计与 C 版完全对应: + *

    + *
  • 开放:{@link #v} 直接引用栈上的槽对象,栈改即 upvalue 改。这正是 + * 「闭包捕获局部变量」能活过变量所在块的原因。
  • + *
  • 关闭:变量离开作用域时把槽的内容复制一份到自身存储,此后与栈无关。 + * C 版是把 {@code uv->v} 从栈地址改指向 {@code &uv->u.value}, + * 这里换成切换 {@link #v} 引用,语义等价。
  • + *
+ *

+ * 一个必须由调用方保证的不变式:栈槽对象一旦创建就不得替换。开放 upvalue + * 持有的是槽对象的引用,若栈扩容时把槽换成新对象,upvalue 就silently 断开了。 + * 所以 {@link LuaState} 一律用 {@code stack[i].set(v)},从不 {@code stack[i] = v}。 + */ +public final class UpVal { + + /** + * 当前指向的活槽。开放时是栈槽对象本身,关闭后是本对象自持的存储。 + */ + LuaValue v; + + /** 开放时对应的栈下标;{@code -1} 表示已关闭 */ + int stackIdx; + + /** 是否是 {@code } 声明的 to-be-closed 变量 */ + boolean toBeClosed; + + private UpVal() { + } + + /** 新建开放 upvalue,指向 {@code stackIdx} 处的栈槽 */ + static UpVal open(LuaValue slot, int stackIdx) { + UpVal uv = new UpVal(); + uv.v = slot; + uv.stackIdx = stackIdx; + return uv; + } + + /** 新建关闭 upvalue,持有 {@code value} 的一份拷贝 */ + static UpVal closed(LuaValue value) { + UpVal uv = new UpVal(); + uv.v = new LuaValue(value); + uv.stackIdx = -1; + return uv; + } + + boolean isOpen() { + return stackIdx >= 0; + } + + /** 读:返回活槽,调用方可直接读写(与 C 的 {@code uv->v.p} 一致) */ + LuaValue value() { + return v; + } + + /** + * 关闭:把栈槽内容搬进自身存储。 + *

+ * 必须是新建一个 {@link LuaValue} 而不能复用旧槽对象——旧槽还在栈上, + * 随时会被下一段代码改写。 + */ + void close() { + if (stackIdx >= 0) { + v = new LuaValue(v); + stackIdx = -1; + } + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/Utf8Lib.java b/src/main/java/com/github/esrrhs/fakescript/lua/Utf8Lib.java new file mode 100644 index 0000000..45fdc53 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/Utf8Lib.java @@ -0,0 +1,337 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * UTF-8 库 —— 对应 {@code lutf8lib.c}。 + *

+ * Lua 字符串是字节串,UTF-8 只是在其之上的一层解释:{@code utf8.len} + * 数的是字符不是字节,{@code #s} 仍然数字节。这个区分是刻意的,写成 + * 「字符串内部就是 UTF-8」会把含 {@code \xFF} 的串变成不可用。 + *

+ * 所有解码都自己按字节做,不交给 Java 的 {@code String}: + * 一方面要能识别「不合法序列」并返回 {@code nil + 位置}(Java 的解码器只会 + * 替换成 U+FFFD),另一方面 {@code lax} 模式下代理项与超范围码点是合法的。 + */ +public final class Utf8Lib { + + private Utf8Lib() { + } + + private static final long MAXUNICODE = 0x10FFFFL; + private static final long MAXUTF = 0x7FFFFFFFL; + private static final String MSG_INVALID = "invalid UTF-8 code"; + + /** {@code iscont} */ + private static boolean isCont(int c) { + return (c & 0xC0) == 0x80; + } + + public static void open(LuaState L) { + LuaTable u = new LuaTable(); + LuaAux.setFn(u, "offset", Utf8Lib::byteOffset); + LuaAux.setFn(u, "codepoint", Utf8Lib::codePoint); + LuaAux.setFn(u, "char", Utf8Lib::utfChar); + LuaAux.setFn(u, "len", Utf8Lib::utfLen); + LuaAux.setFn(u, "codes", Utf8Lib::iterCodes); + u.set("charpattern", LuaValue.ofString(charPattern())); + LuaAux.registerLib(L, "utf8", u); + } + + /** + * {@code UTF8PATT}:{@code [\0-\x7F\xC2-\xFD][\x80-\xBF]*}。 + *

+ * 首字节里排除了 {@code \xC0} / {@code \xC1}(过长编码)与 {@code \xFE} / + * {@code \xFF}(非法),所以它能不重不漏地匹配一个 UTF-8 字符。 + *

+ * 只留这一份:早先 {@code open} 里还抄了一份同样的字面量,而 + * {@code charPattern()} 一个调用者都没有 —— 于是「改了这边没改那边」这种 + * 漂移是看不出来的(覆盖率报告上表现为整段 nc)。合成一处之后 + * {@code utf8.charpattern} 这条也就被真正走过了。 + */ + public static LuaString charPattern() { + return LuaString.of(new byte[] { '[', 0x00, '-', 0x7F, (byte) 0xC2, '-', (byte) 0xFD, ']', + '[', (byte) 0x80, '-', (byte) 0xBF, ']', '*' }); + } + + /* ==================== 解码 ==================== */ + + /** + * {@code utf8_decode}:从 {@code i} 解出一个码点。 + * + * @return 下一个字符的起始下标,{@code -1} 表示序列不合法 + */ + private static int decode(byte[] s, int i, long[] val, boolean strict) { + // 注意 limits[0]:C 里是 ~(utfint)0,而 utfint 是 32 位 unsigned int, + // 所以是 0xFFFFFFFF 而不是 -1。写成 -1 会让「首字节是续字节」这种 + // 情形(count == 0)通过检查——utf8.len("中文", 2) 就会返回 3 而不是 nil + long[] limits = { 0xFFFFFFFFL, 0x80L, 0x800L, 0x10000L, 0x200000L, 0x4000000L }; + int c = s[i] & 0xFF; + long res = 0; + if (c < 0x80) { + res = c; + } else { + int count = 0; + int cc = c; + // 不能把 cc 掩回 8 位:C 的 c 是 32 位 unsigned int,左移后 + // 高位还在,循环条件看的是「原字节从 bit6 往下有几个连续的 1」。 + // 掩成 8 位会把 0xF8 判成 3 个续字节(应为 4 个),于是把 + // 5 字节序列当成合法的 4 字节序列收下。 + for (; (cc & 0x40) != 0; cc <<= 1) { + if (i + count + 1 >= s.length) { + return -1; // C 版靠结尾的 \0 终止,这里必须显式判越界 + } + int next = s[i + count + 1] & 0xFF; + if (!isCont(next)) { + return -1; + } + res = (res << 6) | (next & 0x3F); + count++; + } + res |= ((long) (cc & 0x7F)) << (count * 5); + if (count > 5 || res > MAXUTF || res < limits[count]) { + return -1; + } + i += count; + } + if (strict) { + if (res > MAXUNICODE || (0xD800L <= res && res <= 0xDFFFL)) { + return -1; + } + } + if (val != null) { + val[0] = res; + } + return i + 1; + } + + /** {@code luaO_utf8esc}:把一个码点编成 UTF-8 字节 */ + private static byte[] utf8Esc(long x) { + byte[] out = new byte[8]; + int n = 1; + if (x < 0x80) { + out[8 - 1] = (byte) x; + } else { + long mfb = 0x3F; + do { + out[8 - (n++)] = (byte) (0x80 | (x & 0x3F)); + x >>= 6; + mfb >>= 1; + } while (x > mfb); + out[8 - n] = (byte) (((~mfb) << 1) | x); + } + byte[] r = new byte[n]; + System.arraycopy(out, 8 - n, r, 0, n); + return r; + } + + /* ==================== 各函数 ==================== */ + + /** {@code u_posrelat} */ + private static long uPosRelat(long pos, int len) { + if (pos >= 0) { + return pos; + } + if (pos < -(long) len) { + return 0; + } + return len + pos + 1; + } + + /** {@code utflen} */ + private static int utfLen(LuaState L) { + LuaString str = LuaAux.checkString(L, 1); + byte[] s = str.bytes; + int len = s.length; + long posi = uPosRelat(LuaAux.optInteger(L, 2, 1), len); + long posj = uPosRelat(LuaAux.optInteger(L, 3, -1), len); + boolean lax = !L.isNoneArg(4) && L.arg1(4).toBoolean(); + if (!(1 <= posi && --posi <= len)) { + throw L.argError(2, "initial position out of bounds"); + } + if (!(--posj < len)) { + throw L.argError(3, "final position out of bounds"); + } + long n = 0; + int p = (int) posi; + int pj = (int) posj; + while (p <= pj) { + int next = decode(s, p, null, !lax); + if (next < 0) { + LuaAux.pushFail(L); + L.push(LuaValue.ofInteger(p + 1)); + return 2; + } + p = next; + n++; + } + L.push(LuaValue.ofInteger(n)); + return 1; + } + + /** {@code codepoint} */ + private static int codePoint(LuaState L) { + LuaString str = LuaAux.checkString(L, 1); + byte[] s = str.bytes; + int len = s.length; + long posi = uPosRelat(LuaAux.optInteger(L, 2, 1), len); + long pose = uPosRelat(LuaAux.optInteger(L, 3, posi), len); + boolean lax = !L.isNoneArg(4) && L.arg1(4).toBoolean(); + if (posi < 1) { + throw L.argError(2, "out of bounds"); + } + if (pose > len) { + throw L.argError(3, "out of bounds"); + } + if (posi > pose) { + return 0; + } + if (pose - posi >= Integer.MAX_VALUE) { + throw L.libError("string slice too long"); + } + int n = (int) (pose - posi) + 1; + LuaAux.checkStack(L, n, "string slice too long"); + int count = 0; + int p = (int) (posi - 1); + int end = (int) pose; + while (p < end) { + long[] code = new long[1]; + int next = decode(s, p, code, !lax); + if (next < 0) { + throw L.libError(MSG_INVALID); + } + L.push(LuaValue.ofInteger(code[0])); + count++; + p = next; + } + return count; + } + + /** {@code utfchar} */ + private static int utfChar(LuaState L) { + int n = L.getTop(); + // 范围检查必须按无符号比:C 里 code 是 (lua_Unsigned) 转出来的, + // utf8.char(-1) 于是变成 0xFFFF... 而报错。按有符号比 -1 会「通过」, + // 结果吐出一个 \xBF 开头的垃圾字节。 + if (n == 1) { + long code = LuaAux.checkInteger(L, 1); + if (Long.compareUnsigned(code, MAXUTF) > 0) { + throw L.argError(1, "value out of range"); + } + L.push(LuaValue.ofString(LuaString.of(utf8Esc(code)))); + return 1; + } + LuaAux.Buf b = new LuaAux.Buf(); + for (int i = 1; i <= n; i++) { + long code = LuaAux.checkInteger(L, i); + if (Long.compareUnsigned(code, MAXUTF) > 0) { + throw L.argError(i, "value out of range"); + } + byte[] e = utf8Esc(code); + b.addBytes(e, 0, e.length); + } + L.push(LuaValue.ofString(b.result())); + return 1; + } + + /** + * 按 C 的 {@code iscontp(s + i)} 读一个字节:越界(正好在串尾)时读到的是 + * 结尾那个 {@code '\0'},所以返回 0 而不是抛 {@code ArrayIndexOutOfBounds}。 + */ + private static int byteAt(byte[] s, int i) { + return (i < s.length) ? (s[i] & 0xFF) : 0; + } + + /** {@code byteoffset} */ + private static int byteOffset(LuaState L) { + LuaString str = LuaAux.checkString(L, 1); + byte[] s = str.bytes; + int len = s.length; + long n = LuaAux.checkInteger(L, 2); + long def = (n >= 0) ? 1 : len + 1; + long posi = uPosRelat(LuaAux.optInteger(L, 3, def), len); + if (!(1 <= posi && --posi <= len)) { + throw L.argError(3, "position out of bounds"); + } + int p = (int) posi; + // 下标允许等于 len:C 里那个位置是字符串结尾的 '\0'(不是续字节), + // 越界检查必须靠「当作 0 来读」而不是真的去索引数组 + if (n == 0) { + while (p > 0 && isCont(byteAt(s, p))) { + p--; + } + } else { + if (isCont(byteAt(s, p))) { + throw L.libError("initial position is a continuation byte"); + } + if (n < 0) { + while (n < 0 && p > 0) { + do { + p--; + } while (p > 0 && isCont(byteAt(s, p))); + n++; + } + } else { + n--; + while (n > 0 && p < len) { + do { + p++; + } while (isCont(byteAt(s, p))); // 结尾的 '\0' 会让它停下 + n--; + } + } + } + if (n == 0) { + L.push(LuaValue.ofInteger(p + 1)); + } else { + LuaAux.pushFail(L); + } + return 1; + } + + /** {@code iter_aux}:{@code codes} 的迭代器 */ + private static int iterAux(LuaState L, boolean strict) { + LuaString str = LuaAux.checkString(L, 1); + byte[] s = str.bytes; + long n = L.arg1(2).isInteger() ? L.arg1(2).i : 0; + if (n < 0) { + return 0; + } + int i = (int) n; + if (i < s.length) { + while (i < s.length && isCont(s[i] & 0xFF)) { + i++; + } + } + if (i >= s.length) { + return 0; + } + long[] code = new long[1]; + int next = decode(s, i, code, strict); + if (next < 0 || (next < s.length && isCont(s[next] & 0xFF))) { + throw L.libError(MSG_INVALID); + } + L.push(LuaValue.ofInteger(i + 1)); + L.push(LuaValue.ofInteger(code[0])); + return 2; + } + + private static int iterAuxStrict(LuaState L) { + return iterAux(L, true); + } + + private static int iterAuxLax(LuaState L) { + return iterAux(L, false); + } + + /** {@code iter_codes} */ + private static int iterCodes(LuaState L) { + LuaString s = LuaAux.checkString(L, 1); + boolean lax = !L.isNoneArg(2) && L.arg1(2).toBoolean(); + if (s.length() > 0 && isCont(s.byteAt(0))) { + throw L.argError(1, MSG_INVALID); + } + L.pushFunction(lax ? Utf8Lib::iterAuxLax : Utf8Lib::iterAuxStrict); + L.push(LuaValue.ofString(s)); + L.push(LuaValue.ofInteger(0)); + return 3; + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/Vm.java b/src/main/java/com/github/esrrhs/fakescript/lua/Vm.java new file mode 100644 index 0000000..c37928e --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/Vm.java @@ -0,0 +1,1929 @@ +package com.github.esrrhs.fakescript.lua; + +import java.nio.charset.StandardCharsets; +import java.util.Locale; + +/** + * 虚拟机 —— 对应 {@code lvm.c}。 + *

+ * 主循环用「一个 for + 一个 switch」实现 C 版的 computed goto。两处跳转靠标签模拟: + *

    + *
  • {@code goto startfunc}(调进一个 Lua 函数)→ {@code continue outer}, + * 由 {@code start} 标志重新装载 {@code cl / k / base / pc};
  • + *
  • {@code goto ret}(从 Lua 函数返回)→ 回到 {@code ci.previous} 的帧继续跑。
  • + *
+ * 这与 C 版「Lua 调用 Lua 不消耗 C 栈帧」的做法一致,代价是返回时要靠 + * {@code CIST_FRESH} 判断该在哪一层退出。 + *

+ * 算术不做字符串转换。这一点容易被直觉带偏:{@code "10" + 1 == 11} + * 并不是 VM 做的,而是 {@code lstrlib.c} 给字符串元表挂的 {@code __add} + * 先 {@code lua_stringtonumber} 再 {@code lua_arith}。VM 里所有算术用的都是 + * {@code tonumberns} / {@code tointegerns}(不带字符串转换)—— + * 照手册或照别的语言习惯写成「先转数字再算」,会在字符串有 {@code __add} + * 但转换失败时给出与参考实现不同的报错信息。 + *

+ * 算术指令后面必定跟一条 {@code OP_MMBIN*}:算成就 {@code pc++} 跳过它, + * 算不成(类型不对)就自然落到它上面去派发元方法。这个「成功才跳过」的约定 + * 是整套算术指令的隐式契约,写漏一个 {@code pc++} 都会在正确路径上多跑一次元方法。 + */ +public final class Vm { + + private Vm() { + } + + /** {@code MAXTAGLOOP}:{@code __index} / {@code __newindex} 链的循环上限 */ + private static final int MAX_TAG_LOOP = 2000; + + /* ================================================================ + * 主循环 + * ================================================================ */ + + static void execute(LuaState L, CallInfo ci0) { + CallInfo ci = ci0; + LClosure cl = null; + LuaValue[] k = null; + int[] code = null; + int base = 0; + int pc = 0; + boolean start = true; + + /* + * {@code goto ret} 与 {@code goto startfunc} 在 C 版是两个标签 + * ({@code returning:} 与 {@code startfunc:}),差别只有一句:前者 + * 不重新装载 {@code trap},后者从 {@code L->hookmask} 重载。 + * 这里合进同一个 {@code outer} 循环,就得用两个标志把它们分开—— + * 一律按 {@code startfunc} 处理的话,从内层函数返回时会把上层帧的 + * {@code trap} 冲掉,返回钩子之后的行钩子就没了。 + */ + boolean ret = false; + + /** + * {@code lvm.c} 的 {@code trap}:要不要在每条指令前调 + * {@code luaG_traceexec}。钩子没开时它恒为 0,这是「不开钩子就没有 + * 调试开销」的兑现方式。 + */ + boolean trap = false; + + outer: + for (;;) { + if (start || ret) { + if (start) { + trap = L.hookmask != 0; // 只有 startfunc 才重新装载 + } + start = false; + ret = false; + cl = (LClosure) L.stack[ci.func].o; + k = cl.p.k; + code = cl.p.code; + pc = ci.savedpc; + base = ci.func + 1; + if (trap) { + trap = DebugLib.traceCall(L, ci); + } + } + for (;;) { + /* + * {@code vmfetch()}:钩子(与栈重分配)就在这一个点上被检查。 + * {@code traceExec} 可能改 {@code ci.top},所以 {@code base} + * 要在它之后重算。 + */ + if (trap) { + trap = DebugLib.traceExec(L, ci, pc); + base = ci.func + 1; + } + int i = code[pc++]; + switch (OpCode.getOpCode(i)) { + + /* ---------------- 装载 ---------------- */ + + case OpCode.OP_MOVE: { + L.stack[base + OpCode.getArgA(i)].set(L.stack[base + OpCode.getArgB(i)]); + break; + } + case OpCode.OP_LOADI: { + L.stack[base + OpCode.getArgA(i)].setInteger(OpCode.getArgSBx(i)); + break; + } + case OpCode.OP_LOADF: { + L.stack[base + OpCode.getArgA(i)].setFloat(OpCode.getArgSBx(i)); + break; + } + case OpCode.OP_LOADK: { + L.stack[base + OpCode.getArgA(i)].set(k[OpCode.getArgBx(i)]); + break; + } + case OpCode.OP_LOADKX: { + L.stack[base + OpCode.getArgA(i)].set(k[OpCode.getArgAx(code[pc])]); + pc++; + break; + } + case OpCode.OP_LOADFALSE: { + L.stack[base + OpCode.getArgA(i)].setBoolean(false); + break; + } + case OpCode.OP_LFALSESKIP: { + L.stack[base + OpCode.getArgA(i)].setBoolean(false); + pc++; + break; + } + case OpCode.OP_LOADTRUE: { + L.stack[base + OpCode.getArgA(i)].setBoolean(true); + break; + } + case OpCode.OP_LOADNIL: { + int ra = base + OpCode.getArgA(i); + int b = OpCode.getArgB(i); + do { + L.stack[ra++].setNil(); + } while (b-- > 0); + break; + } + + /* ---------------- upvalue ---------------- */ + + case OpCode.OP_GETUPVAL: { + L.stack[base + OpCode.getArgA(i)].set(cl.upvals[OpCode.getArgB(i)].value()); + break; + } + case OpCode.OP_SETUPVAL: { + cl.upvals[OpCode.getArgB(i)].value().set(L.stack[base + OpCode.getArgA(i)]); + break; + } + + /* ---------------- 取 ---------------- */ + + case OpCode.OP_GETTABUP: { + int ra = base + OpCode.getArgA(i); + LuaValue upval = cl.upvals[OpCode.getArgB(i)].value(); + LuaValue rc = k[OpCode.getArgC(i)]; + LuaValue slot = fastGet(upval, rc); + if (slot != null) { + L.stack[ra].set(slot); + } else { + saveState(L, ci, pc); + finishGet(L, upval, rc, ra); + } + break; + } + case OpCode.OP_GETTABLE: { + int ra = base + OpCode.getArgA(i); + LuaValue rb = L.stack[base + OpCode.getArgB(i)]; + LuaValue rc = L.stack[base + OpCode.getArgC(i)]; + LuaValue slot = fastGet(rb, rc); + if (slot != null) { + L.stack[ra].set(slot); + } else { + saveState(L, ci, pc); + finishGet(L, rb, rc, ra); + } + break; + } + case OpCode.OP_GETI: { + int ra = base + OpCode.getArgA(i); + LuaValue rb = L.stack[base + OpCode.getArgB(i)]; + LuaValue key = LuaValue.ofInteger(OpCode.getArgC(i)); + LuaValue slot = fastGet(rb, key); + if (slot != null) { + L.stack[ra].set(slot); + } else { + saveState(L, ci, pc); + finishGet(L, rb, key, ra); + } + break; + } + case OpCode.OP_GETFIELD: { + int ra = base + OpCode.getArgA(i); + LuaValue rb = L.stack[base + OpCode.getArgB(i)]; + LuaValue rc = k[OpCode.getArgC(i)]; + LuaValue slot = fastGet(rb, rc); + if (slot != null) { + L.stack[ra].set(slot); + } else { + saveState(L, ci, pc); + finishGet(L, rb, rc, ra); + } + break; + } + + /* ---------------- 存 ---------------- */ + + case OpCode.OP_SETTABUP: { + LuaValue upval = cl.upvals[OpCode.getArgA(i)].value(); + LuaValue rb = k[OpCode.getArgB(i)]; + LuaValue rc = rkc(L, k, base, i); + LuaValue slot = fastSet(upval, rb); + if (slot != null) { + slot.set(rc); + } else { + saveState(L, ci, pc); + finishSet(L, upval, rb, rc); + } + break; + } + case OpCode.OP_SETTABLE: { + LuaValue ra = L.stack[base + OpCode.getArgA(i)]; + LuaValue rb = L.stack[base + OpCode.getArgB(i)]; + LuaValue rc = rkc(L, k, base, i); + LuaValue slot = fastSet(ra, rb); + if (slot != null) { + slot.set(rc); + } else { + saveState(L, ci, pc); + finishSet(L, ra, rb, rc); + } + break; + } + case OpCode.OP_SETI: { + LuaValue ra = L.stack[base + OpCode.getArgA(i)]; + LuaValue key = LuaValue.ofInteger(OpCode.getArgB(i)); + LuaValue rc = rkc(L, k, base, i); + LuaValue slot = fastSet(ra, key); + if (slot != null) { + slot.set(rc); + } else { + saveState(L, ci, pc); + finishSet(L, ra, key, rc); + } + break; + } + case OpCode.OP_SETFIELD: { + LuaValue ra = L.stack[base + OpCode.getArgA(i)]; + LuaValue rb = k[OpCode.getArgB(i)]; + LuaValue rc = rkc(L, k, base, i); + LuaValue slot = fastSet(ra, rb); + if (slot != null) { + slot.set(rc); + } else { + saveState(L, ci, pc); + finishSet(L, ra, rb, rc); + } + break; + } + + /* ---------------- 表构造 ---------------- */ + + case OpCode.OP_NEWTABLE: { + int ra = base + OpCode.getArgA(i); + int b = OpCode.getArgB(i); + int c = OpCode.getArgC(i); + if (b > 0) { + b = 1 << (b - 1); + } + /* + * EXTRAARG 对 NEWTABLE 是无条件存在的:k 位为 0 时它 + * 的 Ax 就是 0(上游有断言 (k==0) == (Ax==0))。所以这里的 + * pc++ 必须放在 k 判断外面——只对 SETLIST 才是条件的, + * 那条指令只在 c 溢出 8 位时才有 EXTRAARG。 + */ + if (OpCode.getArgK(i) != 0) { + c += OpCode.getArgAx(code[pc]) * (OpCode.MAXARG_C + 1); + } + pc++; + /* + * {@code L->top = ra + 1} —— C 版那句「correct top in case + * of emergency GC」。不能省:{@code OP_CALL} 收尾时 + * 会把 {@code top} 拉回 {@code ra}({@code moveresults} 的 + * {@code case 0}),此后 {@code top} 就落在还活着的寄存器 + * 底下;而 {@code top} 之上的槽在 Lua 里对 GC 不可见 + * ({@code traversethread} 只 mark {@code [stack, top)})。 + * 少了这一句,下面那个收集点就会把活着的局部量当成死区 + * (实测:{@code local m} 被清成 nil)。 + */ + L.top = ra + 1; + L.stack[ra].setTable(new LuaTable(c, b)); + /* + * {@code checkGC(L, ra+1)}:C 版主循环里仅有的两个收集点 + * 之一(另一个在 {@code OP_CLOSURE})。 + */ + if (L.gcDue()) { + L.maybeCollect(L); + } + break; + } + case OpCode.OP_SETLIST: { + int ra = base + OpCode.getArgA(i); + int n = OpCode.getArgB(i); + long last = OpCode.getArgC(i); + LuaTable h = L.stack[ra].asTable(); + if (n == 0) { + n = (int) (L.top - ra) - 1; + } else { + L.top = ci.top; + } + last += n; + if (OpCode.getArgK(i) != 0) { + last += (long) OpCode.getArgAx(code[pc]) * (OpCode.MAXARG_C + 1); + pc++; + } + for (; n > 0; n--) { + h.setArrayRaw(last, L.stack[ra + n]); + last--; + } + break; + } + + /* ---------------- 算术 ---------------- */ + + case OpCode.OP_ADDI: { + if (arithImm(L, i, base, Arith.OP_ADD)) { + pc++; + } + break; + } + case OpCode.OP_ADDK: { + if (arithK(L, i, base, k, Arith.OP_ADD)) { + pc++; + } + break; + } + case OpCode.OP_SUBK: { + if (arithK(L, i, base, k, Arith.OP_SUB)) { + pc++; + } + break; + } + case OpCode.OP_MULK: { + if (arithK(L, i, base, k, Arith.OP_MUL)) { + pc++; + } + break; + } + case OpCode.OP_MODK: { + saveState(L, ci, pc); + if (arithK(L, i, base, k, Arith.OP_MOD)) { + pc++; + } + break; + } + case OpCode.OP_POWK: { + if (arithK(L, i, base, k, Arith.OP_POW)) { + pc++; + } + break; + } + case OpCode.OP_DIVK: { + if (arithK(L, i, base, k, Arith.OP_DIV)) { + pc++; + } + break; + } + case OpCode.OP_IDIVK: { + saveState(L, ci, pc); + if (arithK(L, i, base, k, Arith.OP_IDIV)) { + pc++; + } + break; + } + case OpCode.OP_BANDK: { + if (arithK(L, i, base, k, Arith.OP_BAND)) { + pc++; + } + break; + } + case OpCode.OP_BORK: { + if (arithK(L, i, base, k, Arith.OP_BOR)) { + pc++; + } + break; + } + case OpCode.OP_BXORK: { + if (arithK(L, i, base, k, Arith.OP_BXOR)) { + pc++; + } + break; + } + case OpCode.OP_SHRI: { + int ra = base + OpCode.getArgA(i); + LuaValue rb = L.stack[base + OpCode.getArgB(i)]; + int ic = OpCode.getArgSC(i); + Long ib = toIntegerNs(rb, Arith.F2I_EQ); + if (ib != null) { + pc++; + L.stack[ra].setInteger(Arith.shiftl(ib, -ic)); + } + break; + } + case OpCode.OP_SHLI: { + int ra = base + OpCode.getArgA(i); + LuaValue rb = L.stack[base + OpCode.getArgB(i)]; + int ic = OpCode.getArgSC(i); + Long ib = toIntegerNs(rb, Arith.F2I_EQ); + if (ib != null) { + pc++; + L.stack[ra].setInteger(Arith.shiftl(ic, ib)); + } + break; + } + case OpCode.OP_ADD: { + if (arith(L, i, base, Arith.OP_ADD)) { + pc++; + } + break; + } + case OpCode.OP_SUB: { + if (arith(L, i, base, Arith.OP_SUB)) { + pc++; + } + break; + } + case OpCode.OP_MUL: { + if (arith(L, i, base, Arith.OP_MUL)) { + pc++; + } + break; + } + case OpCode.OP_MOD: { + saveState(L, ci, pc); + if (arith(L, i, base, Arith.OP_MOD)) { + pc++; + } + break; + } + case OpCode.OP_POW: { + if (arith(L, i, base, Arith.OP_POW)) { + pc++; + } + break; + } + case OpCode.OP_DIV: { + if (arith(L, i, base, Arith.OP_DIV)) { + pc++; + } + break; + } + case OpCode.OP_IDIV: { + saveState(L, ci, pc); + if (arith(L, i, base, Arith.OP_IDIV)) { + pc++; + } + break; + } + case OpCode.OP_BAND: { + if (arith(L, i, base, Arith.OP_BAND)) { + pc++; + } + break; + } + case OpCode.OP_BOR: { + if (arith(L, i, base, Arith.OP_BOR)) { + pc++; + } + break; + } + case OpCode.OP_BXOR: { + if (arith(L, i, base, Arith.OP_BXOR)) { + pc++; + } + break; + } + case OpCode.OP_SHR: { + if (arith(L, i, base, Arith.OP_SHR)) { + pc++; + } + break; + } + case OpCode.OP_SHL: { + if (arith(L, i, base, Arith.OP_SHL)) { + pc++; + } + break; + } + + /* ---------------- 元方法 ---------------- */ + + case OpCode.OP_MMBIN: { + int pi = code[pc - 2]; + LuaValue rb = L.stack[base + OpCode.getArgB(i)]; + int result = base + OpCode.getArgA(pi); + saveState(L, ci, pc); + LuaTm.tryBinTm(L, L.stack[base + OpCode.getArgA(i)], rb, result, + OpCode.getArgC(i)); + break; + } + case OpCode.OP_MMBINI: { + int pi = code[pc - 2]; + int result = base + OpCode.getArgA(pi); + saveState(L, ci, pc); + LuaTm.tryBinITm(L, L.stack[base + OpCode.getArgA(i)], OpCode.getArgSB(i), + OpCode.getArgK(i) != 0, result, OpCode.getArgC(i)); + break; + } + case OpCode.OP_MMBINK: { + int pi = code[pc - 2]; + int result = base + OpCode.getArgA(pi); + saveState(L, ci, pc); + LuaTm.tryBinAssocTm(L, L.stack[base + OpCode.getArgA(i)], + k[OpCode.getArgB(i)], OpCode.getArgK(i) != 0, result, + OpCode.getArgC(i)); + break; + } + + /* ---------------- 一元 ---------------- */ + + case OpCode.OP_UNM: { + int ra = base + OpCode.getArgA(i); + LuaValue rb = L.stack[base + OpCode.getArgB(i)]; + if (rb.isInteger()) { + L.stack[ra].setInteger(-rb.i); + } else { + Double nb = toNumberNs(rb); + if (nb != null) { + L.stack[ra].setFloat(-nb); + } else { + saveState(L, ci, pc); + LuaTm.tryBinTm(L, rb, rb, ra, Tm.TM_UNM); + } + } + break; + } + case OpCode.OP_BNOT: { + int ra = base + OpCode.getArgA(i); + LuaValue rb = L.stack[base + OpCode.getArgB(i)]; + Long ib = toIntegerNs(rb, Arith.F2I_EQ); + if (ib != null) { + L.stack[ra].setInteger(~ib); + } else { + saveState(L, ci, pc); + LuaTm.tryBinTm(L, rb, rb, ra, Tm.TM_BNOT); + } + break; + } + case OpCode.OP_NOT: { + L.stack[base + OpCode.getArgA(i)] + .setBoolean(!L.stack[base + OpCode.getArgB(i)].toBoolean()); + break; + } + case OpCode.OP_LEN: { + saveState(L, ci, pc); + objLen(L, base + OpCode.getArgA(i), L.stack[base + OpCode.getArgB(i)]); + break; + } + case OpCode.OP_CONCAT: { + int ra = base + OpCode.getArgA(i); + int n = OpCode.getArgB(i); + L.top = ra + n; + ci.savedpc = pc; + concat(L, n); + break; + } + + /* ---------------- 控制流 ---------------- */ + + case OpCode.OP_CLOSE: { + saveState(L, ci, pc); + L.closeUpvals(base + OpCode.getArgA(i), null); + break; + } + case OpCode.OP_TBC: { + /* + * {@code luaF_newtbcupval} 在赋值的当下就检查 + * {@code __close},不是在作用域结束时。所以 + * {@code local x = 5} 会在这一条指令上直接抛 + * “variable 'x' got a non-closable value”, + * 后面的 {@code print(x)} 根本不会执行——实测确认。 + * 挪到关闭时检查会让它先打印、最后才报错。 + */ + ci.savedpc = pc; + L.newTbcUpval(base + OpCode.getArgA(i)); + break; + } + case OpCode.OP_JMP: { + pc += OpCode.getArgSJ(i); + break; + } + + /* ---------------- 测试与比较 ---------------- */ + + case OpCode.OP_EQ: { + saveState(L, ci, pc); + int cond = equalObj(L, L.stack[base + OpCode.getArgA(i)], + L.stack[base + OpCode.getArgB(i)]) ? 1 : 0; + if (cond != OpCode.getArgK(i)) { + pc++; + } else { + pc += OpCode.getArgSJ(code[pc]) + 1; + } + break; + } + case OpCode.OP_LT: { + int cond = order(L, ci, pc, i, base, Tm.TM_LT) ? 1 : 0; + if (cond != OpCode.getArgK(i)) { + pc++; + } else { + pc += OpCode.getArgSJ(code[pc]) + 1; + } + break; + } + case OpCode.OP_LE: { + int cond = order(L, ci, pc, i, base, Tm.TM_LE) ? 1 : 0; + if (cond != OpCode.getArgK(i)) { + pc++; + } else { + pc += OpCode.getArgSJ(code[pc]) + 1; + } + break; + } + case OpCode.OP_EQK: { + int cond = LuaValue.rawEqual(L.stack[base + OpCode.getArgA(i)], + k[OpCode.getArgB(i)]) ? 1 : 0; + if (cond != OpCode.getArgK(i)) { + pc++; + } else { + pc += OpCode.getArgSJ(code[pc]) + 1; + } + break; + } + case OpCode.OP_EQI: { + LuaValue ra = L.stack[base + OpCode.getArgA(i)]; + int im = OpCode.getArgSB(i); + int cond; + if (ra.isInteger()) { + cond = (ra.i == im) ? 1 : 0; + } else if (ra.isFloat()) { + cond = (ra.n == (double) im) ? 1 : 0; + } else { + cond = 0; // 其余类型不可能等于数字 + } + if (cond != OpCode.getArgK(i)) { + pc++; + } else { + pc += OpCode.getArgSJ(code[pc]) + 1; + } + break; + } + case OpCode.OP_LTI: + case OpCode.OP_LEI: + case OpCode.OP_GTI: + case OpCode.OP_GEI: { + int op = OpCode.getOpCode(i); + /* + * 四条指令的比较运算符各不同,不能归约成「< 还是 <=」。 + * LTI/LEI/GTI/GEI 的立即数永远在右边({@code ra OP im}), + * inv 只影响元方法的调用顺序({@code ra >= im} 退化成 + * {@code im <= ra} 时要把两个操作数换过来)。 + */ + boolean inv = (op == OpCode.OP_GTI || op == OpCode.OP_GEI); + LuaValue ra = L.stack[base + OpCode.getArgA(i)]; + int im = OpCode.getArgSB(i); + int cond; + if (ra.isInteger()) { + long ia = ra.i; + switch (op) { + case OpCode.OP_LTI: + cond = (ia < im) ? 1 : 0; + break; + case OpCode.OP_LEI: + cond = (ia <= im) ? 1 : 0; + break; + case OpCode.OP_GTI: + cond = (ia > im) ? 1 : 0; + break; + default: + cond = (ia >= im) ? 1 : 0; + break; + } + } else if (ra.isFloat()) { + double fa = ra.n; + switch (op) { + case OpCode.OP_LTI: + cond = (fa < im) ? 1 : 0; + break; + case OpCode.OP_LEI: + cond = (fa <= im) ? 1 : 0; + break; + case OpCode.OP_GTI: + cond = (fa > im) ? 1 : 0; + break; + default: + cond = (fa >= im) ? 1 : 0; + break; + } + } else { + saveState(L, ci, pc); + int ev = (op == OpCode.OP_LTI || op == OpCode.OP_GTI) + ? Tm.TM_LT : Tm.TM_LE; + cond = LuaTm.callOrderITm(L, ra, im, inv, OpCode.getArgC(i) != 0, + ev) ? 1 : 0; + } + if (cond != OpCode.getArgK(i)) { + pc++; + } else { + pc += OpCode.getArgSJ(code[pc]) + 1; + } + break; + } + case OpCode.OP_TEST: { + int cond = L.stack[base + OpCode.getArgA(i)].toBoolean() ? 1 : 0; + if (cond != OpCode.getArgK(i)) { + pc++; + } else { + pc += OpCode.getArgSJ(code[pc]) + 1; + } + break; + } + case OpCode.OP_TESTSET: { + LuaValue rb = L.stack[base + OpCode.getArgB(i)]; + /* + * 判的是 {@code l_isfalse(rb) == k},不是「真值 == k」。 + *

+ * 这两种写法在 {@code k=0} 与 {@code k=1} 上恰好相反, + * 而且是那种「只在一半路径上错」的相反:{@code and} 的 + * {@code TESTSET} 只在「结果寄存器 ≠ B」时保留下来(否则 + * {@code patchTestReg} 会把它改写成 {@code OP_TEST}), + * 于是 {@code if x then} 一直是对的,{@code x and y} 一直是错的。 + *

+ * 症状是 {@code local t = {} print(t and "yes")} 打印出那张表: + * 字节码与参考实现逐条相同,说明错不在代码生成而在这一句。 + */ + int isFalse = rb.toBoolean() ? 0 : 1; // l_isfalse(rb) + if (isFalse == OpCode.getArgK(i)) { + pc++; + } else { + L.stack[base + OpCode.getArgA(i)].set(rb); + pc += OpCode.getArgSJ(code[pc]) + 1; + } + break; + } + case OpCode.OP_SELF: { + int ra = base + OpCode.getArgA(i); + LuaValue rb = L.stack[base + OpCode.getArgB(i)]; + LuaValue rc = rkc(L, k, base, i); + L.stack[ra + 1].set(rb); + LuaValue slot = fastGet(rb, rc); + if (slot != null) { + L.stack[ra].set(slot); + } else { + saveState(L, ci, pc); + finishGet(L, rb, rc, ra); + } + break; + } + + /* ---------------- 调用与返回 ---------------- */ + + case OpCode.OP_CALL: { + int ra = base + OpCode.getArgA(i); + int b = OpCode.getArgB(i); + if (b != 0) { + L.top = ra + b; + } + ci.savedpc = pc; + CallInfo newci = L.precall(ra, OpCode.getArgC(i) - 1); + if (newci == null) { + /* + * {@code updatetrap(ci)}:C 调用可能开/关过钩子。 + * 这一处必须有——{@code debug.sethook} 自己就是个 + * C 函数,它的 {@code settraps} 只改帧上的 {@code trap}, + * 主循环这个局部变量要靠这里才读得到。 + */ + trap = ci.trap; + break; // C 函数:已经跑完,结果在栈上 + } + ci = newci; + start = true; + continue outer; + } + case OpCode.OP_TAILCALL: { + int ra = base + OpCode.getArgA(i); + int b = OpCode.getArgB(i); + int nparams1 = OpCode.getArgC(i); + int delta = (nparams1 != 0) ? ci.nextraargs + nparams1 : 0; + if (b != 0) { + L.top = ra + b; + } else { + b = L.top - ra; + } + ci.savedpc = pc; + if (OpCode.getArgK(i) != 0) { + L.closeUpvals(base, null); + } + int n = L.pretailcall(ci, ra, b, delta); + if (n < 0) { + start = true; // Lua 函数:复用同一帧继续跑 + continue outer; + } + ci.func -= delta; + L.poscall(ci, n); // 里面会报返回钩子 + trap = ci.trap; // luaD_poscall 可能改了钩子 + if (ci.fresh) { + return; + } + ci = ci.previous; + ret = true; + continue outer; + } + case OpCode.OP_RETURN: { + int ra = base + OpCode.getArgA(i); + int n = OpCode.getArgB(i) - 1; + int nparams1 = OpCode.getArgC(i); + if (n < 0) { + n = L.top - ra; + } + ci.savedpc = pc; + if (OpCode.getArgK(i) != 0) { + ci.nres = n; + if (L.top < ci.top) { + L.top = ci.top; + } + L.closeUpvals(base, null); + } + if (nparams1 != 0) { + ci.func -= ci.nextraargs + nparams1; + } + L.top = ra + n; + L.poscall(ci, n); // 里面会报返回钩子 + trap = ci.trap; // luaD_poscall 可能改了钩子 + if (ci.fresh) { + return; + } + ci = ci.previous; + ret = true; + continue outer; + } + case OpCode.OP_RETURN0: { + if (L.hookmask != 0) { + // 有钩子就走正式的 poscall,让它把返回钩子报出去 + int ra = base + OpCode.getArgA(i); + L.top = ra; + ci.savedpc = pc; + L.poscall(ci, 0); + /* + * C 版在这里写 {@code trap = 1}。置位是为了让紧接着的 + * {@code luaG_tracecall}({@code goto ret} 之后)跑一下, + * 把上层帧的 {@code trap} 重新置 1——否则上层帧的 + * {@code trap} 早被 {@code luaG_traceexec} 清成 0 了, + * 返回之后就再也接不上行钩子。 + */ + trap = true; + } else { + int nres = ci.nresults; + L.ci = ci.previous; + L.top = base - 1; + L.ensureSlots(L.top + Math.max(nres, 0)); + while (nres > 0) { + L.stack[L.top++].setNil(); + nres--; + } + } + if (ci.fresh) { + return; + } + ci = ci.previous; + ret = true; + continue outer; + } + case OpCode.OP_RETURN1: { + if (L.hookmask != 0) { + int ra = base + OpCode.getArgA(i); + L.top = ra + 1; + ci.savedpc = pc; + L.poscall(ci, 1); + trap = true; // 同 OP_RETURN0 + } else { + int nres = ci.nresults; + L.ci = ci.previous; + if (nres == 0) { + L.top = base - 1; + } else { + int ra = base + OpCode.getArgA(i); + L.stack[base - 1].set(L.stack[ra]); + L.top = base; + L.ensureSlots(L.top + nres - 1); + while (nres > 1) { + L.stack[L.top++].setNil(); + nres--; + } + } + } + if (ci.fresh) { + return; + } + ci = ci.previous; + ret = true; + continue outer; + } + + /* ---------------- 循环 ---------------- */ + + case OpCode.OP_FORLOOP: { + int ra = base + OpCode.getArgA(i); + LuaValue counter = L.stack[ra + 1]; + if (counter.isInteger()) { + long count = counter.i; + if (count != 0) { // C 里是无符号比较,等价于「计数不为 0」 + long step = L.stack[ra + 2].i; + long idx = L.stack[ra].i + step; + L.stack[ra + 1].setInteger(count - 1); + L.stack[ra].setInteger(idx); + L.stack[ra + 3].setInteger(idx); + pc -= OpCode.getArgBx(i); + } + } else if (floatForLoop(L, ra)) { + pc -= OpCode.getArgBx(i); + } + break; + } + case OpCode.OP_FORPREP: { + saveState(L, ci, pc); + if (forPrep(L, base + OpCode.getArgA(i))) { + pc += OpCode.getArgBx(i) + 1; + } + break; + } + case OpCode.OP_TFORPREP: { + int ra = base + OpCode.getArgA(i); + ci.savedpc = pc; + L.newTbcUpval(ra + 3); + pc += OpCode.getArgBx(i); + // 落到紧随其后的 TFORCALL,再落到 TFORLOOP + i = code[pc++]; + doTforCall(L, ci, ra, i, pc); + i = code[pc++]; + if (doTforLoop(L, ra, i)) { + pc -= OpCode.getArgBx(i); + } + break; + } + case OpCode.OP_TFORCALL: { + int ra = base + OpCode.getArgA(i); + doTforCall(L, ci, ra, i, pc); + i = code[pc++]; + if (doTforLoop(L, ra, i)) { + pc -= OpCode.getArgBx(i); + } + break; + } + case OpCode.OP_TFORLOOP: { + int ra = base + OpCode.getArgA(i); + if (doTforLoop(L, ra, i)) { + pc -= OpCode.getArgBx(i); + } + break; + } + + /* ---------------- 闭包与可变参数 ---------------- */ + + case OpCode.OP_CLOSURE: { + int ra = base + OpCode.getArgA(i); + pushClosure(L, cl.p.p[OpCode.getArgBx(i)], cl.upvals, base, ra); + /* + * {@code checkGC(L, ra+1)}。这里 C 版没有那句 + * {@code L->top = ra+1}({@code OP_NEWTABLE} 有),但收集 + * 要在栈上跑 {@code __gc},{@code top} 得先落在活寄存器 + * 之上——照 {@code OP_NEWTABLE} 的做法抬一下。 + */ + if (L.gcDue()) { + int savedTop = L.top; + L.top = Math.max(savedTop, ra + 1); + L.maybeCollect(L); + L.top = savedTop; + } + break; + } + case OpCode.OP_VARARG: { + saveState(L, ci, pc); + LuaTm.getVarargs(L, ci, base + OpCode.getArgA(i), OpCode.getArgC(i) - 1); + break; + } + case OpCode.OP_VARARGPREP: { + ci.savedpc = pc; + LuaTm.adjustVarargs(L, OpCode.getArgA(i), ci, cl.p); + /* + * {@code ProtectNT} 后半句的 {@code updatetrap(ci)}。 + * 变参函数的 {@code luaG_tracecall} 返回的是 0(钩子推迟到这里 + * 才报),但它已经把帧上的 {@code trap} 置成 1 了,所以这里 + * 读回来正好是 1——上游就是靠这个「先关再开」把调用钩子挪到 + * 参数归位之后的。 + */ + trap = ci.trap; + if (trap) { + DebugLib.dHookCall(L, ci); + L.oldpc = 1; // 下一条指令按「新行」处理 + } + base = ci.func + 1; + break; + } + case OpCode.OP_EXTRAARG: { + throw L.runError("malformed opcode EXTRAARG"); + } + default: + throw L.runError("unknown opcode " + OpCode.getOpCode(i)); + } + } + } + } + + /* ================================================================ + * 取指令操作数 + * ================================================================ */ + + /** {@code RKC(i)}:k 位为 1 时取常量表,否则取寄存器 */ + private static LuaValue rkc(LuaState L, LuaValue[] k, int base, int i) { + if (OpCode.getArgK(i) != 0) { + return k[OpCode.getArgC(i)]; + } + return L.stack[base + OpCode.getArgC(i)]; + } + + /** + * {@code luaV_fastget} 的等价物。 + * + * @return 命中的槽(非 nil);未命中或 {@code t} 不是表时返回 {@code null} + */ + private static LuaValue fastGet(LuaValue t, LuaValue key) { + if (t.tt != Lua.TTABLE) { + return null; + } + LuaValue slot = t.asTable().get(key); + return (slot != null && !slot.isNil()) ? slot : null; + } + + /** + * 写路径用的「快速槽」,与 {@link #fastGet} 分开。 + *

+ * 分开的理由只有一条,但它是致命的:弱表上 {@code LuaTable.get} 返回的是 + * 副本——槽里存的是 {@code WeakRef},不能直接交到 VM 手里,所以读出来 + * 要解一层。往这个副本上 {@code set} 等于什么都没写。实测出来的症状很怪: + * {@code t.f = g} 之后 {@code t.f} 还是 {@code f},因为第二次赋值被写进了 + * 一个马上就被丢掉的临时对象。 + *

+ * 所以弱表一律返回 {@code null},把写入交给 {@link #finishSet} 走 + * {@code LuaTable.set}——包装、插入、失效清扫都在那一条路上。 + */ + private static LuaValue fastSet(LuaValue t, LuaValue key) { + if (t.tt != Lua.TTABLE) { + return null; + } + LuaTable tb = t.asTable(); + if (tb.isWeak()) { + return null; + } + LuaValue slot = tb.get(key); + return (slot != null && !slot.isNil()) ? slot : null; + } + + /** {@code Protect}:出错前把 pc 与 top 修正好 */ + private static void saveState(LuaState L, CallInfo ci, int pc) { + ci.savedpc = pc; + L.top = ci.top; + } + + /* ================================================================ + * luaV_finishOp:补完被 yield 打断的那条指令 + * ================================================================ */ + + /** + * {@code lvm.c: luaV_finishOp}。 + *

+ * 一条指令执行到一半去调了 Lua 代码(元方法、for 迭代器、普通调用), + * 结果那里面 yield 了——协程挂起时这条指令还没跑完。等 resume 回来, + * {@code unroll} 会先调这里把剩下的半步补上,再重新进入解释器。 + *

+ * 补的方式全靠「结果就压在栈顶」这一点:被调用者返回时 + * {@code luaD_poscall} 已经把结果搬好,这里要么把它写进目标寄存器 + * ({@code OP_GETTABLE} 之类),要么拿它决定跳不跳转(比较类指令), + * 要么把 pc 回退一格重跑({@code OP_CLOSE} / {@code OP_RETURN} 这种要 + * 「关一批变量、yield、再关下一批」的)。 + *

+ * 调用方保证 {@code L.ci} 是 Lua 帧,且 {@code savedpc} 指向被中断指令的 + * 下一条(主循环里 {@code pc++} 在取指时就做了),所以 + * {@code code[savedpc - 1]} 才是被中断的那条。 + */ + static void finishOp(LuaState L) { + CallInfo ci = L.ci; + int base = ci.func + 1; + int inst = ((LClosure) L.stack[ci.func].o).p.code[ci.savedpc - 1]; + int op = OpCode.getOpCode(inst); + switch (op) { + case OpCode.OP_MMBIN: + case OpCode.OP_MMBINI: + case OpCode.OP_MMBINK: { + // 结果写回触发元方法的那条算术指令的目标寄存器 + int prev = ((LClosure) L.stack[ci.func].o).p.code[ci.savedpc - 2]; + L.stack[base + OpCode.getArgA(prev)].set(L.stack[--L.top]); + break; + } + case OpCode.OP_UNM: + case OpCode.OP_BNOT: + case OpCode.OP_LEN: + case OpCode.OP_GETTABUP: + case OpCode.OP_GETTABLE: + case OpCode.OP_GETI: + case OpCode.OP_GETFIELD: + case OpCode.OP_SELF: { + L.stack[base + OpCode.getArgA(inst)].set(L.stack[--L.top]); + break; + } + case OpCode.OP_LT: + case OpCode.OP_LE: + case OpCode.OP_LTI: + case OpCode.OP_LEI: + case OpCode.OP_GTI: + case OpCode.OP_GEI: + case OpCode.OP_EQ: { + // 注意 OP_EQI / OP_EQK 不会 yield,所以不在这一组里 + boolean res = L.stack[L.top - 1].toBoolean(); + L.top--; + if (ci.leq) { // 拿 __lt 顶替缺失的 __le,结果要取反 + ci.leq = false; + res = !res; + } + int next = ((LClosure) L.stack[ci.func].o).p.code[ci.savedpc]; + if (OpCode.getOpCode(next) == OpCode.OP_JMP + && res != (OpCode.getArgK(inst) != 0)) { + ci.savedpc++; // 条件不成立,跳过紧跟的 JMP + } + break; + } + case OpCode.OP_CONCAT: { + /* + * 拼接是「一批一批来」的:一次把栈顶若干个拼成一个,再接着拼。 + * yield 发生在调 __concat 的那一刻,所以恢复时要先把元方法的结果 + * 放回它该在的位置(倒数第二格),把 top 收一格,然后把剩下那 + * 几个继续拼完——可能又 yield,那就再来一遍。 + */ + int t = L.top - 1; // 调 luaT_tryconcatTM 时的栈顶 + int total = t - 1 - (base + OpCode.getArgA(inst)); // 还没拼的元素数 + L.stack[t - 2].set(L.stack[t]); + L.top = t - 1; + concat(L, total); + break; + } + case OpCode.OP_CLOSE: { + ci.savedpc--; // 重跑,接着关剩下的变量 + break; + } + case OpCode.OP_RETURN: { + // 先按 nres 把 top 摆正,再重跑,让 return 这一整套走完 + L.top = base + OpCode.getArgA(inst) + ci.nres; + ci.savedpc--; + break; + } + default: + // 剩下的 OP_TFORCALL / OP_CALL / OP_TAILCALL / OP_SET* 没有半步要补: + // 结果已经由 poscall 直接写进目标寄存器了 + break; + } + } + + /* ================================================================ + * 算术(成功返回 true,调用方据此跳过后面的 MMBIN) + * ================================================================ */ + + /** {@code op_arithI}:第二个操作数是立即整数 */ + private static boolean arithImm(LuaState L, int i, int base, int op) { + LuaValue v1 = L.stack[base + OpCode.getArgB(i)]; + LuaValue res = Arith.rawArith(L, op, v1, LuaValue.ofInteger(OpCode.getArgSC(i))); + if (res == null) { + return false; + } + L.stack[base + OpCode.getArgA(i)].set(res); + return true; + } + + /** {@code op_arithK} / {@code op_arithfK} / {@code op_bitwiseK}:第二个操作数在常量表 */ + private static boolean arithK(LuaState L, int i, int base, LuaValue[] k, int op) { + LuaValue v1 = L.stack[base + OpCode.getArgB(i)]; + LuaValue res = Arith.rawArith(L, op, v1, k[OpCode.getArgC(i)]); + if (res == null) { + return false; + } + L.stack[base + OpCode.getArgA(i)].set(res); + return true; + } + + /** {@code op_arith} / {@code op_arithf} / {@code op_bitwise}:两个操作数都在寄存器 */ + private static boolean arith(LuaState L, int i, int base, int op) { + LuaValue v1 = L.stack[base + OpCode.getArgB(i)]; + LuaValue v2 = L.stack[base + OpCode.getArgC(i)]; + LuaValue res = Arith.rawArith(L, op, v1, v2); + if (res == null) { + return false; + } + L.stack[base + OpCode.getArgA(i)].set(res); + return true; + } + + /* ================================================================ + * 比较 + * ================================================================ */ + + /** {@code op_order}:{@code event} 决定是 {@code <}(TM_LT)还是 {@code <=}(TM_LE) */ + private static boolean order(LuaState L, CallInfo ci, int pc, int i, int base, int event) { + LuaValue ra = L.stack[base + OpCode.getArgA(i)]; + LuaValue rb = L.stack[base + OpCode.getArgB(i)]; + boolean lt = (event == Tm.TM_LT); + if (ra.isInteger() && rb.isInteger()) { + return lt ? (ra.i < rb.i) : (ra.i <= rb.i); + } + if (ra.isNumber() && rb.isNumber()) { + return lt ? ltNum(ra, rb) : leNum(ra, rb); + } + if (ra.isString() && rb.isString()) { + int c = lStrcmp(ra.asString(), rb.asString()); + return lt ? (c < 0) : (c <= 0); + } + saveState(L, ci, pc); + return LuaTm.callOrderTm(L, ra, rb, event); + } + + /** + * {@code luaV_lessthan}:给库函数用的「与 VM 完全一致」的小于。 + *

+ * {@code math.max/min} 走的是 {@code lua_compare(LUA_OPLT)},而不是自己 + * 比数字——所以 {@code math.max("a","b")} 是 {@code "b"}(走字符串比较), + * 而 {@code math.max(1,"b")} 报的是 + * {@code attempt to compare number with string}(走 {@code __lt} 失败分支)。 + * 只比数字会把这两条都做错。 + */ + static boolean lessThan(LuaState L, LuaValue a, LuaValue b) { + if (a.isInteger() && b.isInteger()) { + return a.i < b.i; + } + if (a.isNumber() && b.isNumber()) { + return ltNum(a, b); + } + if (a.isString() && b.isString()) { + return lStrcmp(a.asString(), b.asString()) < 0; + } + return LuaTm.callOrderTm(L, a, b, Tm.TM_LT); + } + + /** {@code LTnum} */ + static boolean ltNum(LuaValue l, LuaValue r) { + if (l.isInteger()) { + long li = l.i; + if (r.isInteger()) { + return li < r.i; + } + return ltIntFloat(li, r.n); + } + double lf = l.n; + if (r.isFloat()) { + return lf < r.n; + } + return ltFloatInt(lf, r.i); + } + + /** {@code LEnum} */ + static boolean leNum(LuaValue l, LuaValue r) { + if (l.isInteger()) { + long li = l.i; + if (r.isInteger()) { + return li <= r.i; + } + return leIntFloat(li, r.n); + } + double lf = l.n; + if (r.isFloat()) { + return lf <= r.n; + } + return leFloatInt(lf, r.i); + } + + /** {@code l_intfitsf}:{@code i} 能否无损转成 double —— 判据是 {@code |i| <= 2^53} */ + private static boolean intFitsF(long i) { + return i >= -(1L << 53) && i <= (1L << 53); + } + + /** {@code LTintfloat}:{@code i < f} */ + private static boolean ltIntFloat(long i, double f) { + if (intFitsF(i)) { + return (double) i < f; + } + Long fi = Arith.fltToInteger(f, Arith.F2I_CEIL); + return (fi != null) ? (i < fi) : (f > 0); + } + + /** {@code LEintfloat}:{@code i <= f} */ + private static boolean leIntFloat(long i, double f) { + if (intFitsF(i)) { + return (double) i <= f; + } + Long fi = Arith.fltToInteger(f, Arith.F2I_FLOOR); + return (fi != null) ? (i <= fi) : (f > 0); + } + + /** {@code LTfloatint}:{@code f < i} */ + private static boolean ltFloatInt(double f, long i) { + if (intFitsF(i)) { + return f < (double) i; + } + Long fi = Arith.fltToInteger(f, Arith.F2I_FLOOR); + return (fi != null) ? (fi < i) : (f < 0); + } + + /** {@code LEfloatint}:{@code f <= i} */ + private static boolean leFloatInt(double f, long i) { + if (intFitsF(i)) { + return f <= (double) i; + } + Long fi = Arith.fltToInteger(f, Arith.F2I_CEIL); + return (fi != null) ? (fi <= i) : (f < 0); + } + + /** + * {@code l_strcmp}:按字节比较,与 C locale 下的 {@code strcoll} 一致。 + *

+ * Lua 字符串可以含 {@code \0},所以比较要分段:每段比到 {@code \0} 或串尾, + * 相等则跳过 {@code \0} 继续下一段。段长不同时短的更小。 + *

+ * 每个字节都要当 {@code unsigned char} 比。C99 对 {@code strcoll} + * 的规定是「返回值符号由第一对不相等字节之差决定,两个字节都按 + * {@code unsigned char} 解释」。Java 的 {@code byte} 是有符号的,直接相减会 + * 把 {@code 0xFF} 当成 {@code -1}:于是 {@code "\xff" < "abc"} 从 + * false 翻成 true,{@code table.sort} 的结果也跟着整体反转。 + * 本机实测 {@code strcoll("\xff","abc") == 158 > 0}。 + */ + static int lStrcmp(LuaString a, LuaString b) { + byte[] s1 = a.bytes; + byte[] s2 = b.bytes; + int p1 = 0; + int p2 = 0; + int rl1 = s1.length; + int rl2 = s2.length; + for (;;) { + int z1 = firstZero(s1, p1); + int z2 = firstZero(s2, p2); + int seg1 = (z1 < 0) ? rl1 : (z1 - p1); + int seg2 = (z2 < 0) ? rl2 : (z2 - p2); + int n = Math.min(seg1, seg2); + for (int j = 0; j < n; j++) { + // unsigned char:见上面 javadoc 里 strcoll 的规定 + int d = (s1[p1 + j] & 0xFF) - (s2[p2 + j] & 0xFF); + if (d != 0) { + return d; + } + } + if (seg1 != seg2) { + return seg1 < seg2 ? -1 : 1; + } + if (z2 < 0) { + return (z1 < 0) ? 0 : 1; + } + if (z1 < 0) { + return -1; + } + p1 = z1 + 1; + p2 = z2 + 1; + rl1 -= seg1 + 1; + rl2 -= seg2 + 1; + } + } + + private static int firstZero(byte[] s, int from) { + for (int i = from; i < s.length; i++) { + if (s[i] == 0) { + return i; + } + } + return -1; + } + + /* ================================================================ + * 相等 + * ================================================================ */ + + /** {@code luaV_equalobj} */ + static boolean equalObj(LuaState L, LuaValue t1, LuaValue t2) { + if (t1.tt != t2.tt) { + if (t1.baseType() != t2.baseType() || t1.baseType() != Lua.TNUMBER) { + return false; + } + // 两个变体不同的数字:只有都能取到整数值且相等时才相等 + Long i1 = Arith.toIntegerNs(t1); + Long i2 = Arith.toIntegerNs(t2); + return i1 != null && i2 != null && i1.longValue() == i2.longValue(); + } + switch (t1.tt) { + case Lua.TNIL: + return true; + case Lua.TBOOLEAN: + return t1.i == t2.i; + case Lua.VNUMINT: + return t1.i == t2.i; + case Lua.VNUMFLT: + return t1.n == t2.n; + case Lua.VSHRSTR: + case Lua.VLNGSTR: + return t1.asString().equals(t2.asString()); + case Lua.TTABLE: { + if (t1.o == t2.o) { + return true; + } + LuaValue tm = LuaTm.fastTm(t1.asTable().getMetatable(), Tm.TM_EQ, L); + if (tm == null) { + tm = LuaTm.fastTm(t2.asTable().getMetatable(), Tm.TM_EQ, L); + } + if (tm == null) { + return false; + } + int t = L.top; + L.ensureSlots(t + 1); + LuaTm.callTmRes(L, tm, t1, t2, t); + return L.stack[t].toBoolean(); + } + default: + return t1.o == t2.o; + } + } + + /* ================================================================ + * 表访问的慢路径 + * ================================================================ */ + + /** + * {@code luaV_gettable}:库函数用的表访问(快路径 + {@code __index} 慢路径)。 + *

+ * 与 {@code OP_GETTABLE} 走同一套代码,{@code ipairs} 之类的库函数才会 + * 和脚本里的 {@code t[i]} 表现得一模一样。 + */ + static void getTable(LuaState L, LuaValue t, LuaValue key, int valIdx) { + LuaValue slot = fastGet(t, key); + if (slot != null) { + L.stack[valIdx].set(slot); + return; + } + finishGet(L, t, key, valIdx); + } + + /** + * {@code luaV_settable}:库函数用的表写入(快路径 + {@code __newindex} 慢路径)。 + *

+ * 与 {@link #getTable} 成对,{@code table.insert/remove/move/sort} 都靠它 + * 才能作用于「带 {@code __newindex} 的对象」——C 的 {@code lua_seti} 走的 + * 是 {@code luaV_fastset + luaV_finishset},不是 {@code luaH_setint}, + * 所以元方法是会被调用的。写成裸写会让 {@code table.insert(ud, v)} 静默地 + * 什么也不做。 + */ + static void setTable(LuaState L, LuaValue t, LuaValue key, LuaValue val) { + LuaValue slot = fastSet(t, key); + if (slot != null) { + slot.set(val); + return; + } + finishSet(L, t, key, val); + } + + /** {@code luaV_finishget} */ + static void finishGet(LuaState L, LuaValue t, LuaValue key, int valIdx) { + for (int loop = 0; loop < MAX_TAG_LOOP; loop++) { + LuaValue tm; + if (t.tt == Lua.TTABLE) { + tm = LuaTm.fastTm(t.asTable().getMetatable(), Tm.TM_INDEX, L); + if (tm == null) { + L.stack[valIdx].setNil(); + return; + } + } else { + tm = LuaTm.getTmByObj(L, t, Tm.TM_INDEX); + if (tm == null) { + throw L.typeError(t, "index"); + } + } + if (tm.isFunction()) { + LuaTm.callTmRes(L, tm, t, key, valIdx); + return; + } + t = tm; // __index 是表:继续在它上面查 + LuaValue slot = fastGet(t, key); + if (slot != null) { + L.stack[valIdx].set(slot); + return; + } + } + throw L.runError("'__index' chain too long; possible loop"); + } + + /** {@code luaV_finishset} */ + private static void finishSet(LuaState L, LuaValue t, LuaValue key, LuaValue val) { + for (int loop = 0; loop < MAX_TAG_LOOP; loop++) { + LuaValue tm; + if (t.tt == Lua.TTABLE) { + tm = LuaTm.fastTm(t.asTable().getMetatable(), Tm.TM_NEWINDEX, L); + if (tm == null) { + t.asTable().set(key, val); + return; + } + } else { + tm = LuaTm.getTmByObj(L, t, Tm.TM_NEWINDEX); + if (tm == null) { + throw L.typeError(t, "index"); + } + } + if (tm.isFunction()) { + LuaTm.callTm(L, tm, t, key, val); + return; + } + t = tm; // __newindex 是表:继续在它上面写 + LuaValue slot = fastGet(t, key); + if (slot != null) { + slot.set(val); + return; + } + } + throw L.runError("'__newindex' chain too long; possible loop"); + } + + /* ================================================================ + * 长度 + * ================================================================ */ + + /** {@code luaV_objlen} */ + static void objLen(LuaState L, int raIdx, LuaValue rb) { + LuaValue tm = null; + switch (rb.tt) { + case Lua.TTABLE: { + LuaTable h = rb.asTable(); + tm = LuaTm.fastTm(h.getMetatable(), Tm.TM_LEN, L); + if (tm == null) { + L.stack[raIdx].setInteger(h.length()); + return; + } + break; + } + case Lua.VSHRSTR: + case Lua.VLNGSTR: + L.stack[raIdx].setInteger(rb.asString().length()); + return; + default: + tm = LuaTm.getTmByObj(L, rb, Tm.TM_LEN); + if (tm == null) { + throw L.typeError(rb, "get length of"); + } + break; + } + LuaTm.callTmRes(L, tm, rb, rb, raIdx); + } + + /* ================================================================ + * 拼接 + * ================================================================ */ + + private static boolean cvt2str(LuaValue o) { + return o.isNumber(); + } + + /** {@code tostring(L,o)}:不是字符串但可转换的(数字)就地转成字符串 */ + private static boolean toStr(LuaValue o) { + if (o.isString()) { + return true; + } + if (cvt2str(o)) { + o.setString(LuaString.latin1(numberToString(o))); + return true; + } + return false; + } + + private static boolean isEmptyStr(LuaValue o) { + return o.isString() && o.asString().length() == 0; + } + + /** + * {@code luaV_concat}:把栈顶 {@code total} 个值从右往左折叠成一个。 + *

+ * 每趟取末尾 {@code n} 个合成 1 个,结果落在「收缩后的栈顶」前一格; + * 反复直到只剩一个。之所以从右往左,是因为左端还可能有未拼接的操作数。 + */ + static void concat(LuaState L, int total) { + if (total == 1) { + return; + } + do { + int t = L.top; + int n = 2; + LuaValue v1 = L.stack[t - 2]; + LuaValue v2 = L.stack[t - 1]; + if (!(v1.isString() || cvt2str(v1)) || !toStr(v2)) { + LuaTm.tryConcatTm(L); + } else if (isEmptyStr(v2)) { + toStr(v1); // 结果就是第一个操作数 + } else if (isEmptyStr(v1)) { + L.stack[t - 2].set(v2); + } else { + long tl = v2.asString().length(); + for (n = 1; n < total && toStr(L.stack[t - n - 1]); n++) { + tl += L.stack[t - n - 1].asString().length(); + } + byte[] buff = new byte[(int) tl]; + copyToBuff(L, t, n, buff); + L.stack[t - n].setString(L.newString(buff)); + } + total -= n - 1; + L.top -= n - 1; + } while (total > 1); + } + + /** {@code copy2buff}:把栈顶往下 {@code n} 个字符串拷进 {@code buff} */ + private static void copyToBuff(LuaState L, int top, int n, byte[] buff) { + int tl = 0; + int idx = top - n; + do { + LuaString st = L.stack[idx].asString(); + System.arraycopy(st.bytes, 0, buff, tl, st.bytes.length); + tl += st.bytes.length; + idx++; + } while (--n > 0); + } + + /* ================================================================ + * 数值 for + * ================================================================ */ + + /** + * {@code forlimit}:把循环上界收敛成一个整数。 + * + * @return 循环一次都不该跑时返回 true + */ + private static boolean forLimit(LuaState L, long init, LuaValue lim, long[] p, long step) { + Long conv = toInteger(lim, step < 0 ? Arith.F2I_CEIL : Arith.F2I_FLOOR); + if (conv == null) { + Double flim = toNumber(lim); + if (flim == null) { + throw L.forError(lim, "limit"); + } + if (0 < flim) { // 太大 + if (step < 0) { + return true; // 初值必然小于它 + } + p[0] = Long.MAX_VALUE; + } else { // 太小 + if (step > 0) { + return true; + } + p[0] = Long.MIN_VALUE; + } + } else { + p[0] = conv; + } + return step > 0 ? init > p[0] : init < p[0]; + } + + /** + * {@code forprep}:准备数值 for 循环。 + *

+ * 整数循环把「上界」换成「剩余次数」——因为 {@code for i = mininteger, maxinteger} + * 这种循环的次数本身也塞得进 int64,而边界比较不行。这个替换是 Lua 能正确跑 + * 到 int64 边界的原因。 + * + * @return 循环一次都不跑时返回 true + */ + private static boolean forPrep(LuaState L, int ra) { + LuaValue pinit = L.stack[ra]; + LuaValue plimit = L.stack[ra + 1]; + LuaValue pstep = L.stack[ra + 2]; + if (pinit.isInteger() && pstep.isInteger()) { + long init = pinit.i; + long step = pstep.i; + if (step == 0) { + throw L.runError("'for' step is zero"); + } + L.stack[ra + 3].setInteger(init); + long[] limit = new long[1]; + if (forLimit(L, init, plimit, limit, step)) { + return true; + } + long count; + if (step > 0) { + count = limit[0] - init; // 无符号减法,位模式等同有符号 + if (step != 1) { + count = Long.divideUnsigned(count, step); + } + } else { + count = init - limit[0]; + // -(step+1) 是为了避开对 mininteger 取负 + count = Long.divideUnsigned(count, (-(step + 1)) + 1); + } + L.stack[ra + 1].setInteger(count); + } else { + Double initD = toNumber(pinit); + Double limitD = toNumber(plimit); + Double stepD = toNumber(pstep); + if (limitD == null) { + throw L.forError(plimit, "limit"); + } + if (stepD == null) { + throw L.forError(pstep, "step"); + } + if (initD == null) { + throw L.forError(pinit, "initial value"); + } + double init = initD; + double limit = limitD; + double step = stepD; + if (step == 0) { + throw L.runError("'for' step is zero"); + } + if (step > 0 ? (limit < init) : (init < limit)) { + return true; + } + L.stack[ra + 1].setFloat(limit); + L.stack[ra + 2].setFloat(step); + L.stack[ra].setFloat(init); + L.stack[ra + 3].setFloat(init); + } + return false; + } + + /** {@code floatforloop} */ + private static boolean floatForLoop(LuaState L, int ra) { + if (!L.stack[ra + 2].isFloat()) { + return false; + } + double step = L.stack[ra + 2].n; + double limit = L.stack[ra + 1].n; + double idx = L.stack[ra].n + step; + if (step > 0 ? (idx <= limit) : (limit <= idx)) { + L.stack[ra].setFloat(idx); + L.stack[ra + 3].setFloat(idx); + return true; + } + return false; + } + + /* ================================================================ + * 泛型 for + * ================================================================ */ + + /** + * {@code OP_TFORCALL} 的实体:把「迭代器函数 / 状态 / 控制变量」复制到 + * {@code ra+4} 起,然后调用。 + */ + private static void doTforCall(LuaState L, CallInfo ci, int ra, int i, int pc) { + for (int j = 0; j < 3; j++) { + L.stack[ra + 4 + j].set(L.stack[ra + j]); + } + L.top = ra + 7; + ci.savedpc = pc; + L.call(ra + 4, OpCode.getArgC(i)); + } + + /** + * {@code OP_TFORLOOP} 的实体。 + * + * @return 是否继续循环(调用方据此 {@code pc -= Bx}) + */ + private static boolean doTforLoop(LuaState L, int ra, int i) { + if (!L.stack[ra + 4].isNil()) { + L.stack[ra + 2].set(L.stack[ra + 4]); + return true; + } + return false; + } + + /* ================================================================ + * 闭包 + * ================================================================ */ + + /** {@code pushclosure} */ + private static void pushClosure(LuaState L, Proto p, UpVal[] encup, int base, int ra) { + LClosure ncl = new LClosure(p); + L.stack[ra].setObject(Lua.VLIF, ncl); + for (int i = 0; i < p.sizeupvalues; i++) { + Proto.Upvaldesc uv = p.upvalues[i]; + if (uv.instack) { + ncl.upvals[i] = L.findUpval(base + uv.idx); + } else { + ncl.upvals[i] = encup[uv.idx]; + } + } + } + + /* ================================================================ + * 转换 + * ================================================================ */ + + /** {@code l_strton}:字符串能不能整个解释成一个数字 */ + static LuaValue strToNum(LuaValue o) { + if (!o.isString()) { + return null; + } + String s = new String(o.asString().bytes, StandardCharsets.ISO_8859_1); + return Numeral.str2num(s); + } + + /** {@code luaV_tointegerns},带 mode */ + static Long toIntegerNs(LuaValue v, int mode) { + if (v.isFloat()) { + return Arith.fltToInteger(v.n, mode); + } + if (v.isInteger()) { + return v.i; + } + return null; + } + + /** {@code luaV_tointeger}:含字符串转换 */ + static Long toInteger(LuaValue v, int mode) { + LuaValue n = strToNum(v); + if (n != null) { + v = n; + } + return toIntegerNs(v, mode); + } + + /** {@code tonumberns}:不含字符串转换 */ + static Double toNumberNs(LuaValue v) { + if (v.isInteger()) { + return (double) v.i; + } + if (v.isFloat()) { + return v.n; + } + return null; + } + + /** {@code luaV_tonumber_}:含字符串转换 */ + static Double toNumber(LuaValue v) { + if (v.isInteger()) { + return (double) v.i; + } + if (v.isFloat()) { + return v.n; + } + LuaValue n = strToNum(v); + if (n == null) { + return null; + } + return n.isInteger() ? (double) n.i : n.n; + } + + /** + * {@code luaO_tostring} 的数字格式化:{@code %.14g},看起来像整数的补 {@code .0}。 + *

+ * 整值浮点必须输出成 {@code 1.0} 而不是 {@code 1}——Lua 靠这个区分 + * {@code print(1)} 与 {@code print(1.0)},也靠它保住 {@code math.type} + * 的可观测差异。 + */ + static String numberToString(LuaValue v) { + if (v.isInteger()) { + return Long.toString(v.i); + } + double d = v.n; + if (Double.isNaN(d)) { + return "nan"; + } + if (d == Double.POSITIVE_INFINITY) { + return "inf"; + } + if (d == Double.NEGATIVE_INFINITY) { + return "-inf"; + } + String s = formatG(d, 14); + if (looksLikeInt(s)) { + s = s + ".0"; + } + return s; + } + + /** + * C 的 {@code %.

g} —— 与 {@code string.format} 的 {@code %g} 共用 + * {@link StringLib#generalDigits},理由写在那边。 + *

+ * 不要用 Java 的 {@code %.14g}。 两个坑,第二个是实测撞出来的: + *

    + *
  1. C 的 {@code %g} 要去掉小数部分末尾的零、没有小数部分时连小数点一起 + * 去掉;Java 的 {@code %g} 两者都不做 + * ({@code String.format("%.14g", 1.5)} 给 {@code 1.5000000000000}, + * C 给 {@code 1.5})。
  2. + *
  3. Java 的 {@code %f} / {@code %e} 是拿 {@code Double.toString} 的 + * 最短表示再做一次四舍五入,等于连舍两次。 + * {@code math.sqrt(math.maxinteger)} 的真值是 + * {@code 3037000499.976049900054931640625},最短表示是 + * {@code 3037000499.97605};按真值取 14 位有效数字是 + * {@code 3037000499.976},按最短表示取(末位 5 进一)就成了 + * {@code 3037000499.9761}。参考实现给的是前者。
  4. + *
+ * 所以走 {@link java.math.BigDecimal}:{@code new BigDecimal(double)} 是 + * 精确的二进制展开,一次 {@code setScale} 就舍到位。 + */ + static String formatG(double d, int p) { + if (Double.isNaN(d)) { + return "nan"; + } + if (d == Double.POSITIVE_INFINITY) { + return "inf"; + } + if (d == Double.NEGATIVE_INFINITY) { + return "-inf"; + } + String s = StringLib.generalDigits(Math.abs(d), p, false); + // 符号单独加:BigDecimal 没有 -0.0,tostring(-0.0) 得是 "-0.0" + return (Double.doubleToRawLongBits(d) < 0) ? "-" + s : s; + } + + /** {@code buff[strspn(buff, "-0123456789")] == '\0'} */ + private static boolean looksLikeInt(String s) { + int i = 0; + while (i < s.length()) { + char c = s.charAt(i); + if (c == '-' || (c >= '0' && c <= '9')) { + i++; + } else { + break; + } + } + return i == s.length(); + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/lua/Zio.java b/src/main/java/com/github/esrrhs/fakescript/lua/Zio.java new file mode 100644 index 0000000..a827685 --- /dev/null +++ b/src/main/java/com/github/esrrhs/fakescript/lua/Zio.java @@ -0,0 +1,75 @@ +package com.github.esrrhs.fakescript.lua; + +/** + * 输入流 —— 对应 {@code lzio.c} / {@code lzio.h} 的 {@code Zio}。 + *

+ * C 版为了支持任意来源(文件、字符串、自定义 reader)做了一层带缓冲的 reader + * 抽象。这里保留同样的形状({@code getc()} 一次给一个字节,读完给 EOZ), + * 底层有两种承载: + *

    + *
  • 一次性给全:{@code load} 一个字符串、{@code loadfile} 一个文件, + * 拿到的都是完整字节数组;
  • + *
  • 惰性:{@code load} 一个读函数。这一种是必需的,不是优化—— + * 读函数是边解析边调的,C 的 {@code luaZ_fill} 只在当前那批耗尽 + * 时才去要下一批。改成「先一股脑读完再解析」,遇到永不返回 nil 的读 + * 函数就会一直累积下去(实测直接把堆撑爆),而 C 那边早就因为语法错误 + * 或读到一个完整 chunk 停了。
  • + *
+ *

+ * 注意字节是无符号的。{@code byte} 在 Java 里有符号,取出来必须 + * {@code & 0xFF},否则含高位字节的 UTF-8 会变成负数,与 EOZ 的判断混在一起。 + */ +public final class Zio { + + /** 流结束,对应 {@code EOZ} */ + public static final int EOZ = -1; + + /** + * 惰性的字节来源。 + *

+ * 返回 {@code null} 或长度为 0 都表示流结束——C 判定的是 + * {@code size == 0}({@code luaZ_fill}),空串与 nil 是一回事。 + */ + public interface Source { + byte[] read(); + } + + /** 惰性来源;一次性给全时为 {@code null} */ + private final Source src; + + /** 当前这批字节 */ + private byte[] buf; + + private int p; + + public Zio(byte[] data) { + this.src = null; + this.buf = data; + this.p = 0; + } + + public Zio(Source src) { + this.src = src; + this.buf = null; + this.p = 0; + } + + /** {@code zgetc}:取下一个字节(0..255),读完返回 {@link #EOZ} */ + public int getc() { + for (;;) { + if (buf != null && p < buf.length) { + return buf[p++] & 0xFF; + } + if (src == null) { + return EOZ; + } + byte[] n = src.read(); + if (n == null || n.length == 0) { + buf = null; + return EOZ; + } + buf = n; + p = 0; + } + } +} diff --git a/src/main/java/com/github/esrrhs/fakescript/mybison.java b/src/main/java/com/github/esrrhs/fakescript/mybison.java deleted file mode 100644 index 8c2996f..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/mybison.java +++ /dev/null @@ -1,137 +0,0 @@ -package com.github.esrrhs.fakescript; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; - -import com.github.esrrhs.fakescript.syntree.func_desc_node; -import com.github.esrrhs.fakescript.syntree.syntree_node; - -public class mybison -{ - private fake m_f; - private String m_filename = ""; - private String m_packagename = ""; - private ArrayList m_includelist = new ArrayList(); - private HashSet m_struct_list = new HashSet(); - private HashMap m_constmap = new HashMap(); - private ArrayList m_funclist = new ArrayList(); - private Yylex m_j; - private String m_error; - private int m_errorline; - - public mybison(fake f, Yylex j) - { - m_f = f; - m_j = j; - } - - public fake get_fake() - { - return m_f; - } - - public String get_error() - { - return m_error; - } - - public int get_error_line() - { - return m_errorline; - } - - public void lexer_error(String msg, int lineno, String text) - { - m_error = String.format("%s at line(%d) near(%s)", msg, lineno, text); - m_errorline = lineno; - } - - public Yylex get_jflex() - { - return m_j; - } - - public String get_filename() - { - return m_filename; - } - - public void set_filename(String filename) - { - m_filename = filename; - } - - public String get_package() - { - return m_packagename; - } - - public void set_package(String packagename) - { - m_packagename = packagename; - } - - public void add_include(String includefile) - { - // 加入include list,等待解析完再统一挨个include - m_includelist.add(includefile); - } - - public ArrayList get_include_list() - { - return m_includelist; - } - - public void add_struct_desc(String structname) - { - m_struct_list.add(structname); - } - - boolean is_have_struct(String structname) - { - return m_struct_list.contains(structname); - } - - public HashMap get_const_map() - { - return m_constmap; - } - - public void add_const_desc(String name, syntree_node node) - { - m_constmap.put(name, node); - if (m_f.cfg.open_debug_log != 0) - { - types.log(m_f, "add_const_desc " + name + " " + node.dump(0)); - } - } - - public void add_func_desc(func_desc_node p) - { - types.log(m_f, "add_func_desc " + p.m_funcname); - if (m_f.cfg.open_debug_log != 0) - { - types.log(m_f, "func " + p.m_funcname + " dump " + p.dump(0)); - } - m_funclist.add(p); - } - - public ArrayList get_func_list() - { - return m_funclist; - } - - public boolean is_have_func(String funcname) - { - for (int i = 0; i < (int) m_funclist.size(); i++) - { - func_desc_node p = m_funclist.get(i); - if (p.m_funcname.equals(funcname)) - { - return true; - } - } - return false; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/optimizer.java b/src/main/java/com/github/esrrhs/fakescript/optimizer.java deleted file mode 100644 index a46c84e..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/optimizer.java +++ /dev/null @@ -1,964 +0,0 @@ -package com.github.esrrhs.fakescript; - -import java.util.ArrayList; -import java.util.HashSet; - -class opt_ins_addr -{ - public opt_ins_addr(int offset, long addr) - { - this.offset = offset; - this.addr = addr; - } - - public int offset; - public long addr; -} - -class opt_ins -{ - public int offset; - public int size; - public int code; - public ArrayList read = new ArrayList(); - public ArrayList write = new ArrayList(); - public ArrayList src = new ArrayList(); - public ArrayList dst = new ArrayList(); - - public String toString() - { - return "{" + types.OpCodeStr(code) + ", size:" + size + ", read:" + read + ", write:" + write + "}"; - } -} - -class optimizer -{ - private fake m_f; - private boolean isopt; - private ArrayList inslist = new ArrayList(); - private boolean isopen; - - public optimizer(fake f) - { - m_f = f; - } - - public void open() - { - isopen = true; - } - - public void close() - { - isopen = false; - } - - public void optimize(func_binary fb) throws Exception - { - if (!isopen) - { - return; - } - - if (m_f.cfg.open_debug_log != 0) - { - types.log(m_f, "before %s", fb.dump(-1)); - } - while (true) - { - isopt = false; - - trans_inslist(fb); - - if (!isopt) - { - optimize_write_assign(fb); - } - if (!isopt) - { - optimize_write_write(fb); - } - if (!isopt) - { - optimize_no_use_const(fb); - } - if (!isopt) - { - optimize_no_use_container(fb); - } - if (!isopt) - { - optimize_no_use_stack(fb); - } - if (!isopt) - { - optimize_stack_pos(fb); - } - - if (isopt) - { - if (m_f.cfg.open_debug_log != 0) - { - types.log(m_f, "optimize %s", fb.dump(-1)); - } - } - else - { - break; - } - } - } - - private void optimize_no_use_stack(func_binary fb) - { - for (int i = 0; i < fb.m_debug_stack_variant_info.length; i++) - { - stack_variant_info info = fb.m_debug_stack_variant_info[i]; - int pos = info.m_pos; - - if (pos < fb.m_paramnum) - { - continue; - } - - boolean use = false; - for (int j = 0; j < fb.m_buff.length; j++) - { - long cmd = fb.m_buff[j]; - if (is_addr_type(cmd, command.ADDR_STACK, pos)) - { - use = true; - break; - } - } - - for (int j = 0; j < fb.m_container_addr_list.length; j++) - { - container_addr addr = fb.m_container_addr_list[j]; - if (is_addr_type(addr.m_con, command.ADDR_STACK, pos)) - { - use = true; - break; - } - if (is_addr_type(addr.m_key, command.ADDR_STACK, pos)) - { - use = true; - break; - } - } - - if (!use) - { - stack_variant_info[] newlist = new stack_variant_info[fb.m_debug_stack_variant_info.length - 1]; - for (int z = 0; z < i; z++) - { - newlist[z] = fb.m_debug_stack_variant_info[z]; - } - for (int z = i; z < fb.m_debug_stack_variant_info.length - 1; z++) - { - newlist[z] = fb.m_debug_stack_variant_info[z + 1]; - } - fb.m_debug_stack_variant_info = newlist; - - isopt = true; - } - } - } - - private void optimize_no_use_container(func_binary fb) - { - for (int i = 0; i < fb.m_container_addr_list.length; i++) - { - boolean use = false; - for (int j = 0; j < fb.m_buff.length; j++) - { - long cmd = fb.m_buff[j]; - if (is_addr_type(cmd, command.ADDR_CONTAINER, i)) - { - use = true; - break; - } - } - - for (int j = 0; j < fb.m_container_addr_list.length; j++) - { - container_addr addr = fb.m_container_addr_list[j]; - if (is_addr_type(addr.m_con, command.ADDR_CONTAINER, i)) - { - use = true; - break; - } - if (is_addr_type(addr.m_key, command.ADDR_CONTAINER, i)) - { - use = true; - break; - } - } - - if (!use) - { - for (int j = i; j < fb.m_container_addr_list.length - 1; j++) - { - replace_all_addr(fb, command.ADDR_CONTAINER, j + 1, j); - } - - container_addr[] newlist = new container_addr[fb.m_container_addr_list.length - 1]; - for (int j = 0; j < i; j++) - { - newlist[j] = fb.m_container_addr_list[j]; - } - for (int j = i; j < fb.m_container_addr_list.length - 1; j++) - { - newlist[j] = fb.m_container_addr_list[j + 1]; - } - fb.m_container_addr_list = newlist; - - isopt = true; - return; - } - } - } - - private void optimize_no_use_const(func_binary fb) - { - for (int i = 0; i < fb.m_const_list.length; i++) - { - boolean use = false; - for (int j = 0; j < fb.m_buff.length; j++) - { - long cmd = fb.m_buff[j]; - if (is_addr_type(cmd, command.ADDR_CONST, i)) - { - use = true; - break; - } - } - - for (int j = 0; j < fb.m_container_addr_list.length; j++) - { - container_addr addr = fb.m_container_addr_list[j]; - if (is_addr_type(addr.m_con, command.ADDR_CONST, i)) - { - use = true; - break; - } - if (is_addr_type(addr.m_key, command.ADDR_CONST, i)) - { - use = true; - break; - } - } - - if (!use) - { - for (int j = i; j < fb.m_const_list.length - 1; j++) - { - replace_all_addr(fb, command.ADDR_CONST, j + 1, j); - } - - variant[] newlist = new variant[fb.m_const_list.length - 1]; - for (int j = 0; j < i; j++) - { - newlist[j] = fb.m_const_list[j]; - } - for (int j = i; j < fb.m_const_list.length - 1; j++) - { - newlist[j] = fb.m_const_list[j + 1]; - } - fb.m_const_list = newlist; - - isopt = true; - return; - } - } - } - - private boolean is_addr_type(long cmd, int dsttype, int dstpos) - { - int type = command.COMMAND_TYPE(cmd); - int code = command.COMMAND_CODE(cmd); - - if (type == command.COMMAND_ADDR) - { - int addrtype = command.HIINT16(code); - int pos = command.LOINT16(code); - - if (addrtype == dsttype && pos == dstpos) - { - return true; - } - } - return false; - } - - private void replace_all_addr(func_binary fb, int dsttype, int frompos, int topos) - { - for (int j = 0; j < fb.m_buff.length; j++) - { - long cmd = fb.m_buff[j]; - fb.m_buff[j] = replace_addr_cmd(cmd, dsttype, frompos, topos); - } - - for (int j = 0; j < fb.m_container_addr_list.length; j++) - { - container_addr addr = fb.m_container_addr_list[j]; - addr.m_con = replace_addr_cmd(addr.m_con, dsttype, frompos, topos); - addr.m_key = replace_addr_cmd(addr.m_key, dsttype, frompos, topos); - } - } - - private long replace_addr_cmd(long cmd, int dsttype, int frompos, int topos) - { - int type = command.COMMAND_TYPE(cmd); - int code = command.COMMAND_CODE(cmd); - - if (type == command.COMMAND_ADDR) - { - int addrtype = command.HIINT16(code); - int pos = command.LOINT16(code); - if (addrtype == dsttype && pos == frompos) - { - pos = topos; - cmd = command.MAKE_ADDR(addrtype, pos); - } - } - - return cmd; - } - - private void optimize_stack_pos(func_binary fb) - { - // 整理下栈,可能有空洞 - HashSet set = new HashSet(); - - int max = -1; - for (int j = 0; j < fb.m_paramnum; j++) - { - set.add(j); - if (j > max) - { - max = j; - } - } - - for (int j = 0; j < fb.m_buff.length; j++) - { - long cmd = fb.m_buff[j]; - int type = command.COMMAND_TYPE(cmd); - int code = command.COMMAND_CODE(cmd); - - if (type == command.COMMAND_ADDR) - { - int addrtype = command.HIINT16(code); - int pos = command.LOINT16(code); - if (addrtype == command.ADDR_STACK) - { - set.add(pos); - if (pos > max) - { - max = pos; - } - } - } - } - for (int j = 0; j < fb.m_container_addr_list.length; j++) - { - container_addr addr = fb.m_container_addr_list[j]; - - long cmd = addr.m_con; - int type = command.COMMAND_TYPE(cmd); - int code = command.COMMAND_CODE(cmd); - - if (type == command.COMMAND_ADDR) - { - int addrtype = command.HIINT16(code); - int pos = command.LOINT16(code); - if (addrtype == command.ADDR_STACK) - { - set.add(pos); - if (pos > max) - { - max = pos; - } - } - } - - cmd = addr.m_key; - type = command.COMMAND_TYPE(cmd); - code = command.COMMAND_CODE(cmd); - - if (type == command.COMMAND_ADDR) - { - int addrtype = command.HIINT16(code); - int pos = command.LOINT16(code); - if (addrtype == command.ADDR_STACK) - { - set.add(pos); - if (pos > max) - { - max = pos; - } - } - } - } - - if (set.size() == max + 1) - { - return; - } - - int empty = -1; - for (int j = 0; j < max + 1; j++) - { - if (!set.contains(j)) - { - empty = j; - break; - } - } - - for (int i = empty; i < max; i++) - { - replace_all_addr(fb, command.ADDR_STACK, i + 1, i); - } - - for (int i = empty; i < max; i++) - { - for (int j = 0; j < fb.m_debug_stack_variant_info.length; j++) - { - stack_variant_info info = fb.m_debug_stack_variant_info[j]; - if (info.m_pos == i) - { - stack_variant_info[] newlist = new stack_variant_info[fb.m_debug_stack_variant_info.length - 1]; - for (int z = 0; z < j; z++) - { - newlist[z] = fb.m_debug_stack_variant_info[z]; - } - for (int z = j; z < fb.m_debug_stack_variant_info.length - 1; z++) - { - newlist[z] = fb.m_debug_stack_variant_info[z + 1]; - } - fb.m_debug_stack_variant_info = newlist; - break; - } - } - for (int j = 0; j < fb.m_debug_stack_variant_info.length; j++) - { - stack_variant_info info = fb.m_debug_stack_variant_info[j]; - if (info.m_pos == i + 1) - { - info.m_pos = i; - } - } - } - - isopt = true; - } - - private void set_ins_write(opt_ins ins, func_binary fb, int ip) - { - long v_cmd = fb.m_buff[ip]; - ins.dst.add(new opt_ins_addr(ip, v_cmd)); - set_ins_write_by_cmd(ins, fb, v_cmd); - } - - private void set_ins_write_by_cmd(opt_ins ins, func_binary fb, long v_cmd) - { - int v_addrtype = command.ADDR_TYPE(command.COMMAND_CODE(v_cmd)); - int v_addrpos = command.ADDR_POS(command.COMMAND_CODE(v_cmd)); - if (v_addrtype == command.ADDR_STACK) - { - ins.write.add(v_cmd); - } - else if (v_addrtype == command.ADDR_CONST) - { - } - else if (v_addrtype == command.ADDR_CONTAINER) - { - container_addr ca = fb.m_container_addr_list[v_addrpos]; - - set_ins_read_by_cmd(ins, fb, ca.m_con); - set_ins_write_by_cmd(ins, fb, ca.m_con); - set_ins_read_by_cmd(ins, fb, ca.m_key); - } - } - - private void set_ins_read(opt_ins ins, func_binary fb, int ip) - { - long v_cmd = fb.m_buff[ip]; - ins.src.add(new opt_ins_addr(ip, v_cmd)); - set_ins_read_by_cmd(ins, fb, v_cmd); - } - - private void set_ins_read_by_cmd(opt_ins ins, func_binary fb, long v_cmd) - { - int v_addrtype = command.ADDR_TYPE(command.COMMAND_CODE(v_cmd)); - int v_addrpos = command.ADDR_POS(command.COMMAND_CODE(v_cmd)); - if (v_addrtype == command.ADDR_STACK) - { - ins.read.add(v_cmd); - } - else if (v_addrtype == command.ADDR_CONST) - { - } - else if (v_addrtype == command.ADDR_CONTAINER) - { - container_addr ca = fb.m_container_addr_list[v_addrpos]; - - set_ins_read_by_cmd(ins, fb, ca.m_con); - set_ins_write_by_cmd(ins, fb, ca.m_con); - set_ins_read_by_cmd(ins, fb, ca.m_key); - } - } - - private void trans_inslist(func_binary fb) throws Exception - { - inslist.clear(); - - int ip = 0; - while (ip < fb.m_buff.length) - { - int code = command.COMMAND_CODE(fb.m_buff[ip]); - - opt_ins ins = new opt_ins(); - ins.code = code; - ins.offset = ip; - inslist.add(ins); - - ip++; - - // 执行对应命令,放一起switch效率更高,cpu有缓存 - switch (code) - { - case command.OPCODE_ASSIGN: - { - set_ins_write(ins, fb, ip); - ip++; - - // 赋值来源 - set_ins_read(ins, fb, ip); - ip++; - } - break; - case command.OPCODE_PLUS: - case command.OPCODE_MINUS: - case command.OPCODE_MULTIPLY: - case command.OPCODE_DIVIDE: - case command.OPCODE_DIVIDE_MOD: - case command.OPCODE_STRING_CAT: - case command.OPCODE_AND: - case command.OPCODE_OR: - case command.OPCODE_LESS: - case command.OPCODE_MORE: - case command.OPCODE_EQUAL: - case command.OPCODE_MOREEQUAL: - case command.OPCODE_LESSEQUAL: - case command.OPCODE_NOTEQUAL: - { - set_ins_read(ins, fb, ip); - ip++; - - set_ins_read(ins, fb, ip); - ip++; - - set_ins_write(ins, fb, ip); - ip++; - } - break; - case command.OPCODE_NOT: - { - set_ins_read(ins, fb, ip); - ip++; - - set_ins_write(ins, fb, ip); - ip++; - } - break; - case command.OPCODE_AND_JNE: - case command.OPCODE_OR_JNE: - case command.OPCODE_LESS_JNE: - case command.OPCODE_MORE_JNE: - case command.OPCODE_EQUAL_JNE: - case command.OPCODE_MOREEQUAL_JNE: - case command.OPCODE_LESSEQUAL_JNE: - case command.OPCODE_NOTEQUAL_JNE: - { - set_ins_read(ins, fb, ip); - ip++; - - set_ins_read(ins, fb, ip); - ip++; - - /* dest */ - ip++; - ip++; - } - break; - case command.OPCODE_NOT_JNE: - { - set_ins_read(ins, fb, ip); - ip++; - - /* dest */ - ip++; - ip++; - } - break; - case command.OPCODE_JNE: - { - set_ins_read(ins, fb, ip); - ip++; - - ip++; - } - break; - case command.OPCODE_JMP: - { - ip++; - } - break; - case command.OPCODE_PLUS_ASSIGN: - case command.OPCODE_MINUS_ASSIGN: - case command.OPCODE_MULTIPLY_ASSIGN: - case command.OPCODE_DIVIDE_ASSIGN: - case command.OPCODE_DIVIDE_MOD_ASSIGN: - { - set_ins_read(ins, fb, ip); - set_ins_write(ins, fb, ip); - ip++; - - set_ins_read(ins, fb, ip); - ip++; - } - break; - case command.OPCODE_CALL: - { - ip++; - - set_ins_read(ins, fb, ip); - ip++; - - int retnum = command.COMMAND_CODE(fb.m_buff[ip]); - ip++; - - for (int i = 0; i < retnum; i++) - { - set_ins_write(ins, fb, ip); - ip++; - } - - int argnum = command.COMMAND_CODE(fb.m_buff[ip]); - ip++; - - for (int i = 0; i < argnum; i++) - { - set_ins_write(ins, fb, ip); - set_ins_read(ins, fb, ip); - ip++; - } - } - break; - case command.OPCODE_RETURN: - { - int returnnum = command.COMMAND_CODE(fb.m_buff[ip]); - ip++; - - // 塞给ret - for (int i = 0; i < returnnum; i++) - { - set_ins_read(ins, fb, ip); - ip++; - } - } - break; - case command.OPCODE_FORBEGIN: - { - // var - set_ins_read(ins, fb, ip); - set_ins_write(ins, fb, ip); - ip++; - - // begin - set_ins_read(ins, fb, ip); - ip++; - - // end - set_ins_read(ins, fb, ip); - ip++; - - // add - set_ins_read(ins, fb, ip); - ip++; - - ip++; - } - break; - case command.OPCODE_FORLOOP: - { - // var - set_ins_read(ins, fb, ip); - set_ins_write(ins, fb, ip); - ip++; - - // end - set_ins_read(ins, fb, ip); - ip++; - - // add - set_ins_read(ins, fb, ip); - ip++; - - ip++; - } - break; - case command.OPCODE_SLEEP: - { - set_ins_read(ins, fb, ip); - ip++; - } - break; - case command.OPCODE_YIELD: - { - set_ins_read(ins, fb, ip); - ip++; - } - break; - default: - throw new Exception("next err code " + code + " " + types.OpCodeStr(code)); - } - ins.size = ip - ins.offset; - } - } - - private opt_ins get_assign_src_ins_from(int pos, long cmd) - { - for (int i = pos; i < inslist.size(); i++) - { - opt_ins ins = inslist.get(i); - if (ins.code == command.OPCODE_ASSIGN) - { - for (int j = 0; j < ins.src.size(); j++) - { - if (ins.src.get(j).addr == cmd) - { - return ins; - } - } - } - } - return null; - } - - private opt_ins get_assign_dst_ins_from(int pos, long cmd) - { - for (int i = pos; i < inslist.size(); i++) - { - opt_ins ins = inslist.get(i); - if (ins.code == command.OPCODE_ASSIGN) - { - for (int j = 0; j < ins.dst.size(); j++) - { - if (ins.dst.get(j).addr == cmd) - { - return ins; - } - } - } - } - return null; - } - - private opt_ins get_write_ins_from(int pos, long cmd) - { - for (int i = pos; i < inslist.size(); i++) - { - opt_ins ins = inslist.get(i); - for (int j = 0; j < ins.write.size(); j++) - { - if (ins.write.get(j) == cmd) - { - return ins; - } - } - } - return null; - } - - private opt_ins get_read_ins_from(int pos, long cmd) - { - for (int i = pos; i < inslist.size(); i++) - { - opt_ins ins = inslist.get(i); - for (int j = 0; j < ins.read.size(); j++) - { - if (ins.read.get(j) == cmd) - { - return ins; - } - } - } - return null; - } - - private void replace_ins_addr(func_binary fb, opt_ins_addr oldaddr, opt_ins_addr newaddr) - { - int offset = oldaddr.offset; - fb.m_buff[offset] = newaddr.addr; - } - - private void remove_ins(func_binary fb, opt_ins delins) - { - // 刷一遍jmp,然后删除 - for (int i = 0; i < inslist.size(); i++) - { - opt_ins ins = inslist.get(i); - switch (ins.code) - { - case command.OPCODE_AND_JNE: - case command.OPCODE_OR_JNE: - case command.OPCODE_LESS_JNE: - case command.OPCODE_MORE_JNE: - case command.OPCODE_EQUAL_JNE: - case command.OPCODE_MOREEQUAL_JNE: - case command.OPCODE_LESSEQUAL_JNE: - case command.OPCODE_NOTEQUAL_JNE: - case command.OPCODE_FORLOOP: - { - int destip = command.COMMAND_CODE(fb.m_buff[ins.offset + 4]); - if (destip > delins.offset) - { - fb.m_buff[ins.offset + 4] = command.MAKE_POS(destip - delins.size); - } - } - break; - case command.OPCODE_NOT_JNE: - { - int destip = command.COMMAND_CODE(fb.m_buff[ins.offset + 3]); - if (destip > delins.offset) - { - fb.m_buff[ins.offset + 3] = command.MAKE_POS(destip - delins.size); - } - } - break; - case command.OPCODE_JNE: - { - int destip = command.COMMAND_CODE(fb.m_buff[ins.offset + 2]); - if (destip > delins.offset) - { - fb.m_buff[ins.offset + 2] = command.MAKE_POS(destip - delins.size); - } - } - break; - case command.OPCODE_JMP: - { - int destip = command.COMMAND_CODE(fb.m_buff[ins.offset + 1]); - if (destip > delins.offset) - { - fb.m_buff[ins.offset + 1] = command.MAKE_POS(destip - delins.size); - } - } - break; - case command.OPCODE_FORBEGIN: - { - int destip = command.COMMAND_CODE(fb.m_buff[ins.offset + 5]); - if (destip > delins.offset) - { - fb.m_buff[ins.offset + 5] = command.MAKE_POS(destip - delins.size); - } - } - break; - } - } - - long[] newbuff = new long[fb.m_buff.length - delins.size]; - int[] newlinenobuff = new int[fb.m_lineno_buff.length - delins.size]; - - for (int i = 0; i < delins.offset; i++) - { - newbuff[i] = fb.m_buff[i]; - newlinenobuff[i] = fb.m_lineno_buff[i]; - } - - for (int i = 0; i < fb.m_buff.length - delins.offset - delins.size; i++) - { - newbuff[delins.offset + i] = fb.m_buff[delins.offset + delins.size + i]; - newlinenobuff[delins.offset + i] = fb.m_lineno_buff[delins.offset + delins.size + i]; - } - - fb.m_buff = newbuff; - fb.m_lineno_buff = newlinenobuff; - } - - private void optimize_write_write(func_binary fb) - { - // eg.a=1 return 1 - // write,并没有read,删除这个write - for (int i = 0; i < inslist.size(); i++) - { - opt_ins ins = inslist.get(i); - if (ins.write.size() > 0) - { - int num = 0; - for (int j = 0; j < ins.write.size(); j++) - { - long addr = ins.write.get(j); - - if (command.ADDR_TYPE(command.COMMAND_CODE(addr)) == command.ADDR_CONTAINER) - { - continue; - } - - if (ins.code == command.OPCODE_CALL) - { - continue; - } - - opt_ins readins = get_read_ins_from(0, addr); - if (readins == null) - { - num++; - continue; - } - } - - if (num == ins.write.size()) - { - remove_ins(fb, ins); - isopt = true; - return; - } - } - } - } - - private void optimize_write_assign(func_binary fb) - { - // eg.a=b+c - // write一个后,只assign了一次,再无read,替换掉write的dst,删除assign - for (int i = 0; i < inslist.size(); i++) - { - opt_ins ins = inslist.get(i); - for (int j = 0; j < ins.dst.size(); j++) - { - opt_ins_addr addr = ins.dst.get(j); - if (command.ADDR_TYPE(command.COMMAND_CODE(addr.addr)) == command.ADDR_CONTAINER) - { - continue; - } - opt_ins assign_ins = get_assign_src_ins_from(i + 1, addr.addr); - if (assign_ins != null && get_read_ins_from(i + 1, addr.addr) == assign_ins) - { - if (get_read_ins_from(i + 2, addr.addr) == null) - { - opt_ins_addr assign_ins_addr = assign_ins.dst.get(0); - replace_ins_addr(fb, addr, assign_ins_addr); - remove_ins(fb, assign_ins); - isopt = true; - return; - } - } - } - } - isopt = false; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/packagehelper.java b/src/main/java/com/github/esrrhs/fakescript/packagehelper.java deleted file mode 100644 index e85f9bb..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/packagehelper.java +++ /dev/null @@ -1,182 +0,0 @@ -package com.github.esrrhs.fakescript; - -import java.io.File; -import java.io.FileFilter; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.net.JarURLConnection; -import java.net.URL; -import java.net.URLDecoder; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.List; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; - -public class packagehelper -{ - /** - * 从包package中获取所有的Class - */ - public static List> getClasses(fake f, String packageName) - { - - // 第一个class类的集合 - List> classes = new ArrayList>(); - // 是否循环迭代 - boolean recursive = true; - // 获取包的名字 并进行替换 - String packageDirName = packageName.replace('.', '/'); - // 定义一个枚举的集合 并进行循环来处理这个目录下的things - Enumeration dirs; - try - { - dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName); - // 循环迭代下去 - while (dirs.hasMoreElements()) - { - // 获取下一个元素 - URL url = dirs.nextElement(); - // 得到协议的名称 - String protocol = url.getProtocol(); - // 如果是以文件的形式保存在服务器上 - if ("file".equals(protocol)) - { - // 获取包的物理路径 - String filePath = URLDecoder.decode(url.getFile(), "UTF-8"); - // 以文件的方式扫描整个包下的文件 并添加到集合中 - findAndAddClassesInPackageByFile(f, packageName, filePath, recursive, classes); - } - else if ("jar".equals(protocol)) - { - // 如果是jar包文件 - // 定义一个JarFile - JarFile jar; - try - { - // 获取jar - jar = ((JarURLConnection) url.openConnection()).getJarFile(); - // 从此jar包 得到一个枚举类 - Enumeration entries = jar.entries(); - // 同样的进行循环迭代 - while (entries.hasMoreElements()) - { - // 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件 - JarEntry entry = entries.nextElement(); - String name = entry.getName(); - // 如果是以/开头的 - if (name.charAt(0) == '/') - { - // 获取后面的字符串 - name = name.substring(1); - } - // 如果前半部分和定义的包名相同 - if (name.startsWith(packageDirName)) - { - int idx = name.lastIndexOf('/'); - // 如果以"/"结尾 是一个包 - if (idx != -1) - { - // 获取包名 把"/"替换成"." - packageName = name.substring(0, idx).replace('/', '.'); - } - // 如果可以迭代下去 并且是一个包 - if ((idx != -1) || recursive) - { - // 如果是一个.class文件 而且不是目录 - if (name.endsWith(".class") && !entry.isDirectory()) - { - // 去掉后面的".class" 获取真正的类名 - String className = name.substring(packageName.length() + 1, name.length() - 6); - try - { - // 添加到classes - classes.add(Class.forName(packageName + '.' + className)); - } - catch (Exception e) - { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - types.seterror(f, "", 0, "", e.toString() + "\n" + sw.toString()); - pw.close(); - } - } - } - } - } - } - catch (Exception e) - { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - types.seterror(f, "", 0, "", e.toString() + "\n" + sw.toString()); - pw.close(); - } - } - } - } - catch (Exception e) - { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - types.seterror(f, "", 0, "", e.toString() + "\n" + sw.toString()); - pw.close(); - } - - return classes; - } - - /** - * 以文件的形式来获取包下的所有Class - */ - public static void findAndAddClassesInPackageByFile(fake f, String packageName, String packagePath, - final boolean recursive, List> classes) - { - // 获取此包的目录 建立一个File - File dir = new File(packagePath); - // 如果不存在或者 也不是目录就直接返回 - if (!dir.exists() || !dir.isDirectory()) - { - return; - } - // 如果存在 就获取包下的所有文件 包括目录 - File[] dirfiles = dir.listFiles(new FileFilter() { - // 自定义过滤规则 如果可以循环(包含子目录) 或则是以.class结尾的文件(编译好的java类文件) - public boolean accept(File file) - { - return (recursive && file.isDirectory()) || (file.getName().endsWith(".class")); - } - }); - // 循环所有文件 - for (File file : dirfiles) - { - // 如果是目录 则继续扫描 - if (file.isDirectory()) - { - findAndAddClassesInPackageByFile(f, packageName + "." + file.getName(), file.getAbsolutePath(), - recursive, classes); - } - else - { - // 如果是java类文件 去掉后面的.class 只留下类名 - String className = file.getName().substring(0, file.getName().length() - 6); - try - { - // 添加到集合中去 - classes.add(Class.forName(packageName + '.' + className)); - } - catch (Exception e) - { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - types.seterror(f, "", 0, "", e.toString() + "\n" + sw.toString()); - pw.close(); - } - } - } - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/paramstack.java b/src/main/java/com/github/esrrhs/fakescript/paramstack.java deleted file mode 100644 index 28d68f5..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/paramstack.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.github.esrrhs.fakescript; - -import java.util.ArrayList; - -class paramstack -{ - private fake m_f; - - private ArrayList m_variant_list = new ArrayList(); - - public paramstack(fake f) - { - m_f = f; - } - - public void clear() - { - m_variant_list.clear(); - } - - public variant push_and_get() - { - variant v = new variant(); - m_variant_list.add(v); - return v; - } - - public variant pop_and_get() - { - variant v = m_variant_list.get(m_variant_list.size() - 1); - m_variant_list.remove(m_variant_list.size() - 1); - return v; - } - - public variant get(int i) - { - return m_variant_list.get(i); - } - - public int size() - { - return m_variant_list.size(); - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/parser.java b/src/main/java/com/github/esrrhs/fakescript/parser.java deleted file mode 100644 index 26356e8..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/parser.java +++ /dev/null @@ -1,279 +0,0 @@ -package com.github.esrrhs.fakescript; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; - -class parser -{ - private fake m_f; - private int m_parse_dep = 0; - private ArrayList m_parsing_file_list = new ArrayList(); - // const永久存在映射 - private HashMap m_constv_map = new HashMap(); - private HashMap m_constline_map = new HashMap(); - - public parser(fake f) - { - m_f = f; - } - - public parser clonef(fake f) - { - parser ret = new parser(f); - ret.m_constv_map = (HashMap) this.m_constv_map.clone(); - ret.m_constline_map = (HashMap) this.m_constline_map.clone(); - return ret; - } - - public void clear() - { - m_parse_dep = 0; - m_parsing_file_list.clear(); - } - - public void reg_const_define(String constname, variant v, int lineno) - { - m_constv_map.put(constname, v); - m_constline_map.put(constname, lineno); - } - - public variant get_const_define(String constname) - { - return m_constv_map.get(constname); - } - - public int get_const_define_lineno(String constname) - { - return m_constline_map.get(constname); - } - - public boolean parsestr(String str) - { - // 解析语法 - java.io.Reader reader = new java.io.StringReader(str); - - Yylex yylexer = new Yylex(reader); - yylexer.set_fake(m_f); - mybison mbs = new mybison(m_f, yylexer); - mbs.set_filename(""); - yylexer.set_mybison(mbs); - - YYParser yyparser = new YYParser(yylexer); - - try - { - if (!yyparser.parse()) - { - types.seterror(m_f, "", mbs.get_error_line(), "", "parse %s file fail for reason : %s", "", - mbs.get_error()); - return false; - } - } - catch (Exception e) - { - types.seterror(m_f, "", fk.getcurline(m_f), fk.getcurfunc(m_f), - "parse " + "" + " fail " + types.show_exception(e)); - return false; - } - - // 编译 - try - { - compiler mc = new compiler(m_f, mbs); - if (!mc.compile()) - { - return false; - } - } - catch (Exception e) - { - types.seterror(m_f, "", fk.getcurline(m_f), fk.getcurfunc(m_f), - "compiler " + "" + " fail " + types.show_exception(e)); - return false; - } - - types.log(m_f, "parse str OK"); - - return true; - } - - public boolean parse(String filename) - { - m_parse_dep++; - - // 检查深度 - if (m_parse_dep >= m_f.cfg.include_deps) - { - types.seterror(m_f, filename, 0, "", "parse " + filename + " file too deep " + m_parse_dep); - return false; - } - - // 检查当前文件是否在解析中 - if (is_parsing(filename)) - { - types.seterror(m_f, filename, 0, "", - "already parsing " + filename + " file...include list \n" + get_parsing_file_list()); - return false; - } - - // 加入 - m_parsing_file_list.add(filename); - - // 输入源文件 - String content = read_file(filename); - if (content.isEmpty()) - { - return false; - } - - // 解析语法 - java.io.Reader reader = new java.io.StringReader(content); - - Yylex yylexer = new Yylex(reader); - yylexer.set_fake(m_f); - mybison mbs = new mybison(m_f, yylexer); - mbs.set_filename(filename); - yylexer.set_mybison(mbs); - - YYParser yyparser = new YYParser(yylexer); - - try - { - if (!yyparser.parse()) - { - types.seterror(m_f, filename, yylexer.get_line() + 1, fk.getcurfunc(m_f), - "parse %s file fail for reason : %s", filename, mbs.get_error()); - return false; - } - } - catch (Exception e) - { - types.seterror(m_f, filename, yylexer.get_line() + 1, fk.getcurfunc(m_f), - "parse " + filename + " fail " + types.show_exception(e)); - return false; - } - - // 解析前置文件 - for (int i = 0; i < (int) mbs.get_include_list().size(); i++) - { - String name = mbs.get_include_list().get(i); - if (!parse_include(filename, name)) - { - return false; - } - } - - // 编译 - try - { - compiler mc = new compiler(m_f, mbs); - if (!mc.compile()) - { - return false; - } - } - catch (Exception e) - { - types.seterror(m_f, filename, fk.getcurline(m_f), fk.getcurfunc(m_f), - "compiler " + filename + " fail " + types.show_exception(e)); - return false; - } - - // 弹出 - m_parsing_file_list.remove(m_parsing_file_list.size() - 1); - - types.log(m_f, "parse " + filename + " OK"); - - return true; - } - - private String read_file(String filename) - { - File file = new File(filename); - if (!file.isFile() || !file.exists()) - { - types.seterror(m_f, filename, 0, "", "open " + filename + " fail"); - return ""; - } - - try - { - String ret = ""; - String encoding = "utf-8"; - - Reader reader = new InputStreamReader(new FileInputStream(file), encoding); - BufferedReader bufferedReader = new BufferedReader(reader); - char[] readbuff = new char[10]; - while (bufferedReader.read(readbuff) != -1) - { - ret += String.valueOf(readbuff); - Arrays.fill(readbuff, '\0'); - } - reader.close(); - - types.log(m_f, ret); - - return ret; - } - catch (Exception e) - { - types.seterror(m_f, filename, 0, "", "read " + filename + " fail " + types.show_exception(e)); - return ""; - } - } - - private boolean is_parsing(String filename) - { - boolean ret = false; - for (int i = 0; i < (int) m_parsing_file_list.size(); i++) - { - if (m_parsing_file_list.get(i) == filename) - { - return true; - } - } - return ret; - } - - private String get_parsing_file_list() - { - String ret = ""; - for (int i = 0; i < (int) m_parsing_file_list.size(); i++) - { - ret += m_parsing_file_list.get(i); - ret += "\n"; - } - return ret; - } - - private boolean parse_include(String srcname, String includename) - { - // 拼include的名字 - String dir = srcname; - dir = dir.replace('\\', '/'); - int pos = dir.lastIndexOf('/'); - if (pos == -1) - { - dir = ""; - } - else - { - dir = dir.substring(0, pos + 1); - } - dir += includename; - - // 解析 - if (!parse(dir)) - { - return false; - } - return true; - } - -} diff --git a/src/main/java/com/github/esrrhs/fakescript/processor.java b/src/main/java/com/github/esrrhs/fakescript/processor.java deleted file mode 100644 index 9c46ded..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/processor.java +++ /dev/null @@ -1,239 +0,0 @@ -package com.github.esrrhs.fakescript; - -import java.util.ArrayList; - -class processor -{ - private fake m_f; - private routine m_entryroutine; - private int m_genid; - private routine m_curroutine; - private ArrayList m_routines = new ArrayList(); - int m_lastroutine; - int m_lastroutine_runnum; - - public processor(fake f) - { - m_f = f; - } - - public routine get_curroutine() - { - return m_curroutine; - } - - public routine get_entrycurroutine() - { - return m_entryroutine; - } - - public routine start_routine(variant func, ArrayList retpos) throws Exception - { - routine r = new routine(m_f); - - if (m_entryroutine == null) - { - m_entryroutine = r; - } - - r.set_id(m_genid); - m_genid++; - r.set_processor(this); - r.entry(func, retpos); - - if (m_curroutine == null) - { - m_curroutine = r; - } - - m_routines.add(r); - - return r; - } - - public void run() throws Exception - { - if (m_f.rn.is_stepmod()) - { - if (m_routines.isEmpty()) - { - return; - } - - routine n = m_routines.get(m_lastroutine); - n.run(1); - m_lastroutine_runnum++; - boolean needupdate = false; - if (n.is_end()) - { - m_routines.remove(m_lastroutine); - needupdate = true; - } - else if (m_lastroutine_runnum >= m_f.cfg.per_frame_cmd_num) - { - m_lastroutine_runnum = 0; - needupdate = true; - } - if (needupdate) - { - for (int i = 0; i < m_routines.size(); i++) - { - int index = m_lastroutine + i + 1; - if (index >= m_routines.size()) - { - index = index % m_routines.size(); - } - - routine nex = m_routines.get(index); - if (nex == null) - { - continue; - } - - m_curroutine = n; - m_lastroutine = index; - break; - } - } - } - else - { - while (!m_routines.isEmpty()) - { - for (int i = 0; i < (int) m_routines.size(); i++) - { - routine r = m_routines.get(i); - m_curroutine = r; - // 注意:此函数内部可能会调用到add接口 - r.run(m_f.cfg.per_frame_cmd_num); - if (r.is_end()) - { - m_routines.remove(i); - } - } - } - } - } - - public String get_routine_info() - { - String tmp = ""; - for (int i = 0; i < m_routines.size(); i++) - { - routine r = m_routines.get(i); - - tmp += "#"; - tmp += i; - tmp += "\tId:"; - tmp += r.get_id(); - tmp += "\t"; - tmp += r.get_interpreter().get_running_func_name(); - tmp += "("; - tmp += r.get_interpreter().get_running_file_name(); - tmp += ":"; - tmp += r.get_interpreter().get_running_file_line(); - tmp += ")\t"; - tmp += r.is_end() ? "Dead" : "Alive"; - tmp += "\n"; - } - return tmp; - } - - public routine get_routine_by_id(int id) - { - for (int i = 0; i < m_routines.size(); i++) - { - routine r = m_routines.get(i); - if (r == null) - { - continue; - } - - if (r.get_id() == id) - { - return r; - } - } - return null; - } - - public String get_routine_info_by_id(int id) - { - int j = 0; - for (int i = 0; i < m_routines.size(); i++) - { - routine r = m_routines.get(i); - if (r == null) - { - continue; - } - - if (r.get_id() == id) - { - String ret = ""; - - ret += "#"; - ret += j; - ret += "\tId:"; - ret += id; - ret += "\t"; - ret += r.get_interpreter().get_running_func_name(); - ret += "("; - ret += r.get_interpreter().get_running_file_name(); - ret += ":"; - ret += r.get_interpreter().get_running_file_line(); - ret += ")\t"; - ret += r.is_end() ? "Dead" : "Alive"; - ret += "\n"; - return ret; - } - j++; - } - return ""; - } - - public int get_routine_num() - { - return m_routines.size(); - } - - public routine get_routine_by_index(int index) - { - int j = 0; - for (int i = 0; i < m_routines.size(); i++) - { - routine r = m_routines.get(i); - if (r == null) - { - continue; - } - - if (j >= index) - { - return r; - } - j++; - } - return null; - } - - String get_routine_info_by_index(int index) - { - int j = 0; - for (int i = 0; i < m_routines.size(); i++) - { - routine r = m_routines.get(i); - if (r == null) - { - continue; - } - - if (j >= index) - { - return get_routine_info_by_id(r.get_id()); - } - j++; - } - return ""; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/profile.java b/src/main/java/com/github/esrrhs/fakescript/profile.java deleted file mode 100644 index 6b94902..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/profile.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.github.esrrhs.fakescript; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.Map; - -class profile -{ - private fake m_f; - private boolean m_isopen; - private HashMap m_map = new HashMap(); - private int[] m_codetype = new int[command.OPCODE_MAX]; - - public profile(fake f) - { - m_f = f; - } - - public void open() - { - m_isopen = true; - } - - public void close() - { - m_isopen = false; - } - - public boolean isopen() - { - return m_isopen; - } - - public void add_func_sample(String func, long calltime) - { - profilefuncele p = m_map.get(func); - if (p == null) - { - profilefuncele tmp = new profilefuncele(); - tmp.m_callnum = 1; - tmp.m_calltime = calltime; - m_map.put(func, tmp); - return; - } - - p.m_callnum++; - p.m_calltime += calltime; - } - - public String dump() - { - ArrayList> sortelevec = new ArrayList>(); - for (Map.Entry e : m_map.entrySet()) - { - sortelevec.add(e); - } - - Collections.sort(sortelevec, new Comparator>() { - public int compare(Map.Entry arg0, Map.Entry arg1) - { - Long a0 = arg0.getValue().m_calltime; - Long a1 = arg1.getValue().m_calltime; - return a1.compareTo(a0); - } - }); - - String dumpstr = ""; - - int wraplen = 30; - - dumpstr += "Call Func:\n"; - dumpstr += "\t"; - dumpstr += fix_string_wrap("Func", wraplen); - dumpstr += fix_string_wrap("Calls", wraplen); - dumpstr += fix_string_wrap("TotalTime(ms)", wraplen); - dumpstr += fix_string_wrap("PerCallTime(ms)", wraplen); - dumpstr += "\n"; - for (int i = 0; i < (int) sortelevec.size(); i++) - { - profilefuncele ele = sortelevec.get(i).getValue(); - dumpstr += "\t"; - dumpstr += fix_string_wrap(sortelevec.get(i).getKey(), wraplen); - dumpstr += fix_string_wrap("" + ele.m_callnum, wraplen); - dumpstr += fix_string_wrap("" + ele.m_calltime, wraplen); - dumpstr += fix_string_wrap("" + (ele.m_callnum != 0 ? ele.m_calltime / ele.m_callnum : 0), wraplen); - dumpstr += "\n"; - } - - dumpstr += "Code Num:\n"; - for (int i = 0; i < command.OPCODE_MAX; i++) - { - dumpstr += "\t"; - dumpstr += types.OpCodeStr(i); - for (int j = 0; j < (int) (20 - types.OpCodeStr(i).length()); j++) - { - dumpstr += " "; - } - dumpstr += m_codetype[i]; - dumpstr += "\n"; - } - - return dumpstr; - } - - public void add_code_sample(int code) - { - m_codetype[code]++; - } - - private String fix_string_wrap(String str, int len) - { - String ret = str; - if ((int) ret.length() < len) - { - for (int i = 0; i < len - ret.length() + 1; i++) - { - ret += " "; - } - } - return ret; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/profilefuncele.java b/src/main/java/com/github/esrrhs/fakescript/profilefuncele.java deleted file mode 100644 index 0fd0c07..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/profilefuncele.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.github.esrrhs.fakescript; - -class profilefuncele -{ - public int m_callnum; - public long m_calltime; -} diff --git a/src/main/java/com/github/esrrhs/fakescript/routine.java b/src/main/java/com/github/esrrhs/fakescript/routine.java deleted file mode 100644 index f7d20b4..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/routine.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.github.esrrhs.fakescript; - -import java.util.ArrayList; - -class routine -{ - private fake m_f; - private int m_id; - private interpreter m_interpreter; - - public routine(fake f) - { - m_f = f; - m_interpreter = new interpreter(f); - } - - public interpreter get_interpreter() - { - return m_interpreter; - } - - public variant get_ret() - { - return m_interpreter.get_ret(); - } - - public void set_id(int id) - { - m_id = id; - } - - public int get_id() - { - return m_id; - } - - public void set_processor(processor pro) - { - m_interpreter.set_processor(pro); - } - - public void entry(variant func, ArrayList retpos) throws Exception - { - m_interpreter.call(func, retpos); - } - - public void run(int cmdnum) throws Exception - { - m_interpreter.run(cmdnum); - } - - public boolean is_end() - { - return m_interpreter.is_end(); - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/running.java b/src/main/java/com/github/esrrhs/fakescript/running.java deleted file mode 100644 index 1a3c50d..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/running.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.github.esrrhs.fakescript; - -import java.util.ArrayList; - -class running -{ - private fake m_f; - private ArrayList m_processes = new ArrayList(); - private boolean m_stepmod; - private variant_map m_gmap = new variant_map(); - - public running(fake f) - { - m_f = f; - } - - public processor cur_pro() - { - return m_processes.isEmpty() ? null : m_processes.get(m_processes.size() - 1); - } - - public void push_pro(processor pro) - { - m_processes.add(pro); - } - - public void pop_pro() - { - if (!m_processes.isEmpty()) - { - m_processes.remove(m_processes.size() - 1); - } - } - - public boolean is_stepmod() - { - return m_stepmod; - } - - public void set_stepmod(boolean stepmod) - { - this.m_stepmod = stepmod; - } - - public variant_map get_gmap() - { - return m_gmap; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/stack_variant_info.java b/src/main/java/com/github/esrrhs/fakescript/stack_variant_info.java deleted file mode 100644 index e36748e..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/stack_variant_info.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.github.esrrhs.fakescript; - -class stack_variant_info -{ - public String m_name; - public int m_line; - public int m_pos; -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/assign_stmt.java b/src/main/java/com/github/esrrhs/fakescript/syntree/assign_stmt.java deleted file mode 100644 index a986b03..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/assign_stmt.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class assign_stmt extends syntree_node -{ - public syntree_node m_var; - public syntree_node m_value; - public boolean m_isnew; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_assign_stmt; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[assign]:\n"; - ret += gentab(indent + 1); - ret += "[var]:\n"; - ret += m_var.dump(indent + 2); - ret += gentab(indent + 1); - ret += "[value]:\n"; - ret += m_value.dump(indent + 2); - return ret; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/block_node.java b/src/main/java/com/github/esrrhs/fakescript/syntree/block_node.java deleted file mode 100644 index 1a71027..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/block_node.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -import java.util.ArrayList; - -public class block_node extends syntree_node -{ - public ArrayList m_stmtlist = new ArrayList(); - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_block; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[block]:\n"; - for (int i = 0; i < (int)m_stmtlist.size(); i++) - { - ret += gentab(indent + 1); - ret += "[stmt"; - ret += i; - ret += "]:\n"; - ret += m_stmtlist.get(i).dump(indent + 2); - } - return ret; - } - - public void add_stmt(syntree_node stmt) - { - m_stmtlist.add(stmt); - } - -} \ No newline at end of file diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/break_stmt.java b/src/main/java/com/github/esrrhs/fakescript/syntree/break_stmt.java deleted file mode 100644 index dd56270..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/break_stmt.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class break_stmt extends syntree_node -{ - @Override - public esyntreetype gettype() - { - return esyntreetype.est_break; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[break]:\n"; - return ret; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/cmp_stmt.java b/src/main/java/com/github/esrrhs/fakescript/syntree/cmp_stmt.java deleted file mode 100644 index 2238267..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/cmp_stmt.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class cmp_stmt extends syntree_node -{ - public String m_cmp; - public syntree_node m_left; - public syntree_node m_right; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_cmp_stmt; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[cmp]:"; - ret += m_cmp; - ret += "\n"; - if (m_left != null) - { - ret += gentab(indent + 1); - ret += "[left]:\n"; - ret += m_left.dump(indent + 2); - } - if (m_right != null) - { - ret += gentab(indent + 1); - ret += "[right]:\n"; - ret += m_right.dump(indent + 2); - } - return ret; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/const_array_list_value_node.java b/src/main/java/com/github/esrrhs/fakescript/syntree/const_array_list_value_node.java deleted file mode 100644 index 694b161..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/const_array_list_value_node.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -import java.util.ArrayList; - -public class const_array_list_value_node extends syntree_node -{ - public ArrayList m_lists = new ArrayList(); - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_constarraylist; - } - - @Override - public String dump(int indent) - { - String ret = ""; - for (int i = 0; i < (int) m_lists.size(); i++) - { - ret += gentab(indent); - ret += i; - ret += "\n"; - ret += m_lists.get(i).dump(indent + 1); - } - return ret; - } - - public void add_ele(syntree_node e) - { - m_lists.add(e); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/const_map_list_value_node.java b/src/main/java/com/github/esrrhs/fakescript/syntree/const_map_list_value_node.java deleted file mode 100644 index fdae7ef..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/const_map_list_value_node.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -import java.util.ArrayList; - -public class const_map_list_value_node extends syntree_node -{ - public ArrayList m_lists = new ArrayList(); - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_constmaplist; - } - - @Override - public String dump(int indent) - { - String ret = ""; - for (int i = 0; i < (int) m_lists.size(); i++) - { - ret += gentab(indent); - ret += i; - ret += "\n"; - ret += m_lists.get(i).dump(indent + 1); - } - return ret; - } - - public void add_ele(syntree_node e) - { - m_lists.add(e); - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/const_map_value_node.java b/src/main/java/com/github/esrrhs/fakescript/syntree/const_map_value_node.java deleted file mode 100644 index cc3c864..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/const_map_value_node.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class const_map_value_node extends syntree_node -{ - public syntree_node m_k; - public syntree_node m_v; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_constmapvalue; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "("; - ret += m_k.dump(indent + 1); - ret += ":\n"; - ret += m_v.dump(indent + 1); - ret += gentab(indent); - ret += ")\n"; - return ret; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/container_get_node.java b/src/main/java/com/github/esrrhs/fakescript/syntree/container_get_node.java deleted file mode 100644 index 572b3a1..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/container_get_node.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class container_get_node extends syntree_node -{ - public String m_container; - public syntree_node m_key; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_container_get; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[container_get]:\n"; - - ret += gentab(indent + 1); - ret += "[container]:"; - ret += m_container; - ret += "\n"; - - ret += gentab(indent + 1); - ret += "[key]:\n"; - ret += m_key.dump(indent + 2); - return ret; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/continue_stmt.java b/src/main/java/com/github/esrrhs/fakescript/syntree/continue_stmt.java deleted file mode 100644 index 51a4c77..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/continue_stmt.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class continue_stmt extends syntree_node -{ - @Override - public esyntreetype gettype() - { - return esyntreetype.est_continue; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[continue]:\n"; - return ret; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/else_stmt.java b/src/main/java/com/github/esrrhs/fakescript/syntree/else_stmt.java deleted file mode 100644 index f39a1e1..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/else_stmt.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class else_stmt extends syntree_node -{ - public block_node m_block; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_else_stmt; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[else]:\n"; - if (m_block != null) - { - ret += m_block.dump(indent + 1); - } - else - { - ret += gentab(indent + 1); - ret += "nil\n"; - } - return ret; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/elseif_stmt.java b/src/main/java/com/github/esrrhs/fakescript/syntree/elseif_stmt.java deleted file mode 100644 index 719fcd3..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/elseif_stmt.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class elseif_stmt extends syntree_node -{ - public cmp_stmt m_cmp; - public syntree_node m_block; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_elseif_stmt; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[elseif_stmt]:\n"; - ret += m_cmp.dump(indent + 1); - if (m_block != null) - { - ret += m_block.dump(indent + 1); - } - return ret; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/elseif_stmt_list.java b/src/main/java/com/github/esrrhs/fakescript/syntree/elseif_stmt_list.java deleted file mode 100644 index 39a18e0..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/elseif_stmt_list.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -import java.util.ArrayList; - -public class elseif_stmt_list extends syntree_node -{ - public ArrayList m_stmtlist = new ArrayList(); - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_elseif_stmt_list; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[elseif_stmt_list]:\n"; - for (int i = 0; i < (int)m_stmtlist.size(); i++) - { - ret += gentab(indent + 1); - ret += "[stmt"; - ret += i; - ret += "]:\n"; - ret += m_stmtlist.get(i).dump(indent + 2); - } - return ret; - } - - public void add_stmt(syntree_node stmt) - { - m_stmtlist.add(stmt); - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/esyntreetype.java b/src/main/java/com/github/esrrhs/fakescript/syntree/esyntreetype.java deleted file mode 100644 index 162a753..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/esyntreetype.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public enum esyntreetype -{ - est_nil, - est_func_desc, - est_arglist, - est_block, - est_while_stmt, - est_cmp_stmt, - est_if_stmt, - est_else_stmt, - est_for_stmt, - est_explicit_value, - est_return_stmt, - est_return_value_list, - est_assign_stmt, - est_math_assign_stmt, - est_variable, - est_var, - est_function_call, - est_call_arglist, - est_math_expr, - est_break, - est_identifier, - est_multi_assign_stmt, - est_var_list, - est_container_get, - est_struct_memlist, - est_struct_pointer, - est_continue, - est_sleep, - est_yield, - est_switch_stmt, - est_switch_caselist, - est_switch_case_node, - est_elseif_stmt, - est_elseif_stmt_list, - est_for_loop_stmt, - est_constmaplist, - est_constmapvalue, - est_constarraylist, -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/explicit_value_node.java b/src/main/java/com/github/esrrhs/fakescript/syntree/explicit_value_node.java deleted file mode 100644 index 29d2db5..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/explicit_value_node.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class explicit_value_node extends syntree_node -{ - public String m_str; - public explicit_value_type m_type; - public syntree_node m_v; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_explicit_value; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[explicit_value]:"; - ret += m_str; - ret += "\n"; - return ret; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/explicit_value_type.java b/src/main/java/com/github/esrrhs/fakescript/syntree/explicit_value_type.java deleted file mode 100644 index 5fcba43..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/explicit_value_type.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public enum explicit_value_type -{ - EVT_NULL, EVT_TRUE, EVT_FALSE, EVT_NUM, EVT_STR, EVT_FLOAT, EVT_UUID, EVT_MAP, EVT_ARRAY, -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/for_loop_stmt.java b/src/main/java/com/github/esrrhs/fakescript/syntree/for_loop_stmt.java deleted file mode 100644 index 122d6b4..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/for_loop_stmt.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class for_loop_stmt extends syntree_node -{ - public syntree_node m_var; - public syntree_node m_begin; - public syntree_node m_end; - public syntree_node m_add; - public block_node m_block; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_for_loop_stmt; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[for_loop_stmt]:\n"; - ret += gentab(indent + 1); - ret += "[var]:\n"; - ret += m_var.dump(indent + 2); - ret += gentab(indent + 1); - ret += "[begin]:\n"; - ret += m_begin.dump(indent + 2); - ret += gentab(indent + 1); - ret += "[end]:\n"; - ret += m_end.dump(indent + 2); - ret += gentab(indent + 1); - ret += "[add]:\n"; - ret += m_add.dump(indent + 2); - ret += gentab(indent + 1); - if (m_block != null) - { - ret += "[block]:\n"; - ret += m_block.dump(indent + 2); - } - return ret; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/for_stmt.java b/src/main/java/com/github/esrrhs/fakescript/syntree/for_stmt.java deleted file mode 100644 index d8490bc..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/for_stmt.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class for_stmt extends syntree_node -{ - public block_node m_beginblock; - public cmp_stmt m_cmp; - public block_node m_endblock; - public block_node m_block; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_for_stmt; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[for]:\n"; - ret += gentab(indent + 1); - ret += "[beginblock]:\n"; - if (m_beginblock != null) - { - ret += m_beginblock.dump(indent + 2); - } - ret += gentab(indent + 1); - ret += "[cmp]:\n"; - if (m_cmp != null) - { - ret += m_cmp.dump(indent + 2); - } - ret += gentab(indent + 1); - ret += "[endblock]:\n"; - if (m_endblock != null) - { - ret += m_endblock.dump(indent + 2); - } - ret += gentab(indent + 1); - ret += "[block]:\n"; - if (m_block != null) - { - ret += m_block.dump(indent + 2); - } - return ret; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/func_desc_arglist_node.java b/src/main/java/com/github/esrrhs/fakescript/syntree/func_desc_arglist_node.java deleted file mode 100644 index 3576be8..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/func_desc_arglist_node.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -import java.util.ArrayList; - -public class func_desc_arglist_node extends syntree_node -{ - public ArrayList m_arglist = new ArrayList(); - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_arglist; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[func_desc_arglist]:"; - for (int i = 0; i < (int)m_arglist.size(); i++) - { - ret += m_arglist.get(i); - ret += ","; - } - ret += "\n"; - return ret; - } - - public void add_arg(syntree_node p) - { - identifier_node pi = (identifier_node)(p); - m_arglist.add(pi.m_str); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/func_desc_node.java b/src/main/java/com/github/esrrhs/fakescript/syntree/func_desc_node.java deleted file mode 100644 index d8a96a4..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/func_desc_node.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class func_desc_node extends syntree_node -{ - public String m_funcname; - public func_desc_arglist_node m_arglist; - public block_node m_block; - public int m_endline; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_func_desc; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[func_desc]:"; - ret += m_funcname; - ret += "\n"; - if (m_arglist != null) - { - ret += m_arglist.dump(indent + 1); - } - if (m_block != null) - { - ret += m_block.dump(indent + 1); - } - return ret; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/function_call_arglist_node.java b/src/main/java/com/github/esrrhs/fakescript/syntree/function_call_arglist_node.java deleted file mode 100644 index 4b1f45b..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/function_call_arglist_node.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -import java.util.ArrayList; - -public class function_call_arglist_node extends syntree_node -{ - public ArrayList m_arglist = new ArrayList(); - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_call_arglist; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[func_call_arglist]:\n"; - for (int i = 0; i < (int)m_arglist.size(); i++) - { - ret += gentab(indent + 1); - ret += "[arg"; - ret += i; - ret += "]:\n"; - ret += m_arglist.get(i).dump(indent + 2); - } - return ret; - } - - public void add_arg(syntree_node p) - { - m_arglist.add(p); - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/function_call_node.java b/src/main/java/com/github/esrrhs/fakescript/syntree/function_call_node.java deleted file mode 100644 index 200542a..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/function_call_node.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class function_call_node extends syntree_node -{ - public boolean m_fakecall; - public boolean m_classmem_call; - public String m_fuc; - public function_call_arglist_node m_arglist; - public syntree_node m_prefuc; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_function_call; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - if (m_fakecall) - { - ret += "[func_fake_call]:"; - } - else if (m_classmem_call) - { - ret += "[class_mem_call]:"; - } - else - { - ret += "[func_call]:"; - } - if (m_prefuc != null) - { - ret += m_prefuc.dump(1); - } - else - { - ret += m_fuc; - } - ret += "\n"; - if (m_arglist != null) - { - ret += m_arglist.dump(indent + 1); - } - return ret; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/identifier_node.java b/src/main/java/com/github/esrrhs/fakescript/syntree/identifier_node.java deleted file mode 100644 index fbd0713..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/identifier_node.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class identifier_node extends syntree_node -{ - public String m_str; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_identifier; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[identifier]:"; - ret += m_str; - ret += "\n"; - return ret; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/if_stmt.java b/src/main/java/com/github/esrrhs/fakescript/syntree/if_stmt.java deleted file mode 100644 index dc6fabf..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/if_stmt.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class if_stmt extends syntree_node -{ - public cmp_stmt m_cmp; - public block_node m_block; - public elseif_stmt_list m_elseifs; - public else_stmt m_elses; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_if_stmt; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[if]:\n"; - ret += m_cmp.dump(indent + 1); - if (m_block != null) - { - ret += m_block.dump(indent + 1); - } - if (m_elseifs != null) - { - ret += m_elseifs.dump(indent + 1); - } - if (m_elses != null) - { - ret += m_elses.dump(indent); - } - return ret; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/math_assign_stmt.java b/src/main/java/com/github/esrrhs/fakescript/syntree/math_assign_stmt.java deleted file mode 100644 index 6e1b34c..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/math_assign_stmt.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class math_assign_stmt extends syntree_node -{ - public syntree_node m_var; - public String m_oper; - public syntree_node m_value; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_math_assign_stmt; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[mathassign]:"; - ret += m_oper; - ret += "\n"; - ret += gentab(indent + 1); - ret += "[var]:\n"; - ret += m_var.dump(indent + 2); - ret += gentab(indent + 1); - ret += "[value]:\n"; - ret += m_value.dump(indent + 2); - return ret; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/math_expr_node.java b/src/main/java/com/github/esrrhs/fakescript/syntree/math_expr_node.java deleted file mode 100644 index 0cfb2b7..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/math_expr_node.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class math_expr_node extends syntree_node -{ - public String m_oper; - public syntree_node m_left; - public syntree_node m_right; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_math_expr; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[math_expr]:"; - ret += m_oper; - ret += "\n"; - ret += gentab(indent + 1); - ret += "[left]:\n"; - ret += m_left.dump(indent + 2); - ret += gentab(indent + 1); - ret += "[right]:\n"; - ret += m_right.dump(indent + 2); - return ret; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/multi_assign_stmt.java b/src/main/java/com/github/esrrhs/fakescript/syntree/multi_assign_stmt.java deleted file mode 100644 index f6f033e..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/multi_assign_stmt.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class multi_assign_stmt extends syntree_node -{ - public var_list_node m_varlist; - public syntree_node m_value; - public boolean m_isnew; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_multi_assign_stmt; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[multi_assign]:\n"; - - ret += gentab(indent + 1); - ret += "[var]:\n"; - ret += m_varlist.dump(indent + 2); - - ret += gentab(indent + 1); - ret += "[value]:\n"; - ret += m_value.dump(indent + 2); - return ret; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/return_stmt.java b/src/main/java/com/github/esrrhs/fakescript/syntree/return_stmt.java deleted file mode 100644 index d0c6936..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/return_stmt.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class return_stmt extends syntree_node -{ - public return_value_list_node m_returnlist; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_return_stmt; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[return]:"; - ret += "\n"; - if (m_returnlist != null) - { - ret += m_returnlist.dump(indent + 1); - } - return ret; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/return_value_list_node.java b/src/main/java/com/github/esrrhs/fakescript/syntree/return_value_list_node.java deleted file mode 100644 index d49e084..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/return_value_list_node.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -import java.util.ArrayList; - -public class return_value_list_node extends syntree_node -{ - public ArrayList m_returnlist = new ArrayList();; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_return_value_list; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[return_value_list]:\n"; - for (int i = 0; i < (int)m_returnlist.size(); i++) - { - ret += m_returnlist.get(i).dump(indent + 1); - } - return ret; - } - - public void add_arg(syntree_node stmt) - { - m_returnlist.add(stmt); - } -} \ No newline at end of file diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/sleep_stmt.java b/src/main/java/com/github/esrrhs/fakescript/syntree/sleep_stmt.java deleted file mode 100644 index 3f9b2ff..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/sleep_stmt.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class sleep_stmt extends syntree_node -{ - public syntree_node m_time; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_sleep; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[sleep]:\n"; - ret += gentab(indent + 1); - ret += "[time]:\n"; - ret += m_time.dump(indent + 2); - return ret; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/struct_pointer_node.java b/src/main/java/com/github/esrrhs/fakescript/syntree/struct_pointer_node.java deleted file mode 100644 index 47e705d..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/struct_pointer_node.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class struct_pointer_node extends syntree_node -{ - public String m_str; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_struct_pointer; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[struct_pointer_node]:"; - ret += m_str; - ret += "\n"; - return ret; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/switch_case_node.java b/src/main/java/com/github/esrrhs/fakescript/syntree/switch_case_node.java deleted file mode 100644 index 991ad72..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/switch_case_node.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class switch_case_node extends syntree_node -{ - public syntree_node m_cmp; - public syntree_node m_block; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_switch_case_node; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[case]:\n"; - ret += gentab(indent + 1); - ret += "[cmp]:\n"; - ret += m_cmp.dump(indent + 2); - ret += gentab(indent + 1); - if (m_block != null) - { - ret += "[block]:\n"; - ret += m_block.dump(indent + 2); - } - return ret; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/switch_caselist_node.java b/src/main/java/com/github/esrrhs/fakescript/syntree/switch_caselist_node.java deleted file mode 100644 index 71e46d5..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/switch_caselist_node.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -import java.util.ArrayList; - -public class switch_caselist_node extends syntree_node -{ - public ArrayList m_list = new ArrayList(); - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_switch_caselist; - } - - @Override - public String dump(int indent) - { - String ret = ""; - for (int i = 0; i < (int)m_list.size(); i++) - { - ret += m_list.get(i).dump(indent); - } - return ret; - } - - public void add_case(syntree_node p) - { - m_list.add(p); - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/switch_stmt.java b/src/main/java/com/github/esrrhs/fakescript/syntree/switch_stmt.java deleted file mode 100644 index 0710b15..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/switch_stmt.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class switch_stmt extends syntree_node -{ - public syntree_node m_cmp; - public syntree_node m_caselist; - public syntree_node m_def; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_switch_stmt; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[switch]:\n"; - ret += gentab(indent + 1); - ret += "[cmp]:\n"; - ret += m_cmp.dump(indent + 2); - ret += gentab(indent + 1); - ret += "[case list]:\n"; - ret += m_caselist.dump(indent + 2); - ret += gentab(indent + 1); - if (m_def != null) - { - ret += "[default]:\n"; - ret += m_def.dump(indent + 2); - } - return ret; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/syntree_node.java b/src/main/java/com/github/esrrhs/fakescript/syntree/syntree_node.java deleted file mode 100644 index 72ca29e..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/syntree_node.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class syntree_node -{ - public int m_lno = 0; - - public syntree_node() - { - } - - public esyntreetype gettype() - { - return esyntreetype.est_nil; - } - - public String gettypename() - { - return gettype().toString(); - } - - public String dump(int indent) - { - return gentab(indent) + "nil\n"; - } - - public String gentab(int indent) - { - String ret = ""; - ret += "LINE:"; - ret += m_lno; - ret += " "; - for (int i = 0; i < indent; i++) - { - ret += "\t"; - } - return ret; - } - - public int lineno() - { - return m_lno; - } - -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/var_list_node.java b/src/main/java/com/github/esrrhs/fakescript/syntree/var_list_node.java deleted file mode 100644 index 2276b34..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/var_list_node.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -import java.util.ArrayList; - -public class var_list_node extends syntree_node -{ - public ArrayList m_varlist = new ArrayList();; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_var_list; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[var_list]:\n"; - for (int i = 0; i < (int)m_varlist.size(); i++) - { - ret += m_varlist.get(i).dump(indent + 1); - } - return ret; - } - - public void add_arg(syntree_node stmt) - { - m_varlist.add(stmt); - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/var_node.java b/src/main/java/com/github/esrrhs/fakescript/syntree/var_node.java deleted file mode 100644 index a994454..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/var_node.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class var_node extends syntree_node -{ - public String m_str; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_var; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[var]:"; - ret += m_str; - ret += "\n"; - return ret; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/variable_node.java b/src/main/java/com/github/esrrhs/fakescript/syntree/variable_node.java deleted file mode 100644 index f6e2c9a..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/variable_node.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class variable_node extends syntree_node -{ - public String m_str; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_variable; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[variable]:"; - ret += m_str; - ret += "\n"; - return ret; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/while_stmt.java b/src/main/java/com/github/esrrhs/fakescript/syntree/while_stmt.java deleted file mode 100644 index 10ecd86..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/while_stmt.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class while_stmt extends syntree_node -{ - public cmp_stmt m_cmp; - public block_node m_block; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_while_stmt; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[while]:\n"; - ret += m_cmp.dump(indent + 1); - if (m_block != null) - { - ret += m_block.dump(indent + 1); - } - else - { - ret += gentab(indent + 1); - ret += "nil\n"; - } - return ret; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/esrrhs/fakescript/syntree/yield_stmt.java b/src/main/java/com/github/esrrhs/fakescript/syntree/yield_stmt.java deleted file mode 100644 index db5045d..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/syntree/yield_stmt.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.github.esrrhs.fakescript.syntree; - -public class yield_stmt extends syntree_node -{ - public syntree_node m_time; - - @Override - public esyntreetype gettype() - { - return esyntreetype.est_yield; - } - - @Override - public String dump(int indent) - { - String ret = ""; - ret += gentab(indent); - ret += "[yield]:\n"; - ret += gentab(indent + 1); - ret += "[time]:\n"; - ret += m_time.dump(indent + 2); - return ret; - } -} - diff --git a/src/main/java/com/github/esrrhs/fakescript/types.java b/src/main/java/com/github/esrrhs/fakescript/types.java deleted file mode 100644 index 5e70e57..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/types.java +++ /dev/null @@ -1,255 +0,0 @@ -package com.github.esrrhs.fakescript; - -import java.util.Iterator; -import java.util.Map.Entry; - -class types -{ - public static void seterror(fake f, String file, int lineno, String func, String errorstr, Object... args) - { - f.error = true; - f.errorstr = String.format(errorstr, args); - - if (f.cb != null) - { - f.cb.on_error(f, file, lineno, func, f.errorstr); - } - } - - public static void log(fake f, String str, Object... args) - { - if (f.cfg.open_debug_log != 0) - { - if (args.length == 0) - { - System.out.println(str); - } - else - { - System.out.printf(str, args); - System.out.println(""); - } - } - } - - public static String show_exception(Exception e) - { - String ret = ""; - for (StackTraceElement se : e.getStackTrace()) - { - ret += se.toString() + "\n"; - } - ret += e.toString(); - return ret; - } - - public static String gen_package_name(String p, String n) - { - if (p.isEmpty()) - { - return n; - } - else - { - return p + "." + n; - } - } - - public static boolean isint(double d) - { - long l = (long) d; - return l == d; - } - - public static String pointertoa(Object o) - { - if (o == null) - { - return ""; - } - return o.toString(); - } - - public static String arraytoa(Object o) - { - variant_array va = (variant_array) o; - if (va.m_recur != 0) - { - return "ARRAY IN RECUR"; - } - va.m_recur++; - - String ret = ""; - ret += "["; - - for (int i = 0; i < va.m_va.size(); i++) - { - variant n = va.m_va.get(i); - if (n != null) - { - ret += n.toString(); - } - else - { - ret += " "; - } - ret += ","; - } - - ret += "]"; - - va.m_recur--; - - return ret; - } - - public static String maptoa(Object o) - { - variant_map vm = (variant_map) o; - if (vm.m_recur != 0) - { - return "MAP IN RECUR"; - } - vm.m_recur++; - - String ret = ""; - ret += "{"; - int i = 0; - Iterator> it = vm.m_vm.entrySet().iterator(); - while (it.hasNext()) - { - Entry entry = it.next(); - variant kv = (variant) entry.getKey(); - variant vv = (variant) entry.getValue(); - if (i == 0) - { - ret += "("; - } - else - { - ret += ",("; - } - - ret += kv.toString(); - ret += ","; - ret += vv.toString(); - ret += ")"; - - i++; - } - - vm.m_recur--; - - return ret; - } - - public static String dump_addr(int code) - { - String ret = ""; - int addrtype = command.HIINT16(code); - int pos = command.LOINT16(code); - switch (addrtype) - { - case command.ADDR_STACK: - ret += "STACK"; - break; - case command.ADDR_CONST: - ret += "CONST"; - break; - case command.ADDR_CONTAINER: - ret += "CONTAINER"; - break; - default: - ret += "unknow "; - ret += addrtype; - } - ret += "\t"; - ret += pos; - return ret; - } - - public static String OpCodeStr(int opcode) - { - switch (opcode) - { - case command.OPCODE_ASSIGN: - return "ASSIGN"; - case command.OPCODE_PLUS: - return "PLUS"; - case command.OPCODE_MINUS: - return "MINUS"; - case command.OPCODE_MULTIPLY: - return "MULTIPLY"; - case command.OPCODE_DIVIDE: - return "DIVIDE"; - case command.OPCODE_DIVIDE_MOD: - return "DIVIDE_MOD"; - case command.OPCODE_STRING_CAT: - return "STRING_CAT"; - case command.OPCODE_PLUS_ASSIGN: - return "PLUS_ASSIGN"; - case command.OPCODE_MINUS_ASSIGN: - return "MINUS_ASSIGN"; - case command.OPCODE_MULTIPLY_ASSIGN: - return "MULTIPLY_ASSIGN"; - case command.OPCODE_DIVIDE_ASSIGN: - return "DIVIDE_ASSIGN"; - case command.OPCODE_DIVIDE_MOD_ASSIGN: - return "DIVIDE_MOD_ASSIGN"; - case command.OPCODE_RETURN: - return "RETURN"; - case command.OPCODE_JNE: - return "JNE"; - case command.OPCODE_JMP: - return "JMP"; - case command.OPCODE_FORBEGIN: - return "FORBEGIN"; - case command.OPCODE_FORLOOP: - return "FORLOOP"; - case command.OPCODE_AND: - return "AND"; - case command.OPCODE_OR: - return "OR"; - case command.OPCODE_LESS: - return "LESS"; - case command.OPCODE_MORE: - return "MORE"; - case command.OPCODE_EQUAL: - return "EQUAL"; - case command.OPCODE_MOREEQUAL: - return "MOREEQUAL"; - case command.OPCODE_LESSEQUAL: - return "LESSEQUAL"; - case command.OPCODE_NOTEQUAL: - return "NOTEQUAL"; - case command.OPCODE_NOT: - return "NOT"; - case command.OPCODE_AND_JNE: - return "AND_JNE"; - case command.OPCODE_OR_JNE: - return "OR_JNE"; - case command.OPCODE_LESS_JNE: - return "LESS_JNE"; - case command.OPCODE_MORE_JNE: - return "MORE_JNE"; - case command.OPCODE_EQUAL_JNE: - return "EQUAL_JNE"; - case command.OPCODE_MOREEQUAL_JNE: - return "MOREEQUAL_JNE"; - case command.OPCODE_LESSEQUAL_JNE: - return "LESSEQUAL_JNE"; - case command.OPCODE_NOTEQUAL_JNE: - return "NOTEQUAL_JNE"; - case command.OPCODE_NOT_JNE: - return "NOT_JNE"; - case command.OPCODE_CALL: - return "CALL"; - case command.OPCODE_SLEEP: - return "SLEEP"; - case command.OPCODE_YIELD: - return "YIELD"; - } - return "unknow"; - } - -} diff --git a/src/main/java/com/github/esrrhs/fakescript/variant.java b/src/main/java/com/github/esrrhs/fakescript/variant.java deleted file mode 100644 index 70a73ef..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/variant.java +++ /dev/null @@ -1,423 +0,0 @@ -package com.github.esrrhs.fakescript; - -class variant -{ - // type - public variant_type m_type; - - // data - public Object m_data; - - public variant() - { - set_nil(); - } - - public String toString() - { - String ss = ""; - if (m_type == variant_type.REAL) - { - double real = (double) (Double) m_data; - if (types.isint(real)) - { - ss = "" + (long) (double) real; - } - else - { - ss = "" + real; - } - } - else if (m_type == variant_type.STRING) - { - ss = (String) (m_data); - } - else if (m_type == variant_type.UUID) - { - ss = "" + (long) (Long) m_data; - } - else if (m_type == variant_type.POINTER) - { - ss = "" + m_data; - } - else if (m_type == variant_type.ARRAY) - { - ss = types.arraytoa(m_data); - } - else if (m_type == variant_type.MAP) - { - ss = types.maptoa(m_data); - } - else if (m_type == variant_type.NIL) - { - ss = "nil"; - } - else - { - ss = "ERROR"; - } - return ss; - } - - public void set_nil() - { - m_type = variant_type.NIL; - m_data = null; - } - - public void set_pointer(Object o) - { - m_type = variant_type.POINTER; - m_data = o; - } - - public void set_real(double d) - { - m_type = variant_type.REAL; - m_data = d; - } - - public void set_string(String s) - { - m_type = variant_type.STRING; - m_data = s; - } - - public void set_uuid(long l) - { - m_type = variant_type.UUID; - m_data = l; - } - - public void set_array(variant_array va) - { - m_type = variant_type.ARRAY; - m_data = va; - } - - public void set_map(variant_map vm) - { - m_type = variant_type.MAP; - m_data = vm; - } - - public Object get_pointer() throws Exception - { - if (m_type != variant_type.POINTER && m_type != variant_type.NIL) - { - throw new Exception("variant get pointer fail, the variant is " + m_type.toString() + m_data.toString()); - } - return m_data; - } - - public double get_real() throws Exception - { - if (m_type != variant_type.REAL && m_type != variant_type.NIL) - { - throw new Exception("variant get real fail, the variant is " + m_type.toString() + m_data.toString()); - } - return m_data == null ? 0 : (double) (Double) m_data; - } - - public String get_string() throws Exception - { - if (m_type != variant_type.STRING && m_type != variant_type.NIL) - { - throw new Exception("variant get string fail, the variant is " + m_type.toString() + m_data.toString()); - } - return m_data == null ? "" : (String) m_data; - } - - public long get_uuid() throws Exception - { - if (m_type != variant_type.STRING && m_type != variant_type.NIL) - { - throw new Exception("variant get uuid fail, the variant is " + m_type.toString() + m_data.toString()); - } - return m_data == null ? 0 : (long) (Long) m_data; - } - - public variant_map get_map() throws Exception - { - if (m_type != variant_type.MAP) - { - throw new Exception("variant get map fail, the variant is " + m_type.toString() + m_data.toString()); - } - return (variant_map) m_data; - } - - public variant_array get_array() throws Exception - { - if (m_type != variant_type.ARRAY) - { - throw new Exception("variant get array fail, the variant is " + m_type.toString() + m_data.toString()); - } - return (variant_array) m_data; - } - - public void assert_can_cal() throws Exception - { - if (m_type != variant_type.REAL && m_type != variant_type.NIL) - { - throw new Exception("variant can not calculate, the variant is " + m_type.toString() + " " - + (m_data != null ? m_data.toString() : "null")); - } - } - - public void assert_can_divide() throws Exception - { - if (((double) (Double) m_data) == 0) - { - throw new Exception( - "variant can not be divide, the variant is " + m_type.toString() + " " + m_data.toString()); - } - } - - public void plus(variant l, variant r) throws Exception - { - l.assert_can_cal(); - r.assert_can_cal(); - m_data = (double) (Double) l.m_data + (double) (Double) r.m_data; - m_type = variant_type.REAL; - } - - public void minus(variant l, variant r) throws Exception - { - l.assert_can_cal(); - r.assert_can_cal(); - m_data = (double) (Double) l.m_data - (double) (Double) r.m_data; - m_type = variant_type.REAL; - } - - public void multiply(variant l, variant r) throws Exception - { - l.assert_can_cal(); - r.assert_can_cal(); - m_data = (double) (Double) l.m_data * (double) (Double) r.m_data; - m_type = variant_type.REAL; - } - - public void divide(variant l, variant r) throws Exception - { - l.assert_can_cal(); - r.assert_can_cal(); - r.assert_can_divide(); - m_data = (double) (Double) l.m_data / (double) (Double) r.m_data; - m_type = variant_type.REAL; - } - - public void divide_mod(variant l, variant r) throws Exception - { - l.assert_can_cal(); - r.assert_can_cal(); - r.assert_can_divide(); - m_data = (double) ((long) (double) (Double) l.m_data % (long) (double) (Double) r.m_data); - m_type = variant_type.REAL; - } - - public void string_cat(variant l, variant r) throws Exception - { - set_string(l.toString() + r.toString()); - } - - public void and(variant l, variant r) throws Exception - { - l.assert_can_cal(); - r.assert_can_cal(); - m_data = (((double) (Double) l.m_data != 0) & ((double) (Double) r.m_data != 0)) ? (double) 1 : (double) 0; - m_type = variant_type.REAL; - } - - public void or(variant l, variant r) throws Exception - { - l.assert_can_cal(); - r.assert_can_cal(); - m_data = (((double) (Double) l.m_data != 0) | ((double) (Double) r.m_data != 0)) ? (double) 1 : (double) 0; - m_type = variant_type.REAL; - } - - public void less(variant l, variant r) throws Exception - { - l.assert_can_cal(); - r.assert_can_cal(); - m_data = (double) (Double) l.m_data < (double) (Double) r.m_data ? (double) 1 : (double) 0; - m_type = variant_type.REAL; - } - - public void more(variant l, variant r) throws Exception - { - l.assert_can_cal(); - r.assert_can_cal(); - m_data = (double) (Double) l.m_data > (double) (Double) r.m_data ? (double) 1 : (double) 0; - m_type = variant_type.REAL; - } - - public void equal(variant l, variant r) throws Exception - { - m_data = l.equals(r) ? (double) 1 : (double) 0; - m_type = variant_type.REAL; - } - - public void less_equal(variant l, variant r) throws Exception - { - l.assert_can_cal(); - r.assert_can_cal(); - m_data = (double) (Double) l.m_data <= (double) (Double) r.m_data ? (double) 1 : (double) 0; - m_type = variant_type.REAL; - } - - public void more_equal(variant l, variant r) throws Exception - { - l.assert_can_cal(); - r.assert_can_cal(); - m_data = (double) (Double) l.m_data >= (double) (Double) r.m_data ? (double) 1 : (double) 0; - m_type = variant_type.REAL; - } - - public void not_equal(variant l, variant r) throws Exception - { - m_data = l.equals(r) ? (double) 0 : (double) 1; - m_type = variant_type.REAL; - } - - public void not(variant r) throws Exception - { - m_data = ((double) (Double) r.m_data != 0) ? (double) 0 : (double) 1; - m_type = variant_type.REAL; - } - - public static boolean and_jne(variant l, variant r) throws Exception - { - l.assert_can_cal(); - r.assert_can_cal(); - return ((double) (Double) l.m_data != 0) & ((double) (Double) r.m_data != 0); - } - - public static boolean or_jne(variant l, variant r) throws Exception - { - l.assert_can_cal(); - r.assert_can_cal(); - return ((double) (Double) l.m_data != 0) | ((double) (Double) r.m_data != 0); - } - - public static boolean less_jne(variant l, variant r) throws Exception - { - l.assert_can_cal(); - r.assert_can_cal(); - return ((double) (Double) l.m_data) < ((double) (Double) r.m_data); - } - - public static boolean more_jne(variant l, variant r) throws Exception - { - l.assert_can_cal(); - r.assert_can_cal(); - return ((double) (Double) l.m_data) > ((double) (Double) r.m_data); - } - - public static boolean equal_jne(variant l, variant r) throws Exception - { - return l.equals(r); - } - - public static boolean more_equal_jne(variant l, variant r) throws Exception - { - l.assert_can_cal(); - r.assert_can_cal(); - return ((double) (Double) l.m_data) >= ((double) (Double) r.m_data); - } - - public static boolean less_equal_jne(variant l, variant r) throws Exception - { - l.assert_can_cal(); - r.assert_can_cal(); - return ((double) (Double) l.m_data) <= ((double) (Double) r.m_data); - } - - public static boolean not_equal_jne(variant l, variant r) throws Exception - { - return !l.equals(r); - } - - public static boolean not_jne(variant r) throws Exception - { - r.assert_can_cal(); - return ((double) (Double) r.m_data) == 0; - } - - public boolean bool() - { - return ((double) (Double) m_data) != 0; - } - - @Override - public int hashCode() - { - return m_data != null ? m_data.hashCode() : 0; - } - - @Override - public boolean equals(Object o) - { - if (this == o) - { - return true; - } - - if (o == null || getClass() != o.getClass()) - { - return false; - } - - variant r = (variant) o; - - if ((m_type == variant_type.NIL && r.m_type == variant_type.POINTER && r.m_data == null) - || (m_type == variant_type.POINTER && m_data == null && r.m_type == variant_type.NIL)) - { - return true; - } - - if (m_type != r.m_type) - { - return false; - } - - if (m_type == variant_type.REAL) - { - return (double) (Double) m_data == (double) (Double) r.m_data; - } - else if (m_type == variant_type.STRING) - { - return ((String) m_data).equals(r.m_data); - } - else if (m_type == variant_type.UUID) - { - return (long) (Long) m_data == (long) (Long) r.m_data; - } - else if (m_type == variant_type.POINTER) - { - return m_data == r.m_data; - } - else if (m_type == variant_type.ARRAY) - { - return m_data == r.m_data; - } - else if (m_type == variant_type.MAP) - { - return m_data == r.m_data; - } - else if (m_type == variant_type.NIL) - { - return true; - } - else - { - return false; - } - } - - public void copy_from(variant r) - { - m_type = r.m_type; - m_data = r.m_data; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/variant_array.java b/src/main/java/com/github/esrrhs/fakescript/variant_array.java deleted file mode 100644 index 56ab223..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/variant_array.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.github.esrrhs.fakescript; - -import java.util.ArrayList; - -class variant_array -{ - public ArrayList m_va = new ArrayList(); - public boolean m_isconst; - public int m_recur; - - public variant con_array_get(variant kv) throws Exception - { - int i = (int) kv.get_real(); - - if (i < 0) - { - throw new Exception("interpreter get array variant fail, index " + i); - } - - if (i >= m_va.size()) - { - int num = i - m_va.size() + 1; - for (int j = 0; j < num; j++) - { - m_va.add(null); - } - } - - variant vv = m_va.get(i); - if (vv == null) - { - vv = new variant(); - m_va.set(i, vv); - } - return vv; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/variant_map.java b/src/main/java/com/github/esrrhs/fakescript/variant_map.java deleted file mode 100644 index 58e67df..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/variant_map.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.github.esrrhs.fakescript; - -import java.util.HashMap; - -class variant_map -{ - public HashMap m_vm = new HashMap(); - public boolean m_isconst; - public int m_recur; - - public variant con_map_get(variant kv) - { - variant vv = m_vm.get(kv); - if (vv == null) - { - variant newkv = new variant(); - newkv.copy_from(kv); - vv = new variant(); - m_vm.put(newkv, vv); - } - return vv; - } -} diff --git a/src/main/java/com/github/esrrhs/fakescript/variant_type.java b/src/main/java/com/github/esrrhs/fakescript/variant_type.java deleted file mode 100644 index 4bbfa47..0000000 --- a/src/main/java/com/github/esrrhs/fakescript/variant_type.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.github.esrrhs.fakescript; - -enum variant_type -{ - NIL, - REAL, // 参与计算的数值 - STRING, // 字符串 - POINTER, // 指针 - UUID, // int64的uuid,不参与计算,为了效率 - ARRAY, // 数组 - MAP, // 集合 -} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/ArithTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/ArithTest.java new file mode 100644 index 0000000..44a1402 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/ArithTest.java @@ -0,0 +1,207 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * 算术测试 —— 与真实 {@code lua5.4} 逐例比对。 + *

+ * 为什么值得单独做:这套算术同时服务于编译期常量折叠和将来的 VM。 + * 两边只要有一处与 Lua 不一致,就会出现「写成常量算出一个值、写成变量算出 + * 另一个值」——这类分歧在正常测试里根本看不出来。 + *

+ * 比对方式:把每个用例同时写成 Lua 表达式交给参考实现求值,和 + * {@link Arith#rawArith} 的结果逐例对照(含 {@code math.type} 的子类型, + * 因为 {@code 3 // 1} 是 integer 而 {@code 3 // 1.0} 是 float)。 + */ +class ArithTest { + + @BeforeAll + static void requireReference() { + Assumptions.assumeTrue(ReferenceLua.available(), + "找不到 lua5.4 参考二进制,跳过算术差分比对:" + ReferenceLua.describe()); + } + + private static LuaValue i(long v) { + return LuaValue.ofInteger(v); + } + + private static LuaValue f(double v) { + return LuaValue.ofFloat(v); + } + + /** 一个用例:Lua 表达式 + 我们这边对应的操作与操作数 */ + private static final class Case { + final String expr; + final int op; + final LuaValue a; + final LuaValue b; + + Case(String expr, int op, LuaValue a, LuaValue b) { + this.expr = expr; + this.op = op; + this.a = a; + this.b = b; + } + } + + private static List cases() { + List cs = new ArrayList<>(); + // 算术 + cs.add(new Case("1 + 2", Arith.OP_ADD, i(1), i(2))); + cs.add(new Case("5 - 8", Arith.OP_SUB, i(5), i(8))); + cs.add(new Case("6 * 7", Arith.OP_MUL, i(6), i(7))); + cs.add(new Case("7 // 2", Arith.OP_IDIV, i(7), i(2))); + cs.add(new Case("(0-7) // 2", Arith.OP_IDIV, i(-7), i(2))); + cs.add(new Case("7 // (0-2)", Arith.OP_IDIV, i(7), i(-2))); + cs.add(new Case("7 % 3", Arith.OP_MOD, i(7), i(3))); + cs.add(new Case("(0-7) % 3", Arith.OP_MOD, i(-7), i(3))); + cs.add(new Case("7 % (0-3)", Arith.OP_MOD, i(7), i(-3))); + cs.add(new Case("(0-7) % (0-3)", Arith.OP_MOD, i(-7), i(-3))); + cs.add(new Case("2 ^ 10", Arith.OP_POW, i(2), i(10))); + cs.add(new Case("2 ^ 2", Arith.OP_POW, i(2), i(2))); + cs.add(new Case("7 / 2", Arith.OP_DIV, i(7), i(2))); + // 浮点 + cs.add(new Case("1.5 + 2.25", Arith.OP_ADD, f(1.5), f(2.25))); + cs.add(new Case("7.0 // 2", Arith.OP_IDIV, f(7.0), i(2))); + cs.add(new Case("(0-7.0) // 2", Arith.OP_IDIV, f(-7.0), i(2))); + cs.add(new Case("7.5 % 2", Arith.OP_MOD, f(7.5), i(2))); + cs.add(new Case("(0-7.5) % 2", Arith.OP_MOD, f(-7.5), i(2))); + cs.add(new Case("2.0 ^ 0.5", Arith.OP_POW, f(2.0), f(0.5))); + // 整数与浮点混合(子类型是区分点) + cs.add(new Case("3 + 1.0", Arith.OP_ADD, i(3), f(1.0))); + cs.add(new Case("3 // 1.0", Arith.OP_IDIV, i(3), f(1.0))); + cs.add(new Case("3 % 1.0", Arith.OP_MOD, i(3), f(1.0))); + // 位运算 + cs.add(new Case("12 & 10", Arith.OP_BAND, i(12), i(10))); + cs.add(new Case("12 | 10", Arith.OP_BOR, i(12), i(10))); + cs.add(new Case("12 ~ 10", Arith.OP_BXOR, i(12), i(10))); + cs.add(new Case("1 << 62", Arith.OP_SHL, i(1), i(62))); + cs.add(new Case("1 << 64", Arith.OP_SHL, i(1), i(64))); + cs.add(new Case("1 << (0-1)", Arith.OP_SHL, i(1), i(-1))); + cs.add(new Case("(0-1) >> 1", Arith.OP_SHR, i(-1), i(1))); + cs.add(new Case("(0-8) >> 2", Arith.OP_SHR, i(-8), i(2))); + cs.add(new Case("(0-8) >> 64", Arith.OP_SHR, i(-8), i(64))); cs.add(new Case("1 >> (0-1)", Arith.OP_SHR, i(1), i(-1))); + cs.add(new Case("(0-1) >> (0-1)", Arith.OP_SHR, i(-1), i(-1))); + cs.add(new Case("7 & 1.0", Arith.OP_BAND, i(7), f(1.0))); + // 一元 + cs.add(new Case("(0-5)", Arith.OP_UNM, i(5), i(0))); + cs.add(new Case("~0", Arith.OP_BNOT, i(0), i(0))); + cs.add(new Case("~12", Arith.OP_BNOT, i(12), i(0))); + cs.add(new Case("(0-1.5)", Arith.OP_UNM, f(1.5), f(0))); + // 边界:mininteger 的除法与取模不能溢出 + cs.add(new Case("math.mininteger // (0-1)", Arith.OP_IDIV, i(Long.MIN_VALUE), i(-1))); + cs.add(new Case("math.mininteger % (0-1)", Arith.OP_MOD, i(Long.MIN_VALUE), i(-1))); + cs.add(new Case("math.mininteger + 0", Arith.OP_ADD, i(Long.MIN_VALUE), i(0))); + return cs; + } + + @Test + void rawArithMatchesReferenceLua() { + List cs = cases(); + StringBuilder script = new StringBuilder(); + for (Case c : cs) { + script.append("do local R = ").append(c.expr).append("\n") + .append(" if math.type(R) == 'integer' then\n") + .append(" print('integer', tostring(R))\n") + .append(" else\n") + .append(" local s = string.pack('>d', R)\n") + .append(" local h = ''\n") + .append(" for i = 1, #s do h = h .. string.format('%02x', string.byte(s, i)) end\n") + .append(" print('float', h)\n") + .append(" end\n") + .append("end\n"); + } + ReferenceLua.Result r = ReferenceLua.run(script.toString()); + assertEquals(0, r.exitCode, r.toString()); + String[] lines = r.output.replaceAll("\r\n", "\n").split("\n"); + assertEquals(cs.size(), lines.length, "参考实现输出行数应与用例数相同:" + r.output); + + for (int n = 0; n < cs.size(); n++) { + Case c = cs.get(n); + LuaValue got = Arith.rawArith(null, c.op, c.a, c.b); + assertNotNull(got, "我们这边的运算失败了:" + c.expr); + String ours = typeOf(got) + "\t" + format(got); + assertEquals(lines[n], ours, "用例 " + c.expr); + } + } + + private static String typeOf(LuaValue v) { + return v.isInteger() ? "integer" : "float"; + } + + /** + * 整数直接打十进制;浮点打位模式的十六进制。 + *

+ * 为什么不用 {@code %.17g} 之类的十进制格式化:Java 的 {@code Formatter} + * 对 {@code %g} 不会去掉末尾的零({@code 3} 打成 + * {@code 3.0000000000000000}),而 C 会;用十进制比较等于拿格式化的差异 + * 冒充数值的差异。位模式没有这个问题,一个 ULP 都藏不住。 + */ + private static String format(LuaValue v) { + if (v.isInteger()) { + return Long.toString(v.i); + } + return String.format(Locale.ROOT, "%016x", Double.doubleToRawLongBits(v.n)); + } + + /* ---------- 不依赖参考实现的补充 ---------- */ + + @Test + void fltToIntegerFollowsLuaRules() { + assertEquals(Long.valueOf(3), Arith.fltToInteger(3.0, Arith.F2I_EQ)); + assertNull(Arith.fltToInteger(3.5, Arith.F2I_EQ), "非整值在 F2Ieq 下必须失败"); + assertEquals(Long.valueOf(3), Arith.fltToInteger(3.5, Arith.F2I_FLOOR)); + assertEquals(Long.valueOf(4), Arith.fltToInteger(3.5, Arith.F2I_CEIL)); + assertNull(Arith.fltToInteger(1e300, Arith.F2I_EQ), "超出 int64 范围必须失败"); + assertNull(Arith.fltToInteger(Double.NaN, Arith.F2I_EQ)); + } + + @Test + void shiftOutOfRangeIsZeroNotModulo() { + // Java 的 << 会把位数按 64 取模,Lua 不会:1 << 64 是 0 + assertEquals(0L, Arith.shiftl(1, 64)); + assertEquals(0L, Arith.shiftl(1, -64)); + assertEquals(0L, Arith.shiftl(1, 65)); + assertEquals(2L, Arith.shiftl(1, 1)); + assertEquals(1L, Arith.shiftl(2, -1)); + } + + @Test + void shiftRightIsLogicalNotArithmetic() { + // Lua 手册:两个移位运算符都用 0 填充空位。参考实现实测 -8 >> 2 + // 得到 4611686018427387902(0x3FFFFFFFFFFFFFFE),不是 -2。 + assertEquals(4611686018427387902L, Arith.shiftl(-8, -2)); + assertEquals(9223372036854775807L, Arith.shiftl(-1, -1)); + } + + @Test + void rawArithRejectsNonNumbers() { + assertNull(Arith.rawArith(null, Arith.OP_ADD, LuaValue.ofString(LuaString.latin1("x")), i(1))); + assertNull(Arith.rawArith(null, Arith.OP_BAND, f(1.5), i(1)), "非整值不能参与位运算"); + } + + @Test + void divisionByZeroRaises() { + assertThrowsLuaError(() -> Arith.rawArith(null, Arith.OP_IDIV, i(1), i(0))); + assertThrowsLuaError(() -> Arith.rawArith(null, Arith.OP_MOD, i(1), i(0))); + } + + private static void assertThrowsLuaError(Runnable r) { + try { + r.run(); + } catch (LuaError e) { + return; + } + throw new AssertionError("期望 LuaError"); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/BinaryChunkTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/BinaryChunkTest.java new file mode 100644 index 0000000..53d5f1e --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/BinaryChunkTest.java @@ -0,0 +1,512 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; + +/** + * 二进制 chunk(预编译字节码)的双向兼容 —— 阶段 11。 + *

+ * 为什么单独开一类:Lua 的加载器靠文件头认 chunk,不需要调用方声明。 + * {@code ldo.c: f_parser} 读一个字节,是 {@code LUA_SIGNATURE} 的首字节就走 + * {@code luaU_undump},否则才进 {@code luaY_parser}。所以 + * {@code load} / {@code loadfile} / {@code dofile} / {@code require} + * 全都天生支持预编译 chunk——而这正是真实项目的常见分发形态 + * ({@code luac -o mod.luac} 之后直接 {@code require "mod"})。 + *

+ * 本实现原先只在 {@code BaseLib} 的 {@code load} / {@code loadfile} 里做了这个 + * 分派,{@code lua_load} 本身({@link LuaState#load})不看签名,于是 + * {@code require} 一个预编译模块会报 + * {@code unexpected symbol near '<\27>'}——把签名当源码读了。修法不是给 + * {@code require} 打补丁,而是把分派补回 {@code lua_load} 那一层。 + *

+ * 两个方向都要验: + *

    + *
  • 参考实现编出来的 chunk,本实现要能跑(需要 {@code luac})。 + * 这是硬闸门:自己编给自己判卷等于没判,必须用参考实现自己的编译器。
  • + *
  • 本实现 dump 出来的 chunk,加载与执行也要一致 + * (不需要 {@code luac},用 {@code string.dump} 造 chunk)。
  • + *
+ */ +class BinaryChunkTest { + + /** 固定路径:{@code luaO_chunkid} 会把超过 {@code LUA_IDSIZE} 的源名截断 */ + private static final File DIR = new File("target/rt/chunk"); + + @BeforeAll + static void needReference() { + assumeTrue(ReferenceLua.find() != null, + "no lua5.4 reference binary; set LUA54_BIN"); + } + + @TestFactory + List precompiledChunksMatchReference() { + List tests = new ArrayList<>(); + for (int i = 0; i < SOURCES.length; i++) { + String src = SOURCES[i]; + int n = i; + tests.add(DynamicTest.dynamicTest("luac[" + n + "]", () -> { + assertTrue(DIR.isDirectory() || DIR.mkdirs(), "建不出 " + DIR); + String name = "c" + n + ".luac"; + File out = ReferenceLua.compile(src.getBytes(StandardCharsets.UTF_8), + new File(DIR, name).getPath()); + assumeTrue(out != null, "没有 luac5.4,跳过;设 LUAC54_BIN 或从源码构建"); + /* + * 比的是「同一个 chunk 文件在两边的输出」——不是「本实现编出来的 + * chunk 和参考实现编出来的 chunk 一不一样」(那是 DumpTest 的事)。 + * 这里同时压了 undump 与 VM:chunk 里的调试信息(行号、局部变量名) + * 也要原样活下来,SOURCES 里那几条会报错的用例就是盯这个的。 + */ + Diff.assertFileMatches(DIR, name); + })); + } + return tests; + } + + @TestFactory + List loadingOurOwnChunks() { + List tests = new ArrayList<>(); + for (int i = 0; i < OURS.length; i++) { + String src = OURS[i]; + int n = i; + tests.add(DynamicTest.dynamicTest("ours[" + n + "]", () -> Diff.assertMatches(src))); + } + return tests; + } + + /** + * 畸形 chunk 矩阵:把一个真实 dump 的每一个字节逐个替换成一组 + * 取值({@code H}),再叠加逐长度截断({@code T})、尾部追加垃圾 + * ({@code A})、变长整数失控注入({@code O}),外加 mode 与 chunk + * 名两组交叉,共 4136 行。 + *

+ * 这一片原本是全盲的:{@code lundump.c} 的十条 {@code error()} + * 一条都没被测到(JaCoCo 里 {@code Undump} 十行未覆盖),而 873 个用例 + * 全绿 —— 因为没有任何脚本喂过坏 chunk。扫完之后又多咬出两个真分歧, + * 见下面两处说明。 + * + *

为什么变异取值全部取高位为 1

+ * 变长整数的规则是「最高位置 1 表示最后一组」。取 {@code 0x80..0xff} + * 时每个长度都 <= 127,永远走不到「malloc 成不成」那条随机器而变 + * 的分支。取 {@code 0x00..0x7f} 会让变长整数一路吃下去,声明长度变成 + * 一个随机的 64 位数,参考实现这时给 {@code "not enough memory"} 还是 + * {@code "truncated chunk"} 取决于机器内存与过量提交策略(实测:13GB 在 + * macOS 上能成,8.3e18 不能)—— 同一份输入在本机与 CI 上答案不同, + * 所以那一档不进差分表,另用 + * {@link #hugeDeclaredStringLengthDoesNotCrash()} 非差分钉住「不崩」。 + */ + @Test + void corruptedChunkMatrixMatchesReference() { + Diff.assertMatches(""" + -- 二进制 chunk 腐败矩阵 + -- load 不抛错,返回 nil + 消息,所以看第三个返回值,不是第二个。 + local ESC = string.char(27) + local function q(s) + local t = {} + for i = 1, #s do t[#t + 1] = string.byte(s, i) end + return table.concat(t, ",") + end + local function rep(s) + local ok, a, b = pcall(load, s, "=x") + return tostring(ok), type(a), (type(b) == "string") and q(b) or tostring(b) + end + + -- 三种形态的合法 chunk:空体 / 带常量 / 带内嵌函数(调试信息布局不同) + local function mk(src) return string.dump(load(src, "=X")) end + local CH = { + mk("return 1"), + mk("local s = 'hello' local n = 3.5 local t = {1,2,3} return s, n, t"), + mk("local function g(x) return x * 2 end for i = 1, 3 do g(i) end return g"), + } + print("NCH", #CH) + for c = 1, #CH do print("LEN", c, #CH[c]) end + print("HEAD", q(string.sub(CH[1], 1, 31))) + + --[[ + 逐字节变异用的取值集合,全部高位为 1。 + 变长整数的规则是「最高位置 1 表示最后一组」,所以这样改出来的每个 + 长度都 <= 127,走不到「malloc 成不成」那条随机器而变的分支。 + 反过来,用 0x00-0x7f 去改会让变长整数一路吃下去,声明长度变成 + 一个随机的 64 位数;参考实现这时给不给 "not enough memory" 取决于 + 机器内存与过量提交策略,同一份输入在本机与 CI 上答案不同,所以那一档 + 不进差分表,另用非差分断言钉住「不崩」。 + ]] + local VALS = { 0x80, 0x81, 0x82, 0xC0, 0xFE, 0xFF } + + -- 1) 逐字节变异 + -- 头部:签名(1-4) 版本(5) 格式(6) LUAC_DATA(7-12) 三个 size(13-15) + -- LUAC_INT(16-23) LUAC_NUM(24-31) + -- 之后:nupvalues(32),再是函数体(源名、行号、指令、常量、 + -- upvalue、内嵌函数、调试信息) + for c = 1, #CH do + local base = CH[c] + for i = 1, #base do + for v = 1, #VALS do + local s = string.sub(base, 1, i - 1) .. string.char(VALS[v]) .. string.sub(base, i + 1) + print("H", c, i, VALS[v], rep(s)) + end + end + end + + -- 2) 变长整数失控:连续 n 个高位为 0 的字节。 + -- 累加到第 11 组会跨过 2^63,而溢出检查在分配之前, + -- 所以这一档跟机器内存无关,是确定的 integer overflow。 + for c = 1, #CH do + local base = CH[c] + for n = 10, 12 do + local s = string.sub(base, 1, 32) .. string.rep(string.char(1), n) + print("O", c, n, rep(s)) + end + end + + -- 3) 逐长度截断 + for c = 1, #CH do + local base = CH[c] + for n = 0, #base do + print("T", c, n, rep(string.sub(base, 1, n))) + end + end + + -- 4) 尾部追加垃圾 + for c = 1, #CH do + for v = 1, #VALS do + print("A", c, VALS[v], rep(CH[c] .. string.char(VALS[v]))) + end + end + + -- 5) mode × 二进制 / 文本 + for c = 1, #CH do + for _, m in ipairs({ "b", "t", "bt", "z", "", nil, 5 }) do + local ok, a, b = pcall(load, CH[c], "=x", m) + print("M", c, tostring(m), ok, type(a), (type(b) == "string") and q(b) or tostring(b)) + local ok2, a2, b2 = pcall(load, "return 1", "=x", m) + print("M2", c, tostring(m), ok2, type(a2), (type(b2) == "string") and q(b2) or tostring(b2)) + end + end + + -- 6) chunk 名的前缀 + for c = 1, #CH do + for _, nm in ipairs({ nil, "=x", "@x", "x", "" }) do + local ok, a, b = pcall(load, CH[c], nm) + print("N", c, tostring(nm), ok, type(a), (type(b) == "string") and q(b) or tostring(b)) + end + end + + -- 7) 边界:空 / 只带签名前缀 + local EDGE = { + "", ESC, ESC .. "L", ESC .. "Lu", ESC .. "Lua", + ESC .. "LuaT", ESC .. "LuaT" .. string.char(0), + ESC .. "LuaT" .. string.char(0, 25, 147, 13, 10, 26, 10), + ESC .. "LuaT" .. string.char(0, 25, 147, 13, 10, 26, 10, 4, 8, 8), + ESC .. "LuaT" .. string.char(0, 25, 147, 13, 10, 26, 10, 4, 8, 8, 0x78, 0x56, 0, 0, 0, 0), + } + for e = 1, #EDGE do print("E", e, #EDGE[e], rep(EDGE[e])) end + + -- 8) dump 的形态:带 upvalue 的函数 dump 不出来 / strip + local up = 7 + local function withUp() return up end + print("D1", pcall(string.dump, withUp)) + print("D2", pcall(string.dump, print)) + print("D3", pcall(string.dump, 5)) + print("D4", pcall(string.dump)) + local function plain() return 42 end + local ok5, ds = pcall(string.dump, plain, true) + local ok5b, dn = pcall(string.dump, plain, false) + if ok5 and ok5b then + print("D5", #ds, #dn) + local ok6, f6 = pcall(load, ds) + local ok7, f7 = pcall(load, dn) + if ok6 and ok7 then + print("D6", pcall(f6), pcall(f7)) + end + end + """); + } + + /** + * 「声明了一个天文数字长度的长字符串」不能把进程搞崩。 + *

+ * 这一档不进差分表:C 那头是 {@code luaS_createlngstrobj} 整块 + * malloc,成不成取决于机器内存与过量提交策略(13GB 在 macOS 上能成, + * 8.3e18 不能),同一份输入在本机与 CI 上会给不同答案。 + * 但两边都不该崩——这一点是确定的,所以单独钉在这里。 + */ + @Test + void hugeDeclaredStringLengthDoesNotCrash() { + String out = Diff.mine(""" + local base = string.dump(load("return 1", "=X")) + local f, e = load(base:sub(1, 32) .. string.rep(string.char(0), 12), "=X") + print(type(f), type(e)) + local f2, e2 = load(base:sub(1, 32) .. string.rep(string.char(127), 12), "=X") + print(type(f2), type(e2)) + """); + assertTrue(out.contains("nil\tstring"), "畸形声明长度必须给出 nil + 消息,got:\n" + out); + } + + /** + * 交给 {@code luac} 的语料。挑的都是「编译产物里信息量大的」: + * upvalue、变长参数、跳转与闭包捕获、混合表构造、行号与局部变量名。 + */ + private static final String[] SOURCES = { + + """ + local up = "upvalue" + local function f(a) + local function g(b) return a + b + #up end + return g + end + print(f(1)(2), up) + """, + + """ + local function va(...) + local n = select('#', ...) + return n, ... + end + print(va(1, nil, 3)) + print(va()) + print((function(...) return ... end)("x", "y")) + """, + + """ + for i = 1, 3 do + if i == 2 then goto continue end + print("i", i) + ::continue:: + end + local j = 0 + while j < 2 do j = j + 1 end + print(j) + """, + + """ + local t = {1, 2, 3, x = "X", [10] = "ten", "last"} + print(#t, t.x, t[10], t[4]) + local mt = setmetatable({}, {__index = function(_, k) return k * 2 end}) + print(mt[21]) + print(("abc"):upper() .. 1 .. 2.5 .. true) + """, + + """ + local max, min = math.maxinteger, math.mininteger + print(max, min, max + 1 == min, 1 / 0, -1 / 0, 0 / 0 ~= 0 / 0) + print(0x7fffffffffffffff, 9007199254740993, 1e300 * 1e300) + """, + + /* + * 行号与局部变量名:预编译 chunk 里带的是行号表与调试名, + * 跑出来的错误消息必须和源码版一模一样,否则就是 undump 丢了信息。 + */ + """ + local function boom(x) + local inner = x + error("boom " .. inner) + end + print(pcall(boom, "here")) + print(pcall(function() local a = nil; return a.b end)) + print(pcall(function() error({code = 7}) end)) + """, + + """ + local co = coroutine.create(function(a) + for i = 1, 3 do + coroutine.yield(a + i) + end + return "done" + end) + print(coroutine.resume(co, 10)) + print(coroutine.resume(co)) + print(coroutine.resume(co)) + print(coroutine.resume(co)) + print(coroutine.resume(co)) + """, + + /* + * 常量表过 256、局部变量过 200:这两条会顶到 dump 里变长整数的 + * 多字节编码与 OP_NEWTABLE 的尺寸字段。 + */ + """ + local t = {} + for i = 1, 300 do t[i] = i * i end + print(#t, t[300]) + local a1, a2 = 1, 2 + local s = 0 + for i = 1, 300 do s = s + (i % 7) end + print(s, a1 + a2) + """, + }; + + /** + * 不需要 {@code luac} 的一组:chunk 由 {@code string.dump} 当场造出来, + * 写进文件,再走加载器的各条入口。 + */ + private static final String[] OURS = { + + /* + * 这一条就是修的那个缺陷:require 走的是 LuaState#loadFile, + * 原先不看签名,报 unexpected symbol near '<\27>'。 + */ + """ + local p = "target/rt/chunk/m1.lua" + local f = io.open(p, "wb") + f:write(string.dump(function() return 1, 2, "three" end)) + f:close() + package.path = "target/rt/chunk/?.lua;" .. package.path + print(require("m1")) + print(require("m1")) + print(package.loaded.m1) + """, + + """ + local p = "target/rt/chunk/m2.lua" + local f = io.open(p, "wb") + f:write(string.dump(function(a, b) return a * b end)) + f:close() + print(dofile(p)) + print(dofile(p)) + local g, e = loadfile(p) + print(type(g), e) + print(g(6, 7)) + """, + + /* mode:二进制 chunk 只能按 "b" 加载,按 "t" 是语法期错误 */ + """ + local d = string.dump(function() return "x" end) + print(load(d, "=bin", "b")) + print(load(d, "=bin", "t")) + print(load(d, "=bin", "bt")) + print(load("return 1", "=txt", "t")) + print(load("return 1", "=txt", "b")) + """, + + /* chunk 名:以签名开头时会被换成 "binary string" */ + """ + local d = string.dump(function() return 1 end) + print(pcall(load, d)) + print(load(string.dump(function() error("inside") end))) + print(pcall(load(string.dump(function() error("inside") end)))) + """, + + /* 坏 chunk:截断与篡改都必须是「加载期错误」而不是崩 */ + """ + local d = string.dump(function() return 1 end) + print(pcall(load, d:sub(1, 20))) + print(pcall(load, "\27Lua")) + print(pcall(load, d:sub(1, 1) .. string.rep("\0", 40))) + print(load(d .. "trailing")) + """, + + /* + * 畸形 chunk 矩阵:对一个真实 dump 做逐长度截断 + 逐字节变异 + + * 变长整数上限注入 + 尾部垃圾,共 865 行,把 {@code lundump.c} + * 的每一条 {@code error()} 都点名到。 + *

+ * 为什么要「逐字节」而不是挑几个坏样本:手挑的样本只覆盖想得到的位置, + * 而变异不需要预先知道哪个字节是干什么的一一它把 + * {@code truncated chunk} / {@code integer overflow} / + * {@code bad format for constant string} / {@code version mismatch} / + * {@code format mismatch} / {@code corrupted chunk} / + * 三种 {@code size mismatch} / 两种 {@code format mismatch} + * 全都撞出来了,包括两个手挑时不会想到的地方。 + *

+ * chunk 名固定成 {@code "=X"}:{@code string.dump} 的产物里带着源名, + * 而源名默认就是脚本路径,两边路径不同会让 dump 差 7 个字节、 + * 所有偏移跟着错位,矩阵就白扫了。 + */ + + """ +-- Undump 畸形 chunk 扫描:截断 / 逐字节变异 / 变长整数注入 / 尾部垃圾 / 剥调试信息 +-- chunk 名固定为 "=X",保证 dump 与脚本所在路径无关,两边字节完全一致 +local g = load("return function() return 'hi' end", "=X")() +local base = string.dump(g) +print("len " .. #base) + +local function try(s, name) + local f, e = load(s, name) + if f then + return "ok" + end + return "err " .. tostring(e) +end + +-- 1) 逐长度截断 +for i = 0, #base do + print(string.format("trunc %d %s", i, try(base:sub(1, i), "=C"))) +end + +-- 2) 逐字节变异 +local MUT = { 0x00, 0x80, 0xff, 0x41 } +for i = 1, #base do + for _, m in ipairs(MUT) do + local s = base:sub(1, i - 1) .. string.char(m) .. base:sub(i + 1) + print(string.format("mut %d %d %s", i, m, try(s, "=C"))) + end +end + +-- 3) chunk 名对错误信息的影响(签名第二个字节 L 改掉 / 版本字节改掉) +local nosig = base:sub(1, 1) .. "X" .. base:sub(3) +print("name nil " .. try(nosig)) +print("name at " .. try(nosig, "@file.lua")) +print("name eq " .. try(nosig, "=nm")) +print("name plain " .. try(nosig, "plain")) +print("name empty " .. try(nosig, "")) +print("name sig " .. try(nosig, "\\27zz")) +local badver = base:sub(1, 4) .. string.char(0x63) .. base:sub(6) +print("ver nil " .. try(badver)) +print("ver at " .. try(badver, "@file.lua")) + +-- 4) 变长整数上限:头 31 字节 + 32 字节 upval 数,之后第一个读的就是 size +for n = 1, 12 do + local s = base:sub(1, 32) .. string.rep("\\127", n) + print(string.format("ovf %d %s", n, try(s, "=C"))) +end +-- ovf2 不比:这一条走的是「变长整数溢出后申请一块巨大的内存」,报 +-- "not enough memory" 还是 "truncated chunk" 取决于 libc 的 malloc 在什么 +-- 规模上失败 —— Linux 与 macOS 的答案不同,且不属于本实现能左右的范围。 + +-- 5) 尾部垃圾:undump 不检查 EOF +print("tail " .. try(base .. "GARBAGE", "=C")) +print("tail2 " .. try(base .. string.rep("Z", 100), "=C")) + +-- 6) 剥掉调试信息后的同一批扫描(布局不同,走的是另一组偏移) +local st = string.dump(g, true) +print("slen " .. #st) +for i = 0, #st do + print(string.format("strunc %d %s", i, try(st:sub(1, i), "=C"))) +end +for i = 1, #st do + for _, m in ipairs({ 0x00, 0x80 }) do + local s = st:sub(1, i - 1) .. string.char(m) .. st:sub(i + 1) + print(string.format("smut %d %d %s", i, m, try(s, "=C"))) + end +end + +-- 7) 带 upvalue 与内嵌函数的 chunk(结构更复杂,字段更多) +local h = load([[ + local x = 1 + local function inner(a) return a + x end + return function(b) return inner(b) * 2 end +]], "=Y")() +local big = string.dump(h) +print("blen " .. #big) +for i = 0, #big do + print(string.format("btrunc %d %s", i, try(big:sub(1, i), "=C"))) +end +for i = 1, #big do + for _, m in ipairs({ 0x00, 0x80 }) do + local s = big:sub(1, i - 1) .. string.char(m) .. big:sub(i + 1) + print(string.format("bmut %d %d %s", i, m, try(s, "=C"))) + end +end + """, + }; +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/BinarySafetyTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/BinarySafetyTest.java new file mode 100644 index 0000000..11e2c42 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/BinarySafetyTest.java @@ -0,0 +1,1183 @@ +package com.github.esrrhs.fakescript.lua; + +import java.io.File; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * 二进制安全的差分矩阵 —— 阶段 12。 + *

+ * 为什么单开一类:约定 #9/#10 说「Lua 字符串是 {@code byte[]},禁止用 Java + * String 方法处理内容」,但前面十一轮铺的网格里,含 {@code \0} 与高位字节的 + * 样本只零星出现(全仓库 27 个测试类里 {@code char(0)} 只用了几十次)。 + * 而这一片正是实现最容易错的地方——因为它要求每一层都按字节办事, + * 只要有一处退化成 Java String(UTF-8 解码 → 非法字节变 {@code U+FFFD} + * → 再编码回去),内容就静默变了。 + *

+ * 这一轮实际咬出三个真 bug,全在这条链上: + *

    + *
  • {@code l_strcmp} 用了有符号 byte。C99 规定 {@code strcoll} + * 的返回值符号由第一对不等字节「都按 {@code unsigned char} 解释」之差 + * 决定;Java 的 {@code byte} 有符号,{@code 0xFF} 被当成 {@code -1}, + * 于是 {@code "\xff" < "abc"} 从 false 翻成 true, + * {@code table.sort} 的整体次序跟着反转。本机实测 + * {@code strcoll("\xff", "abc") == 158 > 0}。
  • + *
  • 位置前缀与错误消息的拼接走了 Java String。 + * {@code errorAt} / {@code runError} / {@code assert} 都是 + * {@code latin1(prefix + msg.toJavaString())}:{@code toJavaString} + * 按 UTF-8 解码,{@code 0xFF} 变成 {@code U+FFFD}({@code 0xFFFD > 0xFF}), + * {@code latin1} 直接抛 {@code IllegalArgumentException}。 + * {@code assert(false, "\xff")} 一度把整个进程炸掉——而且是 + * Java 异常不是 LuaError,pcall 接不住(约定 #4)。
  • + *
  • {@code short_src} 与 {@code debug.traceback} 同样走 Java String。 + * {@code load(f, "=\xff")} 之后 {@code debug.getinfo(1,'S').short_src} + * 给的是 {@code "\xef\xbf\xbd"};回溯里 {@code function <...>} 那段 + * 更隐蔽——{@code 0xFFFD} 被 {@code (byte)} 截成 {@code 0xFD}, + * {@code U+4E2D} 截成 {@code '-'},长度倒是对的,只有内容错。
  • + *
+ *

+ * 四片各自成阵: + *

    + *
  • 片一:string 核心函数 × 字节样本。27 个样本(含 NUL 在头/中/尾、 + * {@code 0xFF}、四类非法 UTF-8、超长编码、代理项、合法 UTF-8)乘上 + * {@code byte/char/sub/find/match/gmatch/gsub/format/rep/reverse/ + * upper/lower/tonumber} 的参数网格。
  • + *
  • 片二:比较与排序、表键、错误消息、utf8、pack、load、io。 + * 256 个单字节串全排列的 {@code <} 真值表压成位图,一行钉住 + * 整个次序;再把带 NUL 的值当表键、当错误消息、喂给 utf8 与 + * {@code string.pack}。
  • + *
  • 片三:chunk 名本身是任意字节。{@code =} / {@code @} / 裸三种 + * 写法 × 含 NUL 与高位字节 × 跨 {@code LUA_IDSIZE} 的长度,看 + * {@code luaL_where} / {@code short_src} / {@code traceback} 三条出口。
  • + *
  • 片四:错误消息里嵌的值本身——{@code varinfo} 反推的字段名、 + * {@code __name}、{@code require} 的模块名、{@code io}/{@code os} 的文件名。 + * 这里额外咬出「名字是 C 字符串」这一层:{@code kname} 拿到的是 + * {@code getstr()} 那个 {@code const char*},{@code formatvarinfo} 用 + * {@code "%s"} 拼,于是名字在第一个 {@code \0} 处被砍断——实测 + * {@code load("local t={} return t['a\0b'].x")()} 参考实现报 + * {@code (field 'a')},而高字节不砍({@code t['\255\254']} 原样报 + * {@code (field '')})。
  • + *
+ *

+ * 片二与片四都带一遍协程第二遍(约定:同一批语料塞进 + * {@code coroutine.create} 再跑一遍)。 + * + * @see Vm#lStrcmp + * @see LuaState#errorAt + * @see LuaState#kName + * @see DebugLib#traceback + */ +class BinarySafetyTest { + + @BeforeAll + static void prepare() { + // 片二/片三会往这里写临时文件(约定 #41:开头先删一次) + new File("target/rt").mkdirs(); + } + + /** 片一:string 核心函数 x 字节样本(12000+ 行)。 */ + @Test + void stringCoreGridMatchesReference() { + Diff.assertMatches(""" + +-- 二进制安全矩阵 I:string 核心函数 x 含 NUL / 高位字节 / 非法 UTF-8 的输入 +-- 约定 #9/#10:Lua 字符串是 byte[],内容可以含 NUL、可以是非法 UTF-8。 +-- 所有特殊字节一律用 string.char 造,文件里不出现反斜杠(Java 文本块会炸)。 + +local NUL = string.char(0) +local BS = string.char(92) +local LF = string.char(10) +local CR = string.char(13) +local TAB = string.char(9) + +-- 可打印原样,其余转成 ;方括号也转义,免得跟包围符混 +local function esc(s) + return (s:gsub(".", function(c) + local b = c:byte() + if b == 91 or b == 93 then return string.format("<%.2X>", b) end + if b < 32 or b > 126 then return string.format("<%.2X>", b) end + return c + end)) +end + +local function show(v) + local t = type(v) + if t == "string" then return "#" .. #v .. "[" .. esc(v) .. "]" end + if t == "number" then + local mt = math.type(v) + if mt == "integer" then return "i" .. tostring(v) end + if mt == "float" then return "f" .. tostring(v) end + return "?" .. tostring(v) + end + if t == "nil" or t == "boolean" then return tostring(v) end + return t .. "(" .. (tostring(v):gsub("0x%x+", "PTR")) .. ")" +end + +-- 每个可能抛错的调用都必须在 pcall 里(约定 #38) +local function P(tag, fn, ...) + local r = table.pack(pcall(fn, ...)) + local out = tag + for i = 1, r.n do out = out .. " | " .. show(r[i]) end + print(out .. " | n=" .. r.n) +end + +local SAMPLES = { + {"empty", ""}, + {"ascii", "abc"}, + {"nulonly", NUL}, + {"nulmid", "a" .. NUL .. "b"}, + {"nulend", "ab" .. NUL}, + {"nulbeg", NUL .. "ab"}, + {"nulx3", NUL .. NUL .. NUL}, + {"nulnum", "1" .. NUL .. "2"}, + {"ff", string.char(0xFF)}, + {"ffmid", "a" .. string.char(0xFF) .. "b"}, + {"c3_28", string.char(0xC3) .. string.char(0x28)}, + {"c0_80", string.char(0xC0) .. string.char(0x80)}, + {"e2_82", string.char(0xE2) .. string.char(0x82)}, + {"eda080", string.char(0xED) .. string.char(0xA0) .. string.char(0x80)}, + {"f49080", string.char(0xF4) .. string.char(0x90) .. string.char(0x80) .. string.char(0x80)}, + {"utf8ok", string.char(0xE4) .. string.char(0xB8) .. string.char(0xAD)}, + {"lf", "a" .. LF .. "b"}, + {"cr", "a" .. CR .. "b"}, + {"tab", "a" .. TAB .. "b"}, + {"pct", "a%b"}, + {"bs", "a" .. BS .. "b"}, + {"digits", "12a"}, + {"spaces", " 12 "}, + {"dash", "-12"}, + {"hexnum", "0x1p4"}, + {"dot", "a.b"}, + {"brack", "a[b"}, +} +local NS = #SAMPLES + +print("=== A 长度与表示 ===") +for i = 1, NS do + local n, s = SAMPLES[i][1], SAMPLES[i][2] + P("A len " .. n, function() return #s, s:len(), string.len(s), tostring(s) == s end) + P("A cmp " .. n, function() + return s == "abc", s < "abc", s > "abc", s <= s, s .. NUL == s .. NUL + end) +end + +print("=== B string.byte 网格 ===") +local IDX = {-4, -3, -1, 0, 1, 2, 3, 4, 5, 100} +for i = 1, NS do + local n, s = SAMPLES[i][1], SAMPLES[i][2] + P("B byte " .. n, function() return string.byte(s) end) + for _, a in ipairs(IDX) do + P("B byte1 " .. n .. " " .. a, function() return string.byte(s, a) end) + for _, b in ipairs(IDX) do + P("B byte2 " .. n .. " " .. a .. " " .. b, function() return string.byte(s, a, b) end) + end + end +end + +print("=== C string.char 网格 ===") +local CH = {0, 1, 65, 127, 128, 255, 256, -1, -256, 1.0, 1.5, 2.9, -0.0} +for _, a in ipairs(CH) do + P("C char1 " .. tostring(a), function() return #string.char(a) end) + for _, b in ipairs(CH) do + P("C char2 " .. tostring(a) .. " " .. tostring(b), function() return #string.char(a, b) end) + end +end +P("C char0", function() return #string.char() end) +P("C char str", function() return #string.char("65") end) +P("C char str2", function() return #string.char("0x41") end) +P("C char bool", function() return #string.char(true) end) +P("C char nil", function() return #string.char(nil) end) +P("C char tbl", function() return #string.char({}) end) +P("C char huge", function() return #string.char(1e300) end) +P("C char nan", function() return #string.char(0 / 0) end) +P("C char many", function() return #string.char(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) end) + +print("=== D string.sub 网格 ===") +local SI = {-5, -3, -1, 0, 1, 2, 3, 4, 100, 1.0, 1.5} +for i = 1, NS do + local n, s = SAMPLES[i][1], SAMPLES[i][2] + P("D sub " .. n, function() return string.sub(s, 2) end) + for _, a in ipairs(SI) do + P("D sub1 " .. n .. " " .. tostring(a), function() return string.sub(s, a) end) + for _, b in ipairs(SI) do + P("D sub2 " .. n .. " " .. tostring(a) .. " " .. tostring(b), function() return string.sub(s, a, b) end) + end + end +end +P("D sub bool", function() return string.sub("abc", true) end) +P("D sub str", function() return string.sub("abc", "2") end) +P("D sub nil2", function() return string.sub("abc", 1, nil) end) +P("D sub noarg", function() return string.sub() end) + +print("=== E find / match / gmatch / gsub ===") +local PATS = {".", "%a", "%d", "%s", "%w", "%p", "%c", "a", NUL, "[%z]", "[^%z]", "%z", "b*", "^a", "b$", "()", "%bxy", "[a-c]", ".*", ".+"} +for i = 1, NS do + local n, s = SAMPLES[i][1], SAMPLES[i][2] + for _, p in ipairs(PATS) do + P("E find " .. n .. " " .. esc(p), function() return string.find(s, p) end) + P("E findP " .. n .. " " .. esc(p), function() return string.find(s, p, 1, true) end) + P("E match " .. n .. " " .. esc(p), function() return string.match(s, p) end) + P("E gsub0 " .. n .. " " .. esc(p), function() return string.gsub(s, p, "") end) + P("E gsubX " .. n .. " " .. esc(p), function() return string.gsub(s, p, "X") end) + P("E gsubF " .. n .. " " .. esc(p), function() + return string.gsub(s, p, function(c) return "<" .. (type(c) == "string" and #c or tostring(c)) .. ">" end) + end) + local hits = 0 + local ok, e = pcall(function() + for a, b in string.gmatch(s, p) do + hits = hits + 1 + if hits == 1 then io.write("") end + if hits > 30 then break end + print("E gmatch " .. n .. " " .. esc(p) .. " | " .. show(a) .. " | " .. show(b)) + end + end) + print("E gmatch " .. n .. " " .. esc(p) .. " | ok=" .. tostring(ok) .. " | hits=" .. hits + .. " | err=" .. (ok and "nil" or show(e))) + end +end + +print("=== F string.format ===") +local FMTS = {"%s", "%q", "%d", "%5s", "%-5s", "%.2s", "%10.3s", "%c", "%x", "%a"} +for i = 1, NS do + local n, s = SAMPLES[i][1], SAMPLES[i][2] + for _, f in ipairs(FMTS) do + P("F fmt " .. n .. " " .. f, function() return string.format(f, s) end) + end + P("F fmt2 " .. n, function() return string.format("%s|%s|%q", s, s, s) end) +end +P("F fmt c num", function() return string.format("%c", 65) end) +P("F fmt c nul", function() return #string.format("%c", 0) end) +P("F fmt c 255", function() return #string.format("%c", 255) end) +P("F fmt c 256", function() return #string.format("%c", 256) end) +P("F fmt s nul", function() return #string.format("%s", NUL) end) + +print("=== G rep / reverse / upper / lower ===") +for i = 1, NS do + local n, s = SAMPLES[i][1], SAMPLES[i][2] + P("G rep0 " .. n, function() return string.rep(s, 0) end) + P("G rep1 " .. n, function() return string.rep(s, 1) end) + P("G rep3 " .. n, function() return string.rep(s, 3) end) + P("G repS " .. n, function() return string.rep(s, 2, NUL) end) + P("G repS2 " .. n, function() return string.rep(s, 3, "-") end) + P("G rev " .. n, function() return string.reverse(s) end) + P("G lower " .. n, function() return string.lower(s) end) + P("G upper " .. n, function() return string.upper(s) end) +end +P("G rep neg", function() return string.rep("a", -1) end) +P("G rep huge", function() return #string.rep("a", 1e7) end) + +print("=== H tonumber / 强制转换 ===") +for i = 1, NS do + local n, s = SAMPLES[i][1], SAMPLES[i][2] + P("H tonum " .. n, function() + local v = tonumber(s) + if v == nil then return nil, math.type(nil) end + return v, math.type(v) + end) + for _, base in ipairs({2, 8, 10, 16, 36, 1, 37}) do + P("H tonumB " .. n .. " " .. base, function() + local v = tonumber(s, base) + if v == nil then return nil end + return v, math.type(v) + end) + end + P("H arith " .. n, function() return s + 0 end) + P("H arith2 " .. n, function() return 0 + s end) + P("H concat " .. n, function() return s .. "" end) + P("H lt " .. n, function() return s < 0 end) +end +P("H tonum nonstr", function() return tonumber({}, 10) end) +P("H tonum bool", function() return tonumber(true, 10) end) + """); + } + + /** 片二:比较与排序 / 表键 / 错误消息 / utf8 / pack / load / io。 */ + @Test + void compareKeyMessagePackLoadGridMatchesReference() { + Diff.assertMatches(""" + +-- 二进制安全矩阵 II:比较与排序(专钉 strcoll 的无符号语义)、表键、错误消息、 +-- utf8、string.pack、load、io、以及协程第二遍。 + +local NUL = string.char(0) +local BS = string.char(92) +local LF = string.char(10) + +local HEX = "0123456789ABCDEF" +local function hb(b) return HEX:sub((b // 16) + 1, (b // 16) + 1) .. HEX:sub((b % 16) + 1, (b % 16) + 1) end +local function esc(s) + return (s:gsub(".", function(c) + local b = c:byte() + if b == 91 or b == 93 then return "<" .. hb(b) .. ">" end + if b < 32 or b > 126 then return "<" .. hb(b) .. ">" end + return c + end)) +end +local function show(v) + local t = type(v) + if t == "string" then return "#" .. #v .. "[" .. esc(v) .. "]" end + if t == "number" then + local mt = math.type(v) + if mt == "integer" then return "i" .. tostring(v) end + if mt == "float" then return "f" .. tostring(v) end + end + if t == "nil" or t == "boolean" then return tostring(v) end + return t .. "(" .. (tostring(v):gsub("0x%x+", "PTR")) .. ")" +end +local function P(tag, fn, ...) + local r = table.pack(pcall(fn, ...)) + local out = tag + for i = 1, r.n do out = out .. " | " .. show(r[i]) end + print(out .. " | n=" .. r.n) +end + +print("=== I 全字节对比较 ===") +-- 256 个单字节串,按参考实现排序后的顺序(一整行钉住全部次序) +local all = {} +for b = 0, 255 do all[b + 1] = string.char(b) end +local ok, err = pcall(table.sort, all) +print("I sort ok=" .. tostring(ok) .. " err=" .. (ok and "nil" or show(err))) +if ok then + local s = table.concat(all, "") + print("I order " .. esc(s)) +end +-- 逐对:< 的真值表压成 256 行位图 +for b1 = 0, 255 do + local row = {} + for b2 = 0, 255 do + row[b2 + 1] = (string.char(b1) < string.char(b2)) and "1" or "0" + end + print("I lt " .. hb(b1) .. " " .. table.concat(row)) +end + +print("=== I2 多字节串的排序与 max/min ===") +local WORDS = { + string.char(0xFF), "abc", "Abc", NUL, "a" .. NUL, "a" .. NUL .. "a", + string.char(0xE4) .. string.char(0xB8) .. string.char(0xAD), + string.char(0xC3) .. string.char(0xA9), "", "z", "Z", "0", + string.char(0x7F), string.char(0x80), string.char(0x01), +} +local sorted = {} +for i = 1, #WORDS do sorted[i] = WORDS[i] end +local ok2, e2 = pcall(table.sort, sorted) +print("I2 sort ok=" .. tostring(ok2) .. " err=" .. (ok2 and "nil" or show(e2))) +print("I2 sorted " .. esc(table.concat(sorted, "|"))) +P("I2 max", function() return math.max(WORDS[1], WORDS[2], WORDS[7], WORDS[10]) end) +P("I2 min", function() return math.min(WORDS[1], WORDS[2], WORDS[7], WORDS[10]) end) +for i = 1, #WORDS do + for j = 1, #WORDS do + P("I2 cmp " .. esc(WORDS[i]) .. " " .. esc(WORDS[j]), function() + return WORDS[i] < WORDS[j], WORDS[i] <= WORDS[j], WORDS[i] > WORDS[j], WORDS[i] >= WORDS[j], WORDS[i] == WORDS[j] + end) + end +end + +print("=== J 表键 ===") +local KEYS = {"a", NUL, "a" .. NUL, NUL .. "a", string.char(0xFF), "", "b" .. NUL .. "c", string.char(0xC3)} +local t = {} +for i = 1, #KEYS do t[KEYS[i]] = i end +-- pairs 顺序不可比(约定 #57):收集后排序再打印 +local got = {} +for k, v in pairs(t) do got[#got + 1] = k end +table.sort(got) +print("J n=" .. #got) +for i = 1, #got do print("J key " .. esc(got[i]) .. " = " .. tostring(t[got[i]])) end +for i = 1, #KEYS do + P("J get " .. esc(KEYS[i]), function() return t[KEYS[i]], rawget(t, KEYS[i]) end) + P("J getmiss " .. esc(KEYS[i]), function() return t[KEYS[i] .. "z"] end) +end +P("J concat", function() return table.concat(KEYS, NUL) end) +P("J concat2", function() return table.concat(KEYS, "-", 2, 4) end) +P("J keynil", function() local u = {} u[NUL] = 1 return #u, u[NUL] end) + +print("=== K 错误消息里的 NUL ===") +for i = 1, #KEYS do + local s = KEYS[i] + P("K error " .. esc(s), function() return pcall(error, s) end) + P("K error0 " .. esc(s), function() return pcall(error, s, 0) end) + P("K error2 " .. esc(s), function() return pcall(error, s, 2) end) + P("K assert " .. esc(s), function() return pcall(assert, false, s) end) + P("K xpcall " .. esc(s), function() + return xpcall(function() error(s) end, function(m) return "H:" .. tostring(m) end) + end) + P("K raise " .. esc(s), function() + local f = load("error(" .. string.format("%q", s) .. ")") + return pcall(f) + end) + P("K badarg " .. esc(s), function() return pcall(string.rep, "a", s) end) + P("K badarg2 " .. esc(s), function() return pcall(string.sub, "abc", s) end) + P("K badarg3 " .. esc(s), function() return pcall(math.abs, s) end) + P("K badarg4 " .. esc(s), function() return pcall(table.insert, {}, 1, s) end) +end + +print("=== L utf8 网格 ===") +for i = 1, #KEYS do + local s = KEYS[i] + P("L len " .. esc(s), function() return utf8.len(s) end) + P("L len1 " .. esc(s), function() return utf8.len(s, 1) end) + P("L len2 " .. esc(s), function() return utf8.len(s, 1, -1) end) + P("L len9 " .. esc(s), function() return utf8.len(s, 9) end) + for _, ix in ipairs({-2, -1, 0, 1, 2, 3, 9}) do + P("L offset " .. esc(s) .. " " .. ix, function() return utf8.offset(s, ix) end) + P("L offset2 " .. esc(s) .. " " .. ix, function() return utf8.offset(s, ix, 2) end) + end + P("L cp1 " .. esc(s), function() return utf8.codepoint(s, 1) end) + P("L cpall " .. esc(s), function() return utf8.codepoint(s, 1, -1) end) + P("L cp0 " .. esc(s), function() return utf8.codepoint(s, 0) end) + local n, cs = 0, {} + local okc, ec = pcall(function() + for p, c in utf8.codes(s) do + n = n + 1 + if n <= 20 then cs[#cs + 1] = p .. ":" .. c end + end + end) + print("L codes " .. esc(s) .. " | ok=" .. tostring(okc) .. " | n=" .. n + .. " | " .. table.concat(cs, ",") .. " | err=" .. (okc and "nil" or show(ec))) +end +local UCH = {0, 1, 0x7F, 0x80, 0x7FF, 0x800, 0xFFFF, 0x10000, 0x10FFFF, 0x110000, -1, 1.5, 0xD800} +for _, c in ipairs(UCH) do + P("L char " .. tostring(c), function() return string.byte(utf8.char(c), 1, -1) end) +end +P("L char none", function() return #utf8.char() end) +P("L char str", function() return #utf8.char("65") end) +P("L char bool", function() return #utf8.char(true) end) +print("L pat " .. esc(utf8.charpattern)) + +print("=== M string.pack / unpack ===") +local FMT = {"z", "s", "s1", "s2", "s4", "c1", "c3", "c8", "i1", "i2", "i4", "b", "B", "j", "T", "x"} +for i = 1, #KEYS do + local s = KEYS[i] + for _, f in ipairs(FMT) do + P("M pack " .. f .. " " .. esc(s), function() + local p = string.pack(f, s) + return #p, string.byte(p, 1, -1) + end) + P("M packS " .. f .. " " .. esc(s), function() + local p = string.pack("<" .. f .. " >", s) + return #p + end) + end + P("M fmt " .. esc(s), function() return string.pack("s", s) end) +end +local PACKS = {"z", "s1", "c3", "i4", "b", "B", "j", "T", "x", "i", "I", "f", "d", "n"} +for _, f in ipairs(PACKS) do + local okk, blob = pcall(string.pack, f, (f == "z" or f == "s1" or f == "c3") and "ab" or 1) + if okk then + P("M unpack " .. f, function() + local v, i = string.unpack(f, blob) + return v, i, #blob + end) + P("M unpackPad " .. f, function() + local v, i = string.unpack(f, blob .. NUL .. NUL) + return v, i + end) + else + print("M packfail " .. f .. " | " .. show(blob)) + end +end +P("M packfmt nul", function() return #string.pack("c4", NUL .. NUL .. NUL .. NUL) end) +P("M unpack z", function() + local p = string.pack("z", "ab") + return string.unpack("z", p) +end) + +print("=== N load / chunkname ===") +local SRCS = { + "return 1", + "return 1" .. NUL, + NUL .. "return 1", + "return" .. NUL .. " 1", + "local x" .. NUL .. "=1 return x", + "return '" .. NUL .. "'", + "return [[" .. NUL .. "]]", + "return '" .. string.char(0xFF) .. "'", + "return 1 --" .. NUL, +} +for i = 1, #SRCS do + local s = SRCS[i] + P("N load " .. esc(s), function() + local f, e = load(s) + if f == nil then return nil, e end + return f(), type(f) + end) + P("N loadname " .. esc(s), function() + local f, e = load(s, NUL .. "n") + if f == nil then return nil, e end + return f() + end) + P("N loadname2 " .. esc(s), function() + local f, e = load(s, "a" .. NUL .. "b") + if f == nil then return nil, e end + return f() + end) + P("N loadbin " .. esc(s), function() + local f, e = load(s, "=x", "b") + if f == nil then return nil, e end + return f() + end) + P("N loadtxt " .. esc(s), function() + local f, e = load(s, "=x", "t") + if f == nil then return nil, e end + return f() + end) +end +P("N dumpname", function() + local f = load("return 1") + local d = string.dump(f) + local g, e = load(d, NUL .. "z", "b") + if g == nil then return nil, e end + return g() +end) + +print("=== O io 字节往返 ===") +local TMP = "target/rt/b.bin" +os.remove(TMP) +P("O write", function() + local f = assert(io.open(TMP, "wb")) + f:write(NUL, "a", string.char(0xFF), NUL .. NUL, LF) + f:close() + return true +end) +P("O readall", function() + local f = assert(io.open(TMP, "rb")) + local s = f:read("a") + f:close() + return s +end) +P("O readn", function() + local f = assert(io.open(TMP, "rb")) + local a, b, c = f:read(1, 2, 3) + f:close() + return a, b, c +end) +P("O readline", function() + local f = assert(io.open(TMP, "rb")) + local a, b = f:read("l", "l") + f:close() + return a, b + end) +P("O lines", function() + local out = {} + for l in io.lines(TMP) do out[#out + 1] = l end + return #out, table.concat(out, "|") +end) +P("O size", function() + local f = assert(io.open(TMP, "rb")) + local sz = f:seek("end") + f:close() + return sz +end) +os.remove(TMP) + +print("=== P 协程第二遍 ===") +local function inner() + local out = {} + local function add(...) out[#out + 1] = table.concat({...}, ",") end + local s = "a" .. NUL .. string.char(0xFF) + add(#s, s:byte(1, -1)) + add(tostring(s < "abc"), tostring(s > "abc")) + add(string.format("%q", s)) + add(tostring(string.find(s, NUL))) + add(select(2, pcall(error, s)) == s) + add(tostring(string.sub(s, 2, 3) == NUL .. string.char(0xFF))) + local t2 = {} + t2[s] = 7 + add(tostring(t2[s])) + return table.concat(out, ";") +end +P("P main", function() return inner() end) +P("P wrap", function() + local w = coroutine.wrap(function() coroutine.yield(inner()) end) + return w() +end) +P("P resume", function() + local c = coroutine.create(function() coroutine.yield(inner()) end) + return select(2, coroutine.resume(c)) +end) +P("P err", function() + local c = coroutine.create(function() error("a" .. NUL .. "b") end) + return coroutine.resume(c) +end) +P("P status", function() + local c = coroutine.create(function() coroutine.yield() end) + coroutine.resume(c) + local a = coroutine.status(c) + coroutine.resume(c) + return a, coroutine.status(c) +end) + """); + } + + /** + * 片三:chunk 名是任意字节时的 {@code luaL_where} / {@code short_src} / + * {@code traceback}。 + *

+ * 走 {@code assertFullMatches} 而不是 {@code assertMatches}:片三把 + * {@code debug.traceback} 的返回值打印出来了,回溯属于预期输出, + * 用 {@code assertMatches} 会从回溯处把后面全砍掉(约定 #33)。 + */ + @Test + void chunkNameByteGridMatchesReference() { + Diff.assertFullMatches(""" + +-- 二进制安全矩阵 III:chunk 名是任意字节时的 luaL_where / short_src / traceback +local NUL = string.char(0) +local HEX = "0123456789ABCDEF" +local function hb(b) return HEX:sub((b // 16) + 1, (b // 16) + 1) .. HEX:sub((b % 16) + 1, (b % 16) + 1) end +local function esc(s) + return (s:gsub(".", function(c) + local b = c:byte() + if b == 91 or b == 93 then return "<" .. hb(b) .. ">" end + if b < 32 or b > 126 then return "<" .. hb(b) .. ">" end + return c + end)) +end +local function show(v) + local t = type(v) + if t == "string" then return "#" .. #v .. "[" .. esc(v) .. "]" end + if t == "number" then + local mt = math.type(v) + if mt == "integer" then return "i" .. tostring(v) end + if mt == "float" then return "f" .. tostring(v) end + end + if t == "nil" or t == "boolean" then return tostring(v) end + return t .. "(" .. (tostring(v):gsub("0x%x+", "PTR")) .. ")" +end +local function P(tag, fn, ...) + local r = table.pack(pcall(fn, ...)) + local out = tag + for i = 1, r.n do out = out .. " | " .. show(r[i]) end + print(out .. " | n=" .. r.n) +end + +-- chunk 名样本:= / @ / 裸 三种写法,内容含 NUL 与高位字节,长度跨 LUA_IDSIZE +local NAMES = { + "=x", "=" .. string.char(0xFF), "=" .. NUL .. "ab", "=a" .. NUL .. "b", + "=abc", "@x.lua", "@" .. string.char(0xFF) .. ".lua", "@" .. NUL .. "f.lua", + "plain", string.char(0xFF), NUL, "a" .. NUL .. "b", + "=" .. string.rep("a", 55), "=" .. string.rep("a", 60), "=" .. string.rep("a", 70), + "@" .. string.rep("b", 55) .. ".lua", "@" .. string.rep("b", 70) .. ".lua", + string.rep("c", 55), string.rep("c", 70), + "=" .. string.char(0xE4) .. string.char(0xB8) .. string.char(0xAD), + "", "=", "@", +} + +print("=== Q chunk 名 x error 的位置前缀 ===") +for i = 1, #NAMES do + local nm = NAMES[i] + P("Q err " .. esc(nm), function() + local f = assert(load("error('boom')", nm)) + return f() + end) + P("Q err0 " .. esc(nm), function() + local f = assert(load("error('boom', 0)", nm)) + return f() + end) + P("Q err2 " .. esc(nm), function() + local f = assert(load("local function g() error('boom', 2) end g()", nm)) + return f() + end) + P("Q runerr " .. esc(nm), function() + local f = assert(load("local x = nil return x.y", nm)) + return f() + end) + P("Q shortsrc " .. esc(nm), function() + local f = assert(load("return debug.getinfo(1, 'S').short_src", nm)) + return f() + end) + P("Q src " .. esc(nm), function() + local f = assert(load("return debug.getinfo(1, 'S').source", nm)) + return f() + end) +end + +print("=== R traceback ===") +for i = 1, #NAMES do + local nm = NAMES[i] + P("R tb " .. esc(nm), function() + local f = assert(load("return debug.traceback('m')", nm)) + return f() + end) + P("R tbco " .. esc(nm), function() + local f = assert(load("local co = coroutine.create(function() return debug.traceback() end) return select(2, coroutine.resume(co))", nm)) + return f() + end) + P("R uncaught " .. esc(nm), function() + local f = assert(load("error('boom')", nm)) + return f() + end) +end +P("R tbmsg", function() return debug.traceback(string.char(0xFF) .. NUL .. "m") end) +P("R tbmsg2", function() + local co = coroutine.create(function() error("a" .. string.char(0xFF)) end) + local ok, e = coroutine.resume(co) + return ok, e, debug.traceback(co) +end) + +print("=== S 位置前缀 x 消息里含任意字节 ===") +local MSGS = {"boom", NUL, string.char(0xFF), "a" .. NUL .. "b", string.char(0xED) .. string.char(0xA0)} +for i = 1, #MSGS do + local m = MSGS[i] + P("S error " .. esc(m), function() return pcall(error, m) end) + P("S assert " .. esc(m), function() return pcall(assert, false, m) end) + P("S runerr " .. esc(m), function() + local t = setmetatable({}, {__index = function() error(m, 1) end}) + return pcall(function() return t.x + 1 end) + end) + P("S concaterr " .. esc(m), function() return pcall(function() return {} .. 1 end) end) + P("S callerr " .. esc(m), function() return pcall(function() local x = nil x() end) end) + P("S idxerr " .. esc(m), function() return pcall(function() local x = nil return x[m] end) end) + P("S cmp " .. esc(m), function() return pcall(function() return {} < {} end) end) + P("S forerr " .. esc(m), function() return pcall(function() for i = 1, m do end end) end) +end +-- 非字符串错误对象照原样穿出(不能被 latin1 拦下) +for _, v in ipairs({1, 0, true, false, {}}) do + P("S objerr " .. type(v), function() return pcall(error, v) end) + P("S objerr0 " .. type(v), function() return pcall(error, v, 0) end) +end +P("S objerr mt", function() + local t = setmetatable({}, {__tostring = function() return string.char(0xFF) end}) + return pcall(error, t) +end) + +print("=== T require / loadfile 的 chunk 名 ===") +os.remove("target/rt/m1.lua") +local w = io.open("target/rt/m1.lua", "wb") +w:write("return {}" .. string.char(10)) +w:close() +P("T require", function() + package.path = "target/rt/?.lua" + local m = require("m1") + return type(m) +end) +P("T require2", function() + package.path = "target/rt/?.lua" + package.loaded["m1"] = nil + local m, extra = require("m1") + return type(m), extra +end) +os.remove("target/rt/m1.lua") + +print("=== U 协程第二遍:整片重跑 ===") +local function body() + local out = {} + local B = string.char(0xFF) .. NUL .. "z" + out[#out + 1] = tostring(#B) + out[#out + 1] = tostring(B < "a") .. tostring(B > "a") + out[#out + 1] = esc(string.format("%q", B)) + out[#out + 1] = select(2, pcall(error, B)) + out[#out + 1] = esc(select(2, pcall(assert, false, B))) + local f = load("error('b')", "=" .. B) + out[#out + 1] = esc(select(2, pcall(f))) + out[#out + 1] = esc(load("return debug.getinfo(1,'S').short_src", "=" .. B)()) + return table.concat(out, ";") +end +P("U main", function() return body() end) +P("U co", function() + local c = coroutine.create(function() coroutine.yield(body()) end) + return select(2, coroutine.resume(c)) +end) +P("U wrap", function() + local w2 = coroutine.wrap(function() coroutine.yield(body()) end) + return w2() +end) +P("U xpcall", function() + return xpcall(function() error(string.char(0xFF)) end, + function(m) return "H" .. #m end) +end) + """); + } + + + /** + * 片四:错误消息里「回流的 Lua 字符串」。 + *

+ * 与前三片的差别:前三片修的是位置前缀与回溯,这一片盯的是嵌在消息里的 + * 值本身——{@code varinfo} 反推出的字段名({@code t["\xff"]} 的键)、 + * 元表的 {@code __name}、{@code require} 的模块名、{@code argError} 反查 + * 出来的函数名。这些值从 Lua 字节串出发、绕一圈 Java String、最后又 + * {@code LuaString.latin1} 编码回 Lua 字符串;中间那一圈用 UTF-8 解码就会 + * 把 {@code 0xFF} 变成 {@code U+FFFD}。 + *

+ * 修法与前几片不同:这里没有把每一处的类型都改成 {@code LuaString} + * ({@code varinfo} 那一串 {@code String[]} 出参牵动十几个函数),而是引入 + * {@code LuaString.toLatin1String()} —— 与 {@code latin1} 互为逆运算, + * 保证 {@code byte<->char} 在 0..255 上无损往返。代价是这条链上的中间 + * 表示只能是 Latin-1,凡是拿它去拼 Lua 字符串的地方都必须用 + * {@code latin1} 而不是 {@code utf8}({@code debug.getinfo} 的 + * {@code name} / {@code namewhat}、回溯里的 {@code function 'x'} 就是)。 + *

+ * 光有 Latin-1 还不够,名字还要再走一次 {@code toCString()}: + * {@code W2} 段是专门为此加的——{@code W} 段用变量键({@code t[k]}), + * 编译器走寄存器,压根碰不到 {@code kname};只有写成字面键 + * ({@code load("local t = {} return t['a\0b'].x")})才会编译成 + * {@code OP_GETFIELD}、由 {@code kname} 报 {@code field}。灵敏度脚本一开始 + * 说「{@code kName} 的改动盯不住」,就是缺了这一段。补上之后 470 行里 + * 咬出 14 处分歧({@code (field '')} vs {@code (field '<00>')})。 + *

+ * {@code io} / {@code os} 那一段故意不比三件事(参考实测值记在 + * 约定 #57):文件名含非 UTF-8 字节时,C 把原始字节递给 {@code fopen}, + * macOS 回 EILSEQ(92),而 Java 的文件层走 UTF-8、直接建出别的文件; + * 同理 {@code os.execute} 的命令行;以及 {@code os.setlocale} 设成非 C 之后 + * {@code strcoll} 变成 locale 相关排序(参考在 {@code zh_CN.UTF-8} 下 + * {@code "a" < "B"} 为 true、排序 {@code aAbBcC},本实现恒字节序)。 + */ + @Test + void errorMessageEmbeddedValueGridMatchesReference() { + Diff.assertMatches(""" + +-- 二进制安全矩阵 IV:错误消息里「回流的 Lua 字符串」 +-- 上一片修的是位置前缀与 traceback;这一片盯的是另一类出口: +-- 错误消息里嵌的**值本身**(varinfo 的字段名、__name、模块名、文件名)。 +local NUL = string.char(0) +local HEX = "0123456789ABCDEF" +local function hb(b) return HEX:sub((b // 16) + 1, (b // 16) + 1) .. HEX:sub((b % 16) + 1, (b % 16) + 1) end +local function esc(s) + -- 地址不可比(约定 #43) + s = s:gsub("0x%x+", function(a) return string.rep("P", #a) end) -- 等长替换:# 也要可比 + return (s:gsub(".", function(c) + local b = c:byte() + if b == 91 or b == 93 then return "<" .. hb(b) .. ">" end + if b < 32 or b > 126 then return "<" .. hb(b) .. ">" end + return c + end)) +end +local function show(v) + local t = type(v) + if t == "string" then return "#" .. #v .. "[" .. esc(v) .. "]" end + if t == "number" then + local mt = math.type(v) + if mt == "integer" then return "i" .. tostring(v) end + if mt == "float" then return "f" .. tostring(v) end + end + if t == "nil" or t == "boolean" then return tostring(v) end + return t .. "(" .. (tostring(v):gsub("0x%x+", "PTR")) .. ")" +end +local function P(tag, fn, ...) + local r = table.pack(pcall(fn, ...)) + local out = tag + for i = 1, r.n do out = out .. " | " .. show(r[i]) end + print(out .. " | n=" .. r.n) +end + +local NAMES = { + "ok", NUL, "a" .. NUL .. "b", string.char(0xFF), string.char(0xC3) .. string.char(0x28), + string.char(0xE4) .. string.char(0xB8) .. string.char(0xAD), "", "veryveryverylongname", +} + +print("=== V __name(tostring 的类型名)===") +for i = 1, #NAMES do + local nm = NAMES[i] + -- 地址长度本身不可比(C 与 JVM 的指针位数不同):先抹再取长度 + P("V name " .. esc(nm), function() + return (tostring(setmetatable({}, {__name = nm})):gsub("0x%x+", "PTR")) + end) + P("V nameS " .. esc(nm), function() + return tostring(setmetatable({}, {__name = nm})):gsub("0x%x+", "PTR") + end) + P("V arith " .. esc(nm), function() + local t = setmetatable({}, {__name = nm}) + return t + 1 + end) + P("V call " .. esc(nm), function() + local t = setmetatable({}, {__name = nm}) + return t() + end) + P("V idx " .. esc(nm), function() + local t = setmetatable({}, {__name = nm}) + return t.x + end) + P("V len " .. esc(nm), function() + local t = setmetatable({}, {__name = nm}) + return #t + end) + P("V concat " .. esc(nm), function() + local t = setmetatable({}, {__name = nm}) + return t .. "x" + end) + P("V cmp " .. esc(nm), function() + local t = setmetatable({}, {__name = nm}) + return t < 1 + end) + P("V newidx " .. esc(nm), function() + local t = setmetatable({}, {__name = nm}) + t.x = 1 + return "ok" + end) +end + +print("=== W varinfo:错误消息里嵌的字段名 ===") +for i = 1, #NAMES do + local k = NAMES[i] + P("W getidx " .. esc(k), function() + local t = {} + return t[k].x + end) + P("W call " .. esc(k), function() + local t = {} + return t[k]() + end) + P("W arith " .. esc(k), function() + local t = {} + return t[k] + 1 + end) + P("W concat " .. esc(k), function() + local t = {} + return t[k] .. "x" + end) + P("W cmp " .. esc(k), function() + local t = {} + return t[k] < 1 + end) + P("W len " .. esc(k), function() + local t = {} + return #t[k] + end) + P("W newidx " .. esc(k), function() + local t = {} + t[k].x = 1 + return "ok" + end) + P("W forlimit " .. esc(k), function() + local t = {} + for j = 1, t[k] do end + return "ok" + end) + P("W forinit " .. esc(k), function() + local t = {} + for j = t[k], 2 do end + return "ok" + end) + P("W upval " .. esc(k), function() + local t = {} + local function f() return t[k] + 1 end + return f() + end) + P("W method " .. esc(k), function() + local t = {} + return t[k](t, 1) + end) + P("W intidx", function() + local t = {} + return t[1].x + end) + P("W selfidx " .. esc(k), function() + local t = setmetatable({}, {__index = function(_, kk) error("boom", 1) end}) + return t[k] + end) +end + +print("=== W2 字面键:编译器把它当常量,varinfo 报 constant/field ===") +for i = 1, #NAMES do + local k = NAMES[i] + -- 用 load 造源码:字面写进字符串的是真字节,编译器才会走 OP_GETFIELD -> kname + P("W2 const " .. esc(k), function() + return load("local t = {} return t['" .. k .. "'].x")() + end) + P("W2 arith " .. esc(k), function() + return load("local t = {} return t['" .. k .. "'] + 1")() + end) + P("W2 call " .. esc(k), function() + return load("local t = {} return t['" .. k .. "']()")() + end) + P("W2 set " .. esc(k), function() + return load("local t = {} t['" .. k .. "'].y = 1 return 'ok'")() + end) + P("W2 concat " .. esc(k), function() + return load("local t = {} return t['" .. k .. "'] .. 'x'")() + end) + P("W2 len " .. esc(k), function() + return load("local t = {} return #t['" .. k .. "']")() + end) + P("W2 for " .. esc(k), function() + return load("local t = {} for j = 1, t['" .. k .. "'] do end return 'ok'")() + end) + P("W2 cmp " .. esc(k), function() + return load("local t = {} return t['" .. k .. "'] < 1")() + end) + -- 字段名也要出现在 argError 反查出来的「函数名」里 + P("W2 badself " .. esc(k), function() + local f = load("local t = {} local m = t['" .. k .. "'] return m.x(1)") + return f() + end) + -- 全局名:_ENV 的字面键 + P("W2 global " .. esc(k), function() + return load("return _ENV['" .. k .. "'] + 1")() + end) +end + +-- 约定第四条:同一批语料塞进协程再跑一遍。registry / 位置前缀是 global_State +-- 级的,主线程上怎么跑都正常的 bug 只有在这里现形。 +print("=== W3 协程第二遍:字面键 varinfo ===") +local function PC(tag, src) + P(tag, function() + local co = coroutine.create(assert(load(src))) + local ok, e = coroutine.resume(co) + return ok, e + end) +end +for i = 1, #NAMES do + local k = NAMES[i] + PC("W3 const " .. esc(k), "local t = {} return t['" .. k .. "'].x") + PC("W3 arith " .. esc(k), "local t = {} return t['" .. k .. "'] + 1") + PC("W3 call " .. esc(k), "local t = {} return t['" .. k .. "']()") + PC("W3 concat " .. esc(k), "local t = {} return t['" .. k .. "'] .. 'x'") + PC("W3 global " .. esc(k), "return _ENV['" .. k .. "'] + 1") + PC("W3 method " .. esc(k), "local t = {} return t['" .. k .. "']:x()") +end +print("=== V2 协程第二遍:__name ===") +for i = 1, #NAMES do + local nm = NAMES[i] + P("V2 arith " .. esc(nm), function() + local co = coroutine.create(function() + local t = setmetatable({}, {__name = nm}) + return t + 1 + end) + local ok, e = coroutine.resume(co) + return ok, e + end) + P("V2 cmp " .. esc(nm), function() + local co = coroutine.create(function() + local t = setmetatable({}, {__name = nm}) + return t < 1 + end) + local ok, e = coroutine.resume(co) + return ok, e + end) +end + +print("=== X require 的模块名 ===") +for i = 1, #NAMES do + local nm = NAMES[i] + P("X req " .. esc(nm), function() + package.path = "target/rt/?.lua" + return require(nm) + end) + P("X searchpath " .. esc(nm), function() + return package.searchpath(nm, "target/rt/?.lua") + end) + P("X searchpath2 " .. esc(nm), function() + return package.searchpath("m", nm) + end) + P("X searchpath3 " .. esc(nm), function() + return package.searchpath("m", "target/rt/?.lua", nm) + end) + P("X searchpath4 " .. esc(nm), function() + return package.searchpath("m", "target/rt/?.lua", ".", nm) + end) + P("X searchers " .. esc(nm), function() + return package.searchers[1](nm) + end) + P("X searchers2 " .. esc(nm), function() + return package.searchers[2](nm) + end) + P("X searchers3 " .. esc(nm), function() + return package.searchers[3](nm) + end) + P("X searchers4 " .. esc(nm), function() + return package.searchers[4](nm) + end) + P("X preload " .. esc(nm), function() + package.preload[nm] = function() return 1 end + return require(nm) + end) +end + +print("=== Y io / os 的文件名 ===") +for i = 1, #NAMES do + local fn = NAMES[i] + P("Y open " .. esc(fn), function() return io.open(fn) end) + P("Y lines " .. esc(fn), function() return io.lines(fn) end) + P("Y remove " .. esc(fn), function() return os.remove(fn) end) + P("Y rename " .. esc(fn), function() return os.rename(fn, fn .. "2") end) + P("Y getenv " .. esc(fn), function() return os.getenv(fn) end) +end +print("=== Z 消息里嵌值的其他出口 ===") +for i = 1, #NAMES do + local v = NAMES[i] + -- 约定 #80:debug.setmetatable 改的是全局类型元表,用完必须还原原值 + P("Z setmeta " .. esc(v), function() + local old = debug.getmetatable(v) + local ok, e = pcall(debug.setmetatable, v, {}) + debug.setmetatable(v, old) + return ok, e + end) + P("Z getmeta " .. esc(v), function() return getmetatable(v) end) + P("Z setmeta2 " .. esc(v), function() + return debug.setmetatable(nil, v) + end) + P("Z badarg " .. esc(v), function() return pcall(string.rep, "a", v) end) + P("Z badarg2 " .. esc(v), function() return pcall(string.format, v) end) + P("Z badarg3 " .. esc(v), function() return pcall(io.open, {}, v) end) + P("Z badarg4 " .. esc(v), function() return pcall(os.setlocale, v, "x") end) + P("Z loadmode " .. esc(v), function() return load("return 1", "=x", v) end) + P("Z gsub " .. esc(v), function() return ("abc"):gsub("b", v) end) + P("Z gsubtbl " .. esc(v), function() + local t = setmetatable({}, {__index = function(_, k) return k .. "!" end}) + return ("abc"):gsub("b", t) + end) + P("Z sortcmp " .. esc(v), function() + return pcall(table.sort, {1, 2, 3}, v) + end) +end + +-- 下面三段是灵敏度脚本点出来的「用例盯不住」的补测: +-- findfield 的名字、 变量名、loadfile 的 mode。 +-- 前两条都要绕开「pairs 顺序不可比」(约定 #43)与「标识符只能 ASCII」。 +print("=== AA findfield:package.loaded 里反查出来的函数名 ===") +for i = 1, #NAMES do + local nm = NAMES[i] + -- 名字必须**唯一可达**:只挂在 package.loaded 下、不进 _G, + -- 否则 findfield 撞上 _G 的先后由 lua_next 决定,顺序不可比。 + P("AA tb " .. esc(nm), function() + local inner = function() return debug.traceback("MSG") end + local m = {} + m[nm] = inner + package.loaded["M" .. nm] = m + return inner() + end) + P("AA getinfo " .. esc(nm), function() + local inner = function() return 1 end + local m = {} + m[nm] = inner + package.loaded["M" .. nm] = m + -- 走同一条 pushglobalfuncname,但出口是 getinfo 的 name + local co = coroutine.create(function() return inner() end) + return select(2, coroutine.resume(co)) + end) +end + +print("=== AB 变量名:只有二进制 chunk 能带非 ASCII 局部名 ===") +-- 语法里的标识符只能 ASCII,想让局部名含高位字节必须改 dump; +-- 等长替换(zzzz -> 4 字节)保证 dump 仍然合法。 +for _, v in ipairs({ "nil", "1", "'x'", "{}", "io.stdout" }) do + local src = "local zzzz = " .. v .. " return 'ok'" + P("AB src " .. v, function() return load(src)() end) + local d1 = string.dump(load(src)) + P("AB dump " .. v, function() return load(d1)() end) + local d2 = d1:gsub("zzzz", "\255\254\253\252") + P("AB patched " .. v, function() return load(d2)() end) +end +-- 局部名含 NUL:C 这边是 %s,照样在第一个 \0 处断 +P("AB nulname", function() + local d = string.dump(load("local zzzz = 1 return 'ok'")):gsub("zzzz", "a\0b\0") + return load(d)() +end) + +print("=== AC loadfile 的 mode:原样拼进错误消息 ===") +-- 约定 #41:自己建临时文件,别读 target/rt/case.lua(那个文件的内容随测试而异) +os.remove("target/rt/ac.lua") +local w = assert(io.open("target/rt/ac.lua", "wb")) +w:write("return 1\n") +w:close() + +for i = 1, #NAMES do + local md = NAMES[i] + P("AC loadfile " .. esc(md), function() return loadfile("target/rt/ac.lua", md) end) + P("AC loadfile2 " .. esc(md), function() return loadfile("target/rt/nope.lua", md) end) + P("AC load " .. esc(md), function() return load("return 1", "=x", md) end) +end + """); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/ChunkIdTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/ChunkIdTest.java new file mode 100644 index 0000000..f5a7db7 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/ChunkIdTest.java @@ -0,0 +1,90 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +/** + * {@code lobject.c: luaO_chunkid}。 + *

+ * 单独开一类是因为它有一个只在长名字上才出现的坑,而差分用例全跑在 + * {@code target/rt/case.lua} 这个短路径上,永远碰不到。它是接上游测试套件时 + * 第一个炸出来的东西:用例目录的绝对路径一长,{@code error} 拼消息前缀 + * 就越界一字节。 + *

+ * 期望值全部实测于参考实现({@code load("error('boom')", name)} 后看 + * {@code pcall} 拿到的消息前缀): + *

+ *   = + 30 个 n   (srclen 31) → 30 字节
+ *   = + 80 个 n   (srclen 81) → 59 字节(= LUA_IDSIZE - 1)
+ *   @ + 70 个 a   (srclen 71) → 59 字节,"..." + 56
+ *   @ + 86 字节路径(srclen 87) → 59 字节
+ *   80 个 s                    → [string "sss..."] 共 59 字节
+ *   "has\n..."                 → [string "has..."] 共 17 字节(只取第一行)
+ * 
+ *

+ * 关键点是 {@code srclen} 含 那个前导字符({@code '='} / {@code '@'})。 + * C 版 {@code memcpy} 的末位读到的是字符串结尾的 {@code '\0'},所以可见内容比 + * 参数里的 {@code bufflen} 少一;照抄那个长度在 Java 里就是越界。 + */ +final class ChunkIdTest { + + private static String id(String src) { + return new String(LuaState.chunkId(src.getBytes(StandardCharsets.UTF_8)), + StandardCharsets.UTF_8); + } + + /** {@code '='}:短名原样 */ + @Test + void literalShort() { + assertEquals("nnnnnnnnnnnnnnnnnnnnnnnnnnnnnn", id("=" + "n".repeat(30))); + assertEquals("short", id("=short")); + } + + /** {@code '='}:过长从右边截到 {@code LUA_IDSIZE - 1} */ + @Test + void literalLong() { + String s = id("=" + "n".repeat(80)); + assertEquals(59, s.length()); + assertEquals("n".repeat(59), s); + } + + /** + * {@code '@'}:过长在前面加 {@code ...},保留末尾那一段。 + *

+ * 这一条就是那个越界 bug:C 的 {@code bufflen} 是 57,实际搬 56 字节。 + */ + @Test + void fileNameLong() { + String s = id("@" + "a".repeat(70)); + assertEquals(59, s.length()); + assertEquals("..." + "a".repeat(56), s); + + String p = id("@" + "/dir".repeat(20) + "/f.lua"); + assertEquals(59, p.length()); + assertEquals("...ir/dir/dir/dir/dir/dir/dir/dir/dir/dir/dir/dir/dir/f.lua", p); + } + + /** {@code '@'}:短名去掉 {@code '@'} 原样 */ + @Test + void fileNameShort() { + assertEquals("x.lua", id("@x.lua")); + assertEquals(59, id("@" + "a".repeat(59)).length()); // srclen 60 仍在界内 + } + + /** 其余:包成 {@code [string "..."]},过长在后面加 {@code ...} */ + @Test + void stringSource() { + assertEquals("[string \"sssssssssssssssssssssssssssssssssssssssssssss...\"]", + id("s".repeat(80))); + } + + /** 只取第一行 */ + @Test + void stringSourceStopsAtNewline() { + assertEquals("[string \"has...\"]", id("has\nnewline and " + "z".repeat(80))); + assertEquals("[string \"abc\"]", id("abc")); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/ClosureTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/ClosureTest.java new file mode 100644 index 0000000..4c33fe7 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/ClosureTest.java @@ -0,0 +1,357 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * 阶段 4 的闸门:闭包与 upvalue、{@code goto}/标签、变长参数、多重赋值与多重返回。 + *

+ * 与 {@link RuntimeTest} 走同一套差分驱动({@link Diff}),差别只在选例:这里 + * 专挑「同一个变量被多处引用」的场景——闭包捕获、upvalue 开关闭、跳出作用域、 + * 参数个数与返回值个数的各种失配。这类 bug 的共同特征是不报错、只是值不对: + * 一个 upvalue 关错了时机,闭包读到的就是过期值,脚本照样跑完。 + *

+ * 所以这里的用例一律「先改、后读」:写进去一个值,换一条路径(离开作用域、 + * 跳到标签、走一趟函数调用)再读回来,让读不到正确值的实现当场露馅。 + */ +class ClosureTest { + + @BeforeAll + static void requireReference() { + assumeTrue(ReferenceLua.available(), + "no lua5.4 reference binary; set LUA54_BIN. found: " + ReferenceLua.describe()); + } + + static List scripts() { + List s = new ArrayList<>(); + + // ---- 闭包与 upvalue ---- + + // 一个 upvalue 被两个闭包共享,改一个另一个要看得见 + s.add("local x = 0 local function inc() x = x + 1 end" + + " local function get() return x end" + + " inc() inc() inc() print(get(), x)"); + // 反向:闭包里改,外面读 + s.add("local x = 1 local function set(v) x = v end" + + " set(42) print(x)"); + // 闭包在变量作用域结束之后仍然活着 —— 这是「关闭 upvalue」的核心场景 + s.add("local f do local x = 'captured' f = function() return x end end print(f())"); + // 同一个外层局部变量被三层嵌套捕获 + s.add("local x = 'a' local f1 = function() local f2 = function()" + + " local f3 = function() return x end return f3 end return f2 end print(f1()()())"); + // upvalue 指向表:共享的是引用,不是副本 + s.add("local t = {} local function put(k, v) t[k] = v end" + + " local function get(k) return t[k] end put('a', 1) print(get('a'), #t)"); + // 循环里建闭包:每个闭包捕获自己的那份 i(Lua 5.4 的语义) + s.add("local fs = {} for i = 1, 3 do fs[i] = function() return i end end" + + " print(fs[1](), fs[2](), fs[3]())"); + s.add("local fs = {} local i = 1 while i <= 3 do" + + " local j = i fs[i] = function() return j end i = i + 1 end" + + " print(fs[1](), fs[2](), fs[3]())"); + // 泛型 for 的控制变量同样每轮一份 + s.add("local fs = {} for _, v in ipairs({'x','y','z'}) do" + + " fs[#fs+1] = function() return v end end" + + " print(fs[1](), fs[2](), fs[3]())"); + // 闭包返回闭包,中间那层的局部变量也要留住 + s.add("local function counter() local n = 0" + + " return function() n = n + 1 return n end end" + + " local a, b = counter(), counter() print(a(), a(), b(), a())"); + // 递归闭包:函数体里引用自己 + s.add("local function fact(n) if n <= 1 then return 1 end return n * fact(n - 1) end" + + " print(fact(10))"); + // 互相递归(靠前向声明) + s.add("local isEven, isOdd" + + " isEven = function(n) return n == 0 or isOdd(n - 1) end" + + " isOdd = function(n) return n ~= 0 and isEven(n - 1) end" + + " print(isEven(10), isOdd(10))"); + // upvalue 被关闭后再赋值,两边应当断开 + s.add("local f local x = 1 do f = function() return x end end x = 2 print(f(), x)"); + s.add("local x = 1 local f = function() return x end x = 2 print(f(), x)"); + // 表里的 upvalue 与栈上的同名变量 + s.add("local t = {v = 1} local function bump() t.v = t.v + 1 end" + + " bump() bump() print(t.v)"); + // 把一个闭包当值传来传去 + s.add("local function make() local n = 0 return function() n = n + 1 return n end end" + + " local t = {f = make()} print(t.f(), t.f())"); + // 方法定义里的 self 与 upvalue 混用 + s.add("local n = 0 local o = {} function o:inc() n = n + self.step end" + + " o.step = 5 o:inc() o:inc() print(n)"); + + // ---- goto 与标签 ---- + + s.add("local i = 1 local s = 0 ::top:: if i <= 5 then s = s + i i = i + 1 goto top end" + + " print(s)"); + s.add("for i = 1, 5 do if i == 3 then goto cont end print(i) ::cont:: end print('done')"); + s.add("do goto done ::skipped:: print('never') end ::done:: print('ok')"); + // goto 跳出多层块 + s.add("for i = 1, 3 do for j = 1, 3 do if j == 2 then goto next_i end print(i, j) end" + + " ::next_i:: end print('end')"); + // goto 跳出循环后局部变量已失效 + s.add("local r do for i = 1, 3 do if i == 2 then goto out end end ::out:: r = 'jumped' end" + + " print(r)"); + // 标签在块末尾(需要一个空语句收尾) + s.add("local function f() do goto e end ::e:: return 'ret' end print(f())"); + // goto 与 break 混用 + s.add("local n = 0 while true do n = n + 1 if n >= 3 then break end end print(n)"); + // goto 跳回循环开头(continue 的写法) + s.add("local s = 0 for i = 1, 6 do if i % 2 == 0 then goto cont end s = s + i ::cont:: end" + + " print(s)"); + // 反向跳转跨过局部变量声明 + s.add("local n = 0 ::again:: n = n + 1 if n < 3 then goto again end print(n)"); + + // ---- 变长参数 ---- + + s.add("local function f(...) return select('#', ...) end" + + " print(f(), f(nil), f(nil, nil), f(1, 2, 3))"); + s.add("local function f(...) local a, b, c = ... return a, b, c end print(f(1, 2, 3, 4))"); + s.add("local function f(...) local a, b = ... return b, a end print(f(1))"); + s.add("local function f(a, b, ...) return a, b, select('#', ...) end" + + " print(f(1, 2, 3, 4, 5))"); + s.add("local function f(a, ...) return a, ... end print(f(1, 2, 3))"); + s.add("local function f(...) return ... end print(f(f(1, 2), 3))"); + s.add("local function f(...) return ... , 9 end print(f(1, 2))"); + s.add("local function f(...) local t = {...} return #t end print(f(1, 2, 3, nil))"); + s.add("local function sum(...) local s = 0 for _, v in ipairs({...}) do s = s + v end" + + " return s end print(sum(1, 2, 3, 4, 5))"); + // 主 chunk 自己就是 vararg + s.add("print(select('#', ...))"); + s.add("local a, b = ... print(a, b)"); + // 变长参数穿过元方法 + s.add("local mt = {__call = function(self, ...) return select('#', ...) end}" + + " print(setmetatable({}, mt)(1, 2, 3))"); + // 变长参数与尾调用 + s.add("local function g(...) return select('#', ...) end" + + " local function f(...) return g(...) end print(f(1, 2, 3))"); + + // ---- 多重赋值 ---- + + s.add("local a, b = 1, 2 a, b = b, a print(a, b)"); + s.add("local a, b, c = 1 print(a, b, c)"); + s.add("local a, b, c = 1, 2, 3, 4 print(a, b, c)"); + s.add("local function two() return 1, 2 end local a, b = two() print(a, b)"); + s.add("local function two() return 1, 2 end local a, b, c = two() print(a, b, c)"); + s.add("local function two() return 1, 2 end local a = two() print(a)"); + s.add("local a, b = (function() return 1, 2 end)() print(a, b)"); + // 右边先整体求值:t[1] 要用交换前的 t + s.add("local t = {1, 2} t[1], t[2] = t[2], t[1] print(t[1], t[2])"); + s.add("local t = {a = 1, b = 2} t.a, t.b = t.b, t.a print(t.a, t.b)"); + s.add("local o = {} local i = 1 o[i], i = 'first', 2 print(o[1], i)"); + // 赋值链上带函数调用 + s.add("local function id(...) return ... end" + + " local a, b, c = id(1), id(2, 3), id(4) print(a, b, c)"); + s.add("local function id(...) return ... end local a, b = id(id(1, 2)) print(a, b)"); + // 全局赋值 + s.add("g1, g2 = 1, 2 print(g1, g2)"); + s.add("local t = {} t.x, t.y = 1, 2 print(t.x, t.y)"); + // 多重赋值里的 upvalue + s.add("local x, y = 1, 2 local function swap() x, y = y, x end" + + " swap() print(x, y) swap() print(x, y)"); + + // ---- 多重返回 ---- + + s.add("local function three() return 1, 2, 3 end print(three())"); + s.add("local function three() return 1, 2, 3 end print((three()))"); + s.add("local function three() return 1, 2, 3 end" + + " print(three(), three(), three())"); + s.add("local function three() return 1, 2, 3 end" + + " local t = {three(), three(), three()} print(#t)"); + s.add("local function three() return 1, 2, 3 end" + + " local t = {[1] = three()} print(#t, t[1])"); + s.add("local function three() return 1, 2, 3 end" + + " local t = {three(), (three())} print(#t)"); + // return 后面接表达式:只有最后一项展开 + s.add("local function f() local function three() return 1, 2, 3 end" + + " return three(), three() end print(f())"); + s.add("local function f() local function three() return 1, 2, 3 end" + + " return (three()) end print(f())"); + s.add("local function f(...) return ... end print(f(1, 2, 3))"); + s.add("local function f(...) return (...) end print(f(1, 2, 3))"); + // 尾调用 + s.add("local function loop(n, acc) if n == 0 then return acc end" + + " return loop(n - 1, acc + n) end print(loop(10000, 0))"); + s.add("local function a(x) return b(x) end local function b(x) return x * 2 end" + + " print(a(21))"); + // return 从深层块里跳出来 + s.add("local function f() for i = 1, 10 do if i == 3 then return i end end" + + " return 'none' end print(f())"); + s.add("local function f() do do do return 'deep' end end end end print(f())"); + // 返回值个数在调用点被截断 / 补齐 + s.add("local function one() return 1 end local a, b, c = one() print(a, b, c)"); + s.add("local function none() end local a, b = none() print(a, b)"); + s.add("print((function() end)())"); + + // ---- 批 2:专挑「不报错、只是值不对」的场景 ---- + + // 闭包活得比栈帧久:返回之后栈会被后续调用反复覆盖, + // upvalue 没在返回时关闭就会读到别人写进去的值 + s.add("local function make(v) local x = v return function() return x end end" + + " local a = make('A') local b = make('B') local c = make('C')" + + " print(a(), b(), c())"); + s.add("local function make(v) local x = v return function() return x end end" + + " local fs = {} for i = 1, 8 do fs[i] = make(i * 10) end" + + " local s = 0 for i = 1, 8 do s = s + fs[i]() end print(s)"); + // 闭包之后再跑一堆递归,把栈压深再回来读 + s.add("local function make(v) local x = v return function() return x end end" + + " local f = make('keep')" + + " local function rec(n) if n == 0 then return 0 end return 1 + rec(n - 1) end" + + " rec(200) print(f())"); + // break 跳出循环时也要关 upvalue + s.add("local f for i = 1, 5 do local x = i if i == 3 then f = function() return x end" + + " break end end print(f())"); + // goto 跳出时同样 + s.add("local f do for i = 1, 5 do local x = i if i == 2 then" + + " f = function() return x end goto out end end end ::out:: print(f())"); + // 两个闭包捕获同一个变量必须共用一份 upvalue + s.add("local x = 0 local a = function() return x end local b = function() x = x + 1 end" + + " b() b() print(a(), x)"); + // 三层嵌套各改一次,值要一路传下去 + s.add("local x = 0" + + " local f1 = function() x = x + 1 local y = x" + + " local f2 = function() x = x + 10 return y end return f2 end" + + " local g = f1() print(g(), x) print(g(), x)"); + // 关闭之后外层再改,闭包不应跟着变 + s.add("local f = nil do local x = 1 f = function() return x end end" + + " local t = {f()} print(t[1])"); + // 闭包在表里,稍后才调 + s.add("local t = {} do local x = 'held' t.get = function() return x end" + + " t.set = function(v) x = v end end t.set('new') print(t.get())"); + // 闭包由 C 函数(pcall)调用 + s.add("local x = 7 print(pcall(function() return x end))"); + // 出错时 upvalue 也要关住 + s.add("local f do local x = 'err' f = function() return x end" + + " pcall(function() error('x') end) end print(f())"); + + // / 与闭包 + s.add("local x = 5 local f = function() return x end print(f())"); + s.add("local log = {} do local c = setmetatable({}," + + " {__close = function() log[#log+1] = 'closed' end})" + + " local f = function() return 'in' end print(f()) end" + + " print(log[1])"); + s.add("local x = nil print(x)"); + + // 变长参数的边界 + s.add("local function f(...) return select('#', ...) end" + + " print(f(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))"); + s.add("local function f(...) return select(-1, ...) end print(f(1, 2, 3))"); + s.add("local function f(...) return select(-3, ...) end print(f(1, 2, 3))"); + s.add("local function f(...) return select(-4, ...) end print(f(1, 2, 3))"); + s.add("local function f(...) return ... end print(f(nil, nil))"); + s.add("local function f(...) local t = {...} return t[1], t[2], #t end print(f(nil, 1))"); + s.add("local function f(n, ...) return n, select('#', ...) end print(f(1))"); + s.add("local args = {} for i = 1, 60 do args[i] = i end" + + " local function f(...) return select('#', ...) end print(f(table.unpack(args)))"); + + // 多重赋值的求值顺序:下标表达式在赋值之前全部求值 + s.add("local t = {} local i = 1 local function next_i() i = i + 1 return i end" + + " t[next_i()], t[next_i()] = 'a', 'b' print(t[2], t[3], i)"); + s.add("local t = {} local n = 0 local function bump() n = n + 1 return n end" + + " t[bump()] = bump() print(n, t[1])"); + // 带 __newindex 的多次赋值:每一次都过元方法 + s.add("local log = {} local t = setmetatable({}," + + " {__newindex = function(_, k, v) log[#log+1] = k .. '=' .. v end})" + + " t.a, t.b = 1, 2 print(#log, log[1], log[2])"); + // 交换里带函数调用 + s.add("local function id(...) return ... end local a, b = 1, 2" + + " a, b = id(b), id(a) print(a, b)"); + // 多重赋值右边比左边短 / 长 + s.add("local a, b, c = (function() return 1 end)() print(a, b, c)"); + s.add("local a, b = 1, 2, 3 print(a, b)"); + // 多重赋值链:右值里的函数调用先全部跑完 + s.add("local order = {} local function p(x) order[#order+1] = x return x end" + + " local a, b = p(1), p(2) print(order[1], order[2], a, b)"); + + // 多重返回的落点 + s.add("local function three() return 1, 2, 3 end" + + " local t = {[1] = 'x', three()} print(#t, t[1], t[2])"); + s.add("local function three() return 1, 2, 3 end" + + " local ok, a, b = pcall(three) print(ok, a, b)"); + s.add("local function three() return 1, 2, 3 end" + + " print(#{three(), three(), three(), three()})"); + // 尾调用链 + s.add("local function c(x) return x end local function b(x) return c(x) end" + + " local function a(x) return b(x) end print(a('through'))"); + s.add("local function even(n) if n == 0 then return true end return odd(n - 1) end" + + " local function odd(n) if n == 0 then return false end return even(n - 1) end" + + " print(even(500), odd(500))"); + // return 在元方法里 + s.add("local mt = {__call = function(_, x) return x, x * 2 end}" + + " print(setmetatable({}, mt)(3))"); + // 闭包返回值又当参数 + s.add("local function mk() return function() return 1, 2 end end" + + " print(select('#', mk()()))"); + + // ---- 批 3: 的关闭顺序与 upvalue 的关闭时机 ---- + // + // 这一组专测「什么时候关、按什么顺序关」。顺序错了不报错,只是 + // 打印出来的一串名字次序不对,所以每条都要把次序完整打出来。 + + s.add("local log = {} local function mk(n) return setmetatable({}," + + " {__close = function() log[#log+1] = n end}) end" + + " do local a = mk('a') local b = mk('b')" + + " local c = mk('c') end print(log[1], log[2], log[3])"); + // return 之前也要关 + s.add("local log = {} local function mk(n) return setmetatable({}," + + " {__close = function() log[#log+1] = n end}) end" + + " local function f() local a = mk('a') return 'ret' end" + + " print(f(), log[1])"); + // break 之前 + s.add("local log = {} local function mk(n) return setmetatable({}," + + " {__close = function() log[#log+1] = n end}) end" + + " for i = 1, 3 do local a = mk('a' .. i) if i == 2 then break end end" + + " print(log[1], log[2], log[3])"); + // goto 之前 + s.add("local log = {} local function mk(n) return setmetatable({}," + + " {__close = function() log[#log+1] = n end}) end" + + " do for i = 1, 3 do local a = mk('a' .. i)" + + " if i == 2 then goto out end end end ::out:: print(log[1], log[2], log[3])"); + // 循环体每轮关一次 + s.add("local log = {} local function mk(n) return setmetatable({}," + + " {__close = function() log[#log+1] = n end}) end" + + " for i = 1, 3 do local a = mk(i) end print(#log, log[1], log[3])"); + // 出错时按栈从顶往下关,且错误对象要传进去 + s.add("local log = {} local function mk(n) return setmetatable({}," + + " {__close = function(_, err) log[#log+1] = n .. ':' .. tostring(err) end}) end" + + " print(pcall(function() local a = mk('a')" + + " local b = mk('b') error('boom') end))" + + " print(log[1], log[2])"); + // __close 自己出错:沿用原错误 + s.add("print(pcall(function() local a = setmetatable({}," + + " {__close = function() error('in close') end}) error('orig') end))"); + // 普通(非 )upvalue 出错时也不能泄漏 + s.add("local f do local x = 'keep' f = function() return x end" + + " pcall(function() error('x') end) end print(f())"); + // 同一个函数被调用两次,每次是各自的 upvalue + s.add("local function mk() local v = 0 return function() v = v + 1 return v end," + + " function() return v end end" + + " local a1, a2 = mk() local b1, b2 = mk()" + + " a1() a1() b1() print(a2(), b2())"); + // 闭包捕获的 upvalue 又被内层闭包捕获 + s.add("local x = 1 local outer = function()" + + " local inner = function() return x end x = x + 1 return inner end" + + " local f = outer() print(f(), x)"); + // 函数在表里定义,捕获外层局部 + s.add("local n = 5 local t = {get = function() return n end," + + " set = function(v) n = v end} t.set(9) print(t.get(), n)"); + // _ENV 作为 upvalue 被换掉 + s.add("local print = print do local _ENV = {print = print}" + + " hidden = 1 print(hidden) end print(hidden)"); + // 局部函数与全局函数 + s.add("function gfun() return 'global' end print(gfun())"); + s.add("local t = {} function t.m() return 'method' end print(t.m())"); + s.add("local t = {} function t:m() return self.tag end t.tag = 'T' print(t:m())"); + + return s; + } + + @ParameterizedTest(name = "script #{index}") + @MethodSource("scripts") + void scriptMatchesReference(String src) { + Diff.assertMatches(src); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/CodegenTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/CodegenTest.java new file mode 100644 index 0000000..e010d38 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/CodegenTest.java @@ -0,0 +1,194 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * 字节码差分测试 —— 编译器的主闸门。 + *

+ * 逐个脚本比较「参考实现编出来的字节码」与「我们编出来的字节码」, + * 逐字段比:opcode、A/B/C/k/Bx/sBx/Ax/sB/sC/sJ、行号、常量表、 + * upvalue 表、嵌套函数。 + *

+ * 为什么这道闸门值得单独建:parser 与 codegen 的错误绝大多数是「编出来能跑、 + * 但多/少了一条指令、常量表多了一项、跳转差一格」。这类偏差在运行时 + * 往往不报错,直到某天踩到具体分支才炸。逐字段比对能当场抓住。 + */ +class CodegenTest { + + @BeforeAll + static void requireDumper() { + Assumptions.assumeTrue(ReferenceLua.findFkdump() != null, + "找不到 fkdump 转储器,跳过字节码差分(设 FKDUMP_BIN 指向它)"); + } + + private static final String[] SCRIPTS = { + /* 0 */ "local x = 1\n", + /* 1 */ "local x = 1\nprint(x + 2)\n", + /* 2 */ "local y = 3\nprint(y)\n", + /* 3 */ "if a then b = 1 else b = 2 end\n", + /* 4 */ "if a then b = 1 elseif c then b = 2 elseif d then b = 3 else b = 4 end\n", + /* 5 */ "while a do b() end\n", + /* 6 */ "while a do if b then break end c() end\n", + /* 7 */ "for i = 1, 10 do print(i) end\n", + /* 8 */ "for i = 10, 1, -1 do print(i) end\n", + /* 9 */ "for k, v in pairs(t) do print(k, v) end\n", + /* 10 */ "for a, b, c in f do g(a, b, c) end\n", + /* 11 */ "local t = {1, 2, 3}\n", + /* 12 */ "local t = {1, 2, 3, x = 4, [5] = 6, ['y'] = 7}\n", + /* 13 */ "local t = {f(), g()}\n", + /* 14 */ "function f(a, b, ...) return a + b, ... end\n", + /* 15 */ "local function g() end\n", + /* 16 */ "repeat x = x + 1 until x > 10\n", + /* 17 */ "do goto done end\n::done::\n", + /* 18 */ "print(#s, -x, not y, ~z)\n", + /* 19 */ "local s = 'a' .. 'b' .. 'c'\n", + /* 20 */ "return\n", + /* 21 */ "return 1, 2\n", + /* 22 */ "return f()\n", + /* 23 */ "local x = a and b or c\n", + /* 24 */ "a.b.c = 1\n", + /* 25 */ "a:m(1)\n", + /* 26 */ "local x = 1 + 2 * 3 // 4 % 5\n", + /* 27 */ "local x = 2 ^ 3 ^ 4\n", + /* 28 */ "x = y\n", + /* 29 */ "local a, b = 1\n", + /* 30 */ "local a, b = f()\n", + /* 31 */ "local x = 1\nlocal function f() return x end\nreturn f\n", + /* 32 */ "local x = nil\n", + /* 33 */ "local t = {}\nt[1] = 2\nt.x = t.y\n", + /* 34 */ "a[b], a = 1, 2\n", + /* 35 */ "print(1 == 2, 3 ~= 4, 5 < 6, 7 <= 8, 9 > 10, 11 >= 12)\n", + /* 36 */ "local x = 1.5\nlocal y = 0x10\nlocal z = 1e100\n", + /* 37 */ "local x = -2 ^ 2\n", + /* 38 */ "if a then return end\nprint(1)\n", + /* 39 */ "local f = function(...) return ... end\n", + /* 40 */ "do end\n", + /* 41 */ "local x = 100000\nlocal y = 100000.0\n", + /* 42 */ "for i = 1, 10 do\n for j = 1, 10 do\n print(i, j)\n end\nend\n", + /* 43 */ "local t = {[1] = 1, [2] = 2, [3] = 3}\n", + /* 44 */ "print(('x'):rep(3))\n", + /* 45 */ "local n = 0\nwhile n < 10 do n = n + 1 end\nreturn n\n", + /* 46 */ "local a, b, c = 1, 2, 3\nreturn a, b, c\n", + /* 47 */ "for i = 1, 10 do if x then break end end\n", + /* 48 */ "local s = 'a\\0b'\n", + /* 49 */ "return ...\n", + }; + + @Test + void bytecodeMatchesReference() { + int checked = 0; + for (int i = 0; i < SCRIPTS.length; i++) { + String src = SCRIPTS[i]; + byte[] bytes = src.getBytes(StandardCharsets.UTF_8); + String ref = ReferenceLua.dumpProto(bytes, "target/fkcodegen/t.lua"); + assertNotNull(ref, "fkdump 没有输出"); + if (ref.startsWith("LOADERR")) { + throw new AssertionError("脚本 #" + i + " 参考实现就编译不过:" + ref + "\n" + src); + } + Proto ours; + try { + ours = Parser.parse(bytes, "t".getBytes(StandardCharsets.UTF_8), new StringPool()); + } catch (RuntimeException e) { + throw new AssertionError("脚本 #" + i + " 我们这边编译失败:" + e + "\n" + src, e); + } + String got = ProtoDump.dump(ours); + assertEquals(normalize(ref), normalize(got), "脚本 #" + i + "\n" + src + "\n" + firstDiff(normalize(ref), normalize(got))); + checked++; + } + assertEquals(SCRIPTS.length, checked); + } + + /** + * 用上游自带的测试套件({@code lua-5.4.x/tests/*.lua})当语料,逐文件比对字节码。 + *

+ * 这批脚本是 Lua 作者自己写的、专门用来压边界的(goto 跳出作用域、 + * 多重赋值的求值顺序、const/close 属性、vararg、超长常量表……), + * 比手写几十条用例强得多。只编译不运行——运行要等 VM 落地。 + *

+ * 目录由环境变量 {@code LUA54_TESTS} 指定,默认 {@code target/lua54-tests}; + * 不存在则跳过(CI 的 conformance job 会下载并解压到那里)。 + */ + @Test + void upstreamTestSuiteCompilesIdentically() { + String dir = System.getenv("LUA54_TESTS"); + if (dir == null || dir.isEmpty()) { + dir = "target/lua54-tests"; + } + java.io.File[] files = new java.io.File(dir).listFiles((d, n) -> n.endsWith(".lua")); + Assumptions.assumeTrue(files != null && files.length > 0, + "没有上游测试套件语料,跳过:" + dir); + java.util.Arrays.sort(files); + + int checked = 0; + for (java.io.File file : files) { + byte[] bytes; + try { + bytes = java.nio.file.Files.readAllBytes(file.toPath()); + } catch (java.io.IOException e) { + throw new AssertionError("读不了 " + file, e); + } + String ref = ReferenceLua.dumpProto(bytes, "target/fkcodegen/t.lua"); + assertNotNull(ref, "fkdump 没有输出"); + if (ref.startsWith("LOADERR")) { + throw new AssertionError("参考实现就编译不过:" + ref + "(" + file + ")"); + } + Proto ours; + try { + ours = Parser.parseFile(bytes, "t".getBytes(StandardCharsets.UTF_8), + new StringPool()); + } catch (RuntimeException e) { + throw new AssertionError("我们这边编译失败:" + e + "(" + file + ")", e); + } + assertEquals(normalize(ref), normalize(ProtoDump.dump(ours)), "文件 " + file.getName() + "\n" + firstDiff(normalize(ref), normalize(ProtoDump.dump(ours)))); + checked++; + } + assertEquals(files.length, checked); + } + + /** + * 失败时只报第一处不同及其上下文。 + *

+ * 整份转储动辄上千行,直接丢进断言信息里等于什么都没说;真正要看的只有 + * 第一个分歧点。 + */ + private static String firstDiff(String expected, String actual) { + String[] a = expected.split("\n", -1); + String[] b = actual.split("\n", -1); + int n = Math.max(a.length, b.length); + for (int i = 0; i < n; i++) { + String x = i < a.length ? a[i] : "<缺行>"; + String y = i < b.length ? b[i] : "<缺行>"; + if (!x.equals(y)) { + StringBuilder sb = new StringBuilder(); + sb.append("第 ").append(i + 1).append(" 行不同(共 ref=").append(a.length) + .append(" ours=").append(b.length).append(")\n"); + for (int j = Math.max(0, i - 3); j < Math.min(n, i + 4); j++) { + String p = j < a.length ? a[j] : "<缺行>"; + String q = j < b.length ? b[j] : "<缺行>"; + sb.append(p.equals(q) ? " " : "! ").append(j + 1) + .append(" ref=").append(p).append("\n"); + if (!p.equals(q)) { + sb.append(" ours=").append(q).append("\n"); + } + } + return sb.toString(); + } + } + return "<无差异>"; + } + + private static String normalize(String s) { + String out = s.replace("\r\n", "\n"); + while (out.endsWith("\n")) { + out = out.substring(0, out.length() - 1); + } + return out; + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/ConformanceTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/ConformanceTest.java new file mode 100644 index 0000000..5d174c5 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/ConformanceTest.java @@ -0,0 +1,227 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * 一致性测试:拿真实的 {@code lua5.4} 二进制当裁判。 + *

+ * 这是本项目的主闸门(见 README「怎么证明它真的是 Lua」)。目前解释器还没落地, + * 所以这里先做两件已经能做的事: + *

    + *
  1. 确认参考二进制可用 —— 否则后面所有差分结论都无从谈起;
  2. + *
  3. 把已经移植好的那部分语义(表键规范化、{@code #t} 边界等)用真实 + * Lua 跑一遍并记录结果,作为可执行的语义契约。
  4. + *
+ * 第 2 条不是空转:它把「我们以为 Lua 是这样」变成「实测 Lua 是这样」, + * 一旦参考版本变了、或者当初的理解是错的,这里会先红。等解释器在阶段 3 落地, + * 这些脚本改成两侧各跑一遍、比对 stdout 即可,无需另建框架。 + */ +public class ConformanceTest { + + /** + * 没有参考二进制时整类跳过。 + *

+ * 为什么是 skip 而不是 fail:普通的 {@code mvn test}(本地与 build job) + * 不保证装了 lua5.4,硬失败会让「没装参考实现」变成编译失败,掩盖真正的 + * 回归。真正的一致性校验放在 CI 的 conformance job 里跑——那个 job 会先 + * 从源码构建 lua5.4,构建不出来它自己就红了,不需要这里再守一道。 + */ + @BeforeAll + public static void requireReference() { + Assumptions.assumeTrue(ReferenceLua.available(), + "找不到 lua5.4 参考二进制,跳过一致性测试。设置 LUA54_BIN," + + "或在仓库内构建到 target/lua54/src/lua。查找结果:" + ReferenceLua.describe()); + } + + /** 参考二进制确实是 5.4(5.5 或其他版本会得出错误的「不一致」结论) */ + @Test + public void referenceBinaryIsLua54() { + ReferenceLua.Result r = ReferenceLua.run("print(_VERSION)"); + assertEquals(0, r.exitCode); + assertTrue(r.output.contains("Lua 5.4"), "版本应当是 5.4,实际:" + r.output); + } + + /** + * 表键语义:整值浮点规范化、整数键与字符串键分离、{@code #t} 的边界。 + *

+ * 这些断言与 {@link LuaTableTest} 里的一一对应,区别在于这里的期望值是 + * 真实 Lua 5.4 跑出来的,不是我们替它写的。 + */ + @Test + public void tableKeySemantics() { + String script = "" + + "local t = {}\n" + + "t[1.0] = 'a'\n" + + "local k, v = next(t)\n" + + "print(math.type(k), k, v, #t)\n" + + "t['1'] = 'b'\n" + + "print(t[1], t['1'])\n" + + "local n = 0 for _ in pairs(t) do n = n + 1 end print(n)\n" + + "local z = {}\n" + + "z[0.0] = 'zero'\n" + + "print(#z, z[0], z[1])\n" + + "local f = {}\n" + + "f[1.5] = 'x'\n" + + "local fk = next(f) print(math.type(fk), fk)\n" + + "local ok, err = pcall(function() local q = {} q[0/0] = 1 end)\n" + + "print(ok, err ~= nil and tostring(err):find('table index is NaN') ~= nil)\n"; + ReferenceLua.Result r = ReferenceLua.run(script); + assertEquals(0, r.exitCode, r.toString()); + assertEquals("" + + "integer\t1\ta\t1\n" + + "a\tb\n" + + "2\n" + + "0\tzero\tnil\n" + + "float\t1.5\n" + + "false\ttrue\n", r.output.replaceAll("\r\n", "\n")); + } + + /** + * 词法错误信息:两侧逐字比对。 + *

+ * 这是第一个真正意义上的差分用例(前面的用例只是记录参考行为)。 + * 做法是把每一段错误源码写成文件交给参考实现跑,再把同一份字节喂给我们的 + * {@link Lexer},两边比错误信息的完整文本。 + *

+ * 之所以值得单独做:错误信息的措辞、{@code near} 后面那段 token 的切法 + * (保留几个字符、要不要带引号、{@code } 有没有引号)全是上游的 + * 细节约定,凭记忆写必错;而真实项目里 {@code load} 失败时打印的就是 + * 这个字符串,逐字节一致才有意义。 + */ + @Test + public void lexerErrorMessagesMatchReference() { + String[] cases = { + "x = 1a", // malformed number + "x = 0x", + "x = 1e+", + "x = \"abc", // unfinished string,撞 EOZ + "x = \"abc\n", // unfinished string,撞换行 + "x = [[abc", // unfinished long string + "--[[ x", // unfinished long comment + "x = [==abc]==]", // invalid long string delimiter + "x = \"a\\qb\"", // invalid escape sequence + "x = \"\\256\"", // decimal escape too large + "x = \"\\xZ1\"", // hexadecimal digit expected + "x = \"\\x4\"", + "x = \"\\u{80000000}\"", // UTF-8 value too large + "x = \"\\u{41\"", // missing '}' + "x = \"\\uA\"", // missing '{' + "x = 1\n[[abc\n", // 跨行的 unfinished long string + }; + // 路径必须短:参考实现会把过长的源名截成 '...'(LUA_IDSIZE) + String path = "target/fklex.lua"; + for (String src : cases) { + byte[] bytes = src.getBytes(StandardCharsets.ISO_8859_1); + ReferenceLua.FileResult ref = ReferenceLua.runBytes(bytes, path); + assertEquals(1, ref.result.exitCode, + "参考实现应当在加载阶段就失败,用例:" + escape(src)); + int at = ref.result.output.indexOf(ref.path + ":"); + assertTrue(at >= 0, "参考输出里应当带脚本路径,实际:" + ref.result.output); + String expected = ref.result.output.substring(at).trim(); + + String actual; + try { + /* + * 源名带 '@':lua.c 加载脚本时加的就是这个前缀, + * luaO_chunkid 再把它剥掉,于是两边看到的都是裸路径。 + * 不带的话这里会得到 [string "target/fklex.lua"]。 + */ + Lexer ls = new Lexer(bytes, ("@" + ref.path).getBytes(StandardCharsets.UTF_8), + new StringPool()); + ls.next(); + while (ls.t.token != Lexer.TK_EOS) { + ls.next(); + } + fail("期望词法错误但没有报错,用例:" + escape(src)); + return; + } catch (LuaError e) { + assertNotNull(e.value); + actual = e.getMessage().trim(); + } + assertEquals(expected, actual, "用例:" + escape(src)); + } + } + + /** + * 反向验证:参考实现能加载的源码,我们的词法层也不能报错。 + *

+ * 只查「不报错」这一件事——token 序列的逐条比对要等 parser 落地才能做 + * (没有语法树就没有可比的中介表示)。但这一条已经能挡住最常见的 + * 「词法层把合法写法当成非法」这类回归,比如误拒 {@code 0x1p4} + * 或把 {@code \z} 后面的内容吃掉。 + */ + @Test + public void lexerAcceptsWhatReferenceAccepts() { + String[] cases = { + "return 1", + "local x = 0x1p4 return x", + "local s = [[hi]] return s", + "--[[ 长注释 ]] return", + "-- 短注释\nreturn", + // 注意:不能放 '::l:: goto l' —— 它在参考实现里是死循环,会挂住测试 + "do goto done end ::done:: return", + "return 1 // 2, 1 >> 2, 1 << 2", + "return '\\u{4E2D}'", + "return '\\z x'", + "return 'a\\\nb'", + "local f = function(...) return ... end return f(1)", + "return [==[a]]b]==]", + "return 0x10, 0X1P4, .5, 1e2", + "local t = {[1]=1, x=2} return t", + "return #'abc' ~= 3", + "local a = 1 return a", + "return ('x'):upper()", + }; + String path = "target/fklexok.lua"; + for (String src : cases) { + byte[] bytes = src.getBytes(StandardCharsets.ISO_8859_1); + ReferenceLua.FileResult ref = ReferenceLua.runBytes(bytes, path); + assertEquals(0, ref.result.exitCode, + "参考实现应当能加载,用例:" + escape(src) + " -> " + ref.result.output); + + Lexer ls = new Lexer(bytes, path.getBytes(StandardCharsets.UTF_8), new StringPool()); + try { + ls.next(); + while (ls.t.token != Lexer.TK_EOS) { + ls.next(); + } + } catch (LuaError e) { + fail("我们的词法层报错了,但参考实现能加载。用例:" + escape(src) + + ",错误:" + e.getMessage()); + } + } + } + + private static String escape(String s) { + return s.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t"); + } + + /** {@code #t} 在数组部分与哈希部分交界处的行为 */ + @Test + public void lengthBorderSemantics() { + String script = "" + + "local z = {}\n" + + "for i = 1, 8 do z[i] = i end\n" + + "print(#z)\n" + + "z[9] = 9\n" + + "print(#z)\n" + + "z[4] = nil\n" + + "print(#z >= 3 and #z <= 9, z[4])\n" + + "local w = {}\n" + + "w[2^53] = 1\n" + + "print(#w, w[2^53])\n"; + ReferenceLua.Result r = ReferenceLua.run(script); + assertEquals(0, r.exitCode, r.toString()); + String out = r.output.replaceAll("\r\n", "\n"); + assertEquals("8\n9\ntrue\tnil\n0\t1\n", out); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/CoroutineTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/CoroutineTest.java new file mode 100644 index 0000000..d802a5a --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/CoroutineTest.java @@ -0,0 +1,859 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +/** + * 协程的差分用例 —— 每个脚本都在参考实现与本实现上各跑一遍,逐字节比输出。 + *

+ * 协程是最容易「看起来对、其实差一点」的一块:yield 能穿过哪些边界、 + * resume 的参数怎么变成 yield 的返回值、出错时状态机怎么走,全都是靠这一批 + * 用例钉住的。写用例时有两条硬规则: + *

    + *
  • 每个脚本都要有输出。{@link Diff#assertMatches} 会挡掉空输出, + * 否则一个静默结束的脚本会假绿;
  • + *
  • 协程必须会结束。while true 的脚本会让两边一起挂住, + * 测试永远跑不完。
  • + *
+ */ +class CoroutineTest { + + @TestFactory + List coroutineScriptsMatchReference() { + List tests = new ArrayList<>(); + for (int i = 0; i < SCRIPTS.length; i++) { + String src = SCRIPTS[i]; + int n = i; + tests.add(DynamicTest.dynamicTest("coroutine[" + n + "]", + () -> Diff.assertMatches(src))); + } + return tests; + } + + /** + * 第二批:在元方法里 yield。 + *

+ * 这批用例的价值不在「协程能不能用」,而在把 {@code luaV_finishOp} 的每个 + * 分支都跑到。一条指令执行到一半去调 Lua 代码、结果那里面 yield 了, + * 协程恢复时得先把这条指令剩下的半步补完——比较类要决定跳不跳 JMP, + * 算术类要写回目标寄存器,{@code OP_RETURN} / {@code OP_CLOSE} 要把 pc + * 回退一格重跑。这些分支用普通脚本一条都碰不到,写漏了也不会红, + * 只能靠这里钉住。 + */ + @TestFactory + List yieldInsideMetamethods() { + List tests = new ArrayList<>(); + for (int i = 0; i < DEEP.length; i++) { + String src = DEEP[i]; + int n = i; + tests.add(DynamicTest.dynamicTest("coroutineDeep[" + n + "]", + () -> Diff.assertMatches(src))); + } + return tests; + } + + /** + * 这批脚本不做逐行比对:错误位置前缀里的行号依赖 {@code luaL_where}, + * 而本实现还没有完整的行号表回溯。只比第一行或整体结构。 + */ + @TestFactory + List coroutineFirstLineMatches() { + List tests = new ArrayList<>(); + for (int i = 0; i < FIRST_LINE.length; i++) { + String src = FIRST_LINE[i]; + int n = i; + tests.add(DynamicTest.dynamicTest("coroutineFirstLine[" + n + "]", + () -> Diff.assertFirstLineMatches(src))); + } + return tests; + } + + /** 确认「空用例会被挡掉」这条护栏本身还在 */ + @TestFactory + List vacuousCaseIsRejected() { + String src = "local co = coroutine.create(function() end)\n"; + return List.of(DynamicTest.dynamicTest("vacuous", () -> { + try { + Diff.assertMatches(src); + throw new AssertionError("空输出的用例本应被挡掉"); + } catch (AssertionError e) { + assertEquals(true, e.getMessage().contains("没有产生任何输出")); + } + })); + } + + private static final String[] SCRIPTS = { + + /* ---------- 基本生命周期 ---------- */ + + """ + local co = coroutine.create(function(a, b) + print("start", a, b) + local x = coroutine.yield(a + b) + print("got", x) + return "end", x + end) + print(coroutine.status(co)) + print(coroutine.resume(co, 1, 2)) + print(coroutine.status(co)) + print(coroutine.resume(co, 10)) + print(coroutine.status(co)) + print(coroutine.resume(co)) + print(coroutine.status(co)) + """, + + """ + -- yield 出去多个值、resume 传进多个值 + local co = coroutine.create(function() + print(coroutine.yield(1, 2, 3)) + return 9 + end) + print(coroutine.resume(co)) + print(coroutine.resume(co, "a", "b")) + """, + + """ + -- 不 yield、直接返回 + local co = coroutine.create(function() return 1, 2, 3 end) + print(coroutine.resume(co)) + print(coroutine.resume(co)) + """, + + """ + -- 主协程里 running 返回 nil + print(coroutine.running()) + local co = coroutine.create(function() + print(coroutine.running()) + end) + coroutine.resume(co) + """, + + """ + -- 初始状态是 suspended,不是 dead + local co = coroutine.create(function() end) + print(coroutine.status(co)) + coroutine.resume(co) + print(coroutine.status(co)) + """, + + """ + -- 协程里看主协程的状态 + local main = nil + local co = coroutine.create(function(m) + print(coroutine.status(m)) + print(coroutine.status(coroutine.running())) + end) + coroutine.resume(co, coroutine.running()) + """, + + /* ---------- wrap ---------- */ + + """ + local w = coroutine.wrap(function(a) + local b = coroutine.yield(a * 2) + return b .. "!" + end) + print(w(3)) + print(w("hi")) + """, + + """ + local w = coroutine.wrap(function() return 1, 2 end) + print(w()) + """, + + """ + -- wrap 出来的函数崩了会带位置,且会传播 + local w = coroutine.wrap(function() error("boom") end) + print(pcall(w)) + """, + + """ + local w = coroutine.wrap(function() error({code = 42}) end) + local ok, e = pcall(w) + print(ok, type(e), e.code) + """, + + /* ---------- isyieldable ---------- */ + + """ + print(coroutine.isyieldable()) + local co = coroutine.create(function() + print(coroutine.isyieldable()) + coroutine.yield() + end) + coroutine.resume(co) + print(coroutine.isyieldable(co)) + """, + + /* ---------- yield 能穿过的边界(实测参考实现允许的) ---------- */ + + """ + -- pcall 可以 yield + local co = coroutine.create(function() + local ok, v = pcall(function() return coroutine.yield(1) end) + print("inner", ok, v) + return "done" + end) + print(coroutine.resume(co)) + print(coroutine.resume(co, 42)) + """, + + """ + -- xpcall 也可以 yield + local co = coroutine.create(function() + local ok, v = xpcall(function() return coroutine.yield(1) end, print) + print("inner", ok, v) + return "done" + end) + print(coroutine.resume(co)) + print(coroutine.resume(co, 42)) + """, + + """ + -- 元方法可以 yield + local co = coroutine.create(function() + local t = setmetatable({}, {__index = function(_, k) + return coroutine.yield(k) + end}) + print("v", t.foo) + return "done" + end) + print(coroutine.resume(co)) + print(coroutine.resume(co, "V")) + """, + + """ + -- for 的迭代器可以 yield + local co = coroutine.create(function() + local function iter(s, c) + if c < 3 then return coroutine.yield(c + 1) end + end + for x in iter, nil, 0 do print("x", x) end + return "done" + end) + print(coroutine.resume(co)) + print(coroutine.resume(co, 1)) + print(coroutine.resume(co, 2)) + print(coroutine.resume(co, 3)) + """, + + """ + -- 嵌套协程:在协程里 resume 另一个协程 + local inner = coroutine.create(function() + coroutine.yield("i1") + return "i2" + end) + local outer = coroutine.create(function() + print(coroutine.resume(inner)) + print(coroutine.resume(inner)) + return "o" + end) + print(coroutine.resume(outer)) + """, + + """ + -- 在协程里 yield 出去,外层再 resume + local co = coroutine.create(function() + for i = 1, 3 do coroutine.yield(i) end + return "fin" + end) + for i = 1, 4 do print(coroutine.resume(co)) end + """, + + /* ---------- 不能 yield 的地方 ---------- */ + + """ + print(pcall(function() return coroutine.yield(1) end)) + """, + + """ + local co = coroutine.create(function() + local ok, err = pcall(function() coroutine.yield(1) end) + print("no yield here", ok, err) + return "done" + end) + print(coroutine.resume(co)) + """, + + /* ---------- 错误传播 ---------- */ + + """ + local co = coroutine.create(function() error("boom") end) + print(coroutine.resume(co)) + print(coroutine.status(co)) + print(coroutine.resume(co)) + """, + + """ + local co = coroutine.create(function() + local x = nil + return x.field + end) + print(coroutine.resume(co)) + """, + + """ + -- 错误对象非字符串时,resume 原样搬回来 + local co = coroutine.create(function() error({code = 7}) end) + local ok, e = coroutine.resume(co) + print(ok, type(e), e.code) + """, + + """ + -- 协程里嵌套 pcall:内层 pcall 先吃掉错误 + local co = coroutine.create(function() + print(pcall(function() error("inner") end)) + error("outer") + end) + print(coroutine.resume(co)) + """, + + """ + -- yield 之后才出错:走 precover 的恢复路径 + local co = coroutine.create(function() + local a = coroutine.yield(1) + error("after yield " .. tostring(a)) + end) + print(coroutine.resume(co)) + print(coroutine.resume(co, "Z")) + print(coroutine.status(co)) + """, + + """ + -- pcall 里 yield,恢复后出错 + local co = coroutine.create(function() + local ok, err = pcall(function() + coroutine.yield(1) + error("in pcall") + end) + print("caught", ok, err) + return "done" + end) + print(coroutine.resume(co)) + print(coroutine.resume(co)) + """, + + """ + -- xpcall 的处理函数在 yield 之后仍然生效 + local co = coroutine.create(function() + local ok, err = xpcall(function() + coroutine.yield(1) + error("raw") + end, function(m) return "H:" .. m end) + print("caught", ok, err) + return "done" + end) + print(coroutine.resume(co)) + print(coroutine.resume(co)) + """, + + /* ---------- close ---------- */ + + """ + local co = coroutine.create(function() coroutine.yield(1) end) + coroutine.resume(co) + print(coroutine.close(co)) + print(coroutine.status(co)) + """, + + """ + local co = coroutine.create(function() return 1 end) + coroutine.resume(co) + print(pcall(coroutine.close, co)) + """, + + """ + local co = coroutine.create(function() coroutine.yield() end) + coroutine.resume(co) + -- 已挂起的协程可以关;正在跑的不行 + print(coroutine.close(co)) + """, + + """ + -- 变量在协程被关掉时要跑 __close + local log = {} + local co = coroutine.create(function() + local obj = setmetatable({}, {__close = function() + log[#log + 1] = "closed" + end}) + coroutine.yield(1) + end) + coroutine.resume(co) + print(coroutine.close(co)) + print(#log, log[1]) + """, + + """ + -- 变量在协程出错返回时也要跑 + local log = {} + local co = coroutine.create(function() + local obj = setmetatable({}, {__close = function(_, err) + log[#log + 1] = "closed:" .. tostring(err) + end}) + error("boom") + end) + print(coroutine.resume(co)) + print(#log, log[1]) + """, + + /* ---------- 参数错误 ---------- */ + + """ + print(pcall(coroutine.resume, 1)) + print(pcall(coroutine.status, "x")) + print(pcall(coroutine.create, 42)) + print(pcall(coroutine.wrap, nil)) + """, + + """ + print(pcall(coroutine.close, 1)) + print(pcall(coroutine.isyieldable, 1)) + """, + + /* ---------- 生成器惯用法 ---------- */ + + """ + local function range(n) + return coroutine.wrap(function() + for i = 1, n do coroutine.yield(i) end + end) + end + local sum = 0 + for x in range(5) do sum = sum + x end + print(sum) + """, + + """ + -- 递归协程(用协程实现树的遍历) + local function walk(t) + return coroutine.wrap(function() + for _, v in ipairs(t) do + if type(v) == "table" then + for x in walk(v) do coroutine.yield(x) end + else + coroutine.yield(v) + end + end + end) + end + -- 用不了 table.concat / io.write:这两个属于标准库阶段,还没实现 + local out = "" + for x in walk({1, {2, 3}, {4, {5}}}) do out = out .. tostring(x) .. " " end + print(out) + """, + }; + + /** yield 发生在元方法里、以及 {@code } 里的一批用例 */ + private static final String[] DEEP = { + + """ + -- __add 里 yield:finishOp 的 OP_MMBIN 分支(结果要写回上一条指令的 A) + local mt = {__add = function(a, b) return coroutine.yield("add") end} + local co = coroutine.create(function() + local x = setmetatable({}, mt) + 1 + print("res", x) + return "done" + end) + print(coroutine.resume(co)) + print(coroutine.resume(co, 100)) + """, + + """ + -- __concat 里 yield:finishOp 的 OP_CONCAT 分支 + local mt = {__concat = function(a, b) return coroutine.yield("cat") end} + local co = coroutine.create(function() + print(setmetatable({}, mt) .. "x") + return "done" + end) + print(coroutine.resume(co)) + print(coroutine.resume(co, "C")) + """, + + """ + -- __lt 里 yield:finishOp 要根据结果决定跳不跳紧跟的 JMP + local mt = {__lt = function(a, b) return coroutine.yield("lt") end} + local co = coroutine.create(function() + local a, b = setmetatable({}, mt), setmetatable({}, mt) + if a < b then print("lt") else print("not lt") end + if a < b then print("lt2") else print("not lt2") end + return "done" + end) + print(coroutine.resume(co)) + print(coroutine.resume(co, true)) + print(coroutine.resume(co, false)) + """, + + """ + -- __le 用 __lt 顶替时结果要取反 + local mt = {__lt = function(a, b) return coroutine.yield("v") end} + local co = coroutine.create(function() + local a, b = setmetatable({}, mt), setmetatable({}, mt) + print(a <= b) + print(a <= b) + return "done" + end) + print(coroutine.resume(co)) + print(coroutine.resume(co, true)) + print(coroutine.resume(co, false)) + """, + + """ + -- __eq 里 yield + local mt = {__eq = function(a, b) return coroutine.yield("eq") end} + local co = coroutine.create(function() + local a, b = setmetatable({}, mt), setmetatable({}, mt) + print(a == b) + return "done" + end) + print(coroutine.resume(co)) + print(coroutine.resume(co, true)) + """, + + """ + -- __index 里 yield:finishOp 的 OP_GETFIELD / OP_GETTABLE 分支 + local mt = {__index = function(t, k) return coroutine.yield(k) end} + local co = coroutine.create(function() + local t = setmetatable({}, mt) + print(t.foo) + print(t["bar"]) + return "done" + end) + print(coroutine.resume(co)) + print(coroutine.resume(co, "V1")) + print(coroutine.resume(co, "V2")) + """, + + """ + -- __newindex 里 yield + local log = {} + local mt = {__newindex = function(t, k, v) log[#log + 1] = coroutine.yield(k) end} + local co = coroutine.create(function() + local t = setmetatable({}, mt) + t.a = 1 + t.b = 2 + return "done" + end) + print(coroutine.resume(co)) + print(coroutine.resume(co, "X")) + print(coroutine.resume(co, "Y")) + print(#log, log[1], log[2]) + """, + + """ + -- __call 里 yield + local mt = {__call = function(self, x) return coroutine.yield(x) end} + local co = coroutine.create(function() + local t = setmetatable({}, mt) + print(t(5)) + return "done" + end) + print(coroutine.resume(co)) + print(coroutine.resume(co, "called")) + """, + + """ + -- __len / __unm 里 yield + local mt = {__len = function() return coroutine.yield("len") end, + __unm = function() return coroutine.yield("unm") end} + local co = coroutine.create(function() + local t = setmetatable({}, mt) + print(#t) + print(-t) + return "done" + end) + print(coroutine.resume(co)) + print(coroutine.resume(co, 7)) + print(coroutine.resume(co, -7)) + """, + + """ + -- 的 __close 里 yield:finishOp 的 OP_RETURN 分支(pc 要回退) + local log = {} + local co = coroutine.create(function() + local obj = setmetatable({}, {__close = function() + log[#log + 1] = coroutine.yield("closing") + end}) + return "body" + end) + print(coroutine.resume(co)) + print(coroutine.resume(co, "A")) + print(#log, log[1]) + """, + + """ + -- 的 __close 里连续 yield 多次 + local n = 0 + local co = coroutine.create(function() + local a = setmetatable({}, {__close = function() n = n + 1 end}) + local b = setmetatable({}, {__close = function() coroutine.yield("b") end}) + return "body" + end) + print(coroutine.resume(co)) + print(coroutine.resume(co)) + print("n", n) + """, + + """ + -- 作用域结束时的 OP_CLOSE 里 yield + local co = coroutine.create(function() + do + local obj = setmetatable({}, {__close = function() + coroutine.yield("scope") + end}) + end + print("after scope") + return "done" + end) + print(coroutine.resume(co)) + print(coroutine.resume(co)) + """, + + """ + -- 元方法里 yield,外面还套着 pcall + local mt = {__index = function(_, k) return coroutine.yield(k) end} + local co = coroutine.create(function() + local t = setmetatable({}, mt) + print(pcall(function() return t.x end)) + return "done" + end) + print(coroutine.resume(co)) + print(coroutine.resume(co, "V")) + """, + + """ + -- 元方法里 yield,恢复后元方法自己出错 + local mt = {__index = function(_, k) + local v = coroutine.yield(k) + error("mt failed") + end} + local co = coroutine.create(function() + local t = setmetatable({}, mt) + print(pcall(function() return t.x end)) + return "done" + end) + print(coroutine.resume(co)) + print(coroutine.resume(co, "V")) + """, + + """ + -- resume 传的值比 yield 出去的多 / 少 + local co = coroutine.create(function() + local a, b, c = coroutine.yield(1) + print(a, b, c) + return "done" + end) + print(coroutine.resume(co)) + print(coroutine.resume(co, "x")) + """, + + """ + local co = coroutine.create(function() + print(coroutine.yield()) + return "done" + end) + print(coroutine.resume(co)) + print(coroutine.resume(co, 1, 2, 3)) + """, + + """ + -- 协程里再起协程并 yield 进去 + local co = coroutine.create(function() + local inner = coroutine.create(function() return coroutine.yield("i") end) + print(coroutine.resume(inner)) + print(coroutine.status(inner)) + print(coroutine.resume(inner, "R")) + print(coroutine.status(inner)) + return "done" + end) + print(coroutine.resume(co)) + """, + + """ + -- __tostring 是被 print 用 C 调进去的,属于「不可 yield」的边界 + local co = coroutine.create(function() + print(setmetatable({}, {__tostring = function() + return coroutine.yield("t") + end})) + return "done" + end) + print(coroutine.resume(co)) + print(coroutine.status(co)) + """, + + """ + -- 协程里 resume 自己 + local co + co = coroutine.create(function() + print(coroutine.status(co)) + print(coroutine.resume(co)) + end) + print(coroutine.resume(co)) + print(coroutine.status(co)) + """, + + """ + -- 协程里 close 自己 + local co + co = coroutine.create(function() + print(pcall(coroutine.close, co)) + return "done" + end) + print(coroutine.resume(co)) + """, + + """ + -- wrap 出来的函数在协程死后再次调用 + local w = coroutine.wrap(function() return 1 end) + print(w()) + print(pcall(w)) + """, + + """ + -- 一个 wrap 内的递归 resume(用协程实现的一对一传递) + local function passthrough(f) + return coroutine.wrap(function(...) + local args = table.pack(...) + while true do + args = table.pack(coroutine.yield(f(table.unpack(args, 1, args.n)))) + end + end) + end + local ok = pcall(passthrough, function() end) + print(ok) + """, + }; + + /** + * 第三批:出错之后在 {@code __close} 里 yield。 + *

+ * 这一批钉的是 {@code ldo.c: finishpcallk} 那条恢复路径。C 版里 + * {@code pcall} 在可 yield 的线程上不建本地恢复点:错误一路冒到 + * {@code lua_resume},由 {@code precover} → {@code finishpcallk} 用 + * {@code luaF_close(..., yy = 1)} 收尾,于是每个 {@code __close} 都能 + * yield,而且 {@code __close} 自己抛的新错误要顶掉原来的、接着传给 + * 下一个还没关的变量。 + *

+ * 本实现用 Java 异常代替长跳转,恢复点落在 {@code pcall} 的 {@code catch} + * 里,所以「让不让 {@code __close} yield」得显式传下去( + * {@link LuaState#pcall(int, int, LuaValue)} 按 {@code yieldable()} 决定); + * yield 一旦打断收尾,错误对象又不在栈上,只能靠 + * {@link LuaState#closeErrPending} 暂存到下一次 {@code finishPcallK}。 + * 这三样缺一样,这段脚本就会在第一个 {@code __close} 上报 + * 「attempt to yield across a C-call boundary」。 + */ + @TestFactory + List yieldInsideClosingMetamethods() { + List tests = new ArrayList<>(); + for (int i = 0; i < CLOSE_YIELD.length; i++) { + String src = CLOSE_YIELD[i]; + int n = i; + tests.add(DynamicTest.dynamicTest("closeYield[" + n + "]", + () -> Diff.assertMatches(src))); + } + return tests; + } + + /** 出错解绕时 {@code __close} 里的 yield(见 {@link #yieldInsideClosingMetamethods}) */ + private static final String[] CLOSE_YIELD = { + + /* + * 函数体里 error(10):x / y / z 依次 yield 出去,y 在 yield 之后 + * 再 error(err + 20),于是 z 看到的错误对象和 pcall 最终拿到的 + * 都是 30 —— 不是 10。 + */ + """ + local function func2close (f) + return setmetatable({}, {__close = f}) + end + + local co = coroutine.wrap(function () + local function foo (err) + local z = func2close(function(_, msg) + print("z", msg) + coroutine.yield("z") + return 100, 200 + end) + local y = func2close(function(_, msg) + print("y", msg) + coroutine.yield("y") + if err then error(err + 20) end + end) + local x = func2close(function(_, msg) + print("x", msg) + coroutine.yield("x") + return 100, 200 + end) + if err == 10 then error(err) else return 10, 20 end + end + coroutine.yield(pcall(foo, nil)) + coroutine.yield(pcall(foo, 1)) + return pcall(foo, 10) + end) + + for i = 1, 12 do + local a, b, c = co() + print(i, a, b, c) + end + """, + + /* 只有一层 ,yield 之后 pcall 仍要返回 false + 原错误 */ + """ + local function func2close (f) + return setmetatable({}, {__close = f}) + end + + local co = coroutine.wrap(function () + local function foo () + local x = func2close(function(_, msg) + print("x", msg) + coroutine.yield("x") + end) + error("boom") + end + return pcall(foo) + end) + + print(co()) + print(co()) + """, + + /* 主线上(不可 yield)出错时,__close 里的 yield 必须报错 */ + """ + local function func2close (f) + return setmetatable({}, {__close = f}) + end + + local function foo () + local x = func2close(function(_, msg) + print("x", msg) + local ok, e = pcall(coroutine.yield, "x") + print("yield?", ok, e) + end) + error("boom") + end + + print(pcall(foo)) + """, + }; + + /** 只比第一行的用例(位置前缀细节尚未对齐) */ + private static final String[] FIRST_LINE = { + + """ + local w = coroutine.wrap(function() error("boom") end) + w() + """, + + """ + local co = coroutine.create(function() error("boom") end) + local ok, e = coroutine.resume(co) + print(ok) + """, + }; +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/DebugGridTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/DebugGridTest.java new file mode 100644 index 0000000..32e4af0 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/DebugGridTest.java @@ -0,0 +1,752 @@ +package com.github.esrrhs.fakescript.lua; + +import org.junit.jupiter.api.Test; + +import org.junit.jupiter.api.BeforeAll; + +import java.io.File; + +/** + * {@code debug} 库的全网格差分 —— 阶段 12。 + *

+ * 为什么单开一类:{@code DebugTest} 只有 39 个用例,而 {@code debug} 是本项目里 + * 唯一「往回看自己的栈」的模块——栈形状、帧种类、钩子时机 + * 三条线在这里交汇(约定 #28/#31/#32)。手挑用例盖不住 + * 「帧种类 × 层数 × 选项字母」这个三维。 + *

+ * 三条用例: + *

    + *
  • 主网格(559 行,16 段): + *
      + *
    • {@code A}–{@code E}:{@code getinfo} 的 18 种选项(含 {@code nil} + * 取默认值、{@code >} 与 {@code x} 两种非法选项)× 九个层号 × 六种 + * 帧(主 chunk / 一层 Lua / 三层嵌套 / C 函数回调 {@code gsub}· + * {@code sort}·{@code pcall} / 尾调用 / 方法调用 / {@code __index}· + * {@code __add} / {@code for} 迭代器);
    • + *
    • {@code F}:协程里看自己、以及把 {@code co} 当第一个参数看别人;
    • + *
    • {@code G}–{@code H}:{@code getinfo(函数)} 七种函数,以及错误参数;
    • + *
    • {@code I}–{@code J}:{@code getlocal}/{@code setlocal} 与 + * {@code getupvalue}/{@code setupvalue} 的下标全扫(-3…6 与 + * -2…5),含 vararg、数值 {@code for}、泛型 {@code for}、 + * {@code (C temporary)}、写临时寄存器;
    • + *
    • {@code K}:{@code traceback} 的九种消息 × 五个 level, + * 回溯本身逐字节比;
    • + *
    • {@code L}:{@code getmetatable}/{@code setmetatable}/ + * {@code getuservalue}/{@code setuservalue}/{@code getregistry} + * 在九种值上;
    • + *
    • {@code M}:钩子——12 种 mask × count,钩子里读 {@code getinfo(2)/(3)}, + * {@code gethook} 三元组,钩子里 yield;
    • + *
    • {@code N}:协程第二遍;
    • + *
    • {@code O}:{@code chunkid} 边界——12 种 chunkname(含 {@code \0}、 + * {@code 0xFF}、58/60/100 字符)在 {@code source}/{@code short_src}/ + * 位置前缀/回溯四个出口上(约定 #84);
    • + *
    • {@code P}:{@code }/{@code } 的局部名、 + * 长函数的 {@code activelines}。
    • + *
    + *
  • + *
  • 行钩子里抛错与调用钩子里抛错各一条:主线程上这是 + * 致命的(约定 #31——{@code pcall} 接不住,脚本当场终止), + * 所以各自单独成条;塞进主网格里会把后面几百行全吃掉。
  • + *
+ *

+ * 三条都走 {@link Diff#assertFullMatches}:回溯属于预期输出 + * ({@code debug.traceback} 的返回值、钩子崩掉后的回溯), + * {@code assertMatches} 会从第一个 {@code stack traceback:} 处把后面全砍掉。 + *

+ * 两个必须在写同类探针时记住的坑: + *

    + *
  • {@code nil} 不能直接放进 {@code ipairs} 的表里——首元素是 + * {@code nil} 时 {@code ipairs} 就地停住,整段静默不跑。用 {@code "NIL"} + * 哨兵 + {@code if w == "NIL" then return nil end};写成 + * {@code (w == "NIL") and nil or w} 也不行,Lua 的 {@code and/or} 习语 + * 在中间值为 {@code nil} 时会返回 {@code w}。
  • + *
  • {@code debug.setmetatable} 用完必须还原成原值(约定 #80)。 + * 这里的第一版还成 {@code nil},于是字符串的类型元表被摘掉,后面所有 + * {@code ("x"):gsub(...)} 全部 {@code attempt to index a string value} + * ——探针自己把自己崩了。
  • + *
+ *

+ * 这一片一条分歧都没咬出来——铺之前担心、铺完确认干净:{@code debug} + * 这一块的栈形状与 C 版是一致的。 + */ +class DebugGridTest { + + @BeforeAll + static void prepare() { + new File("target/rt").mkdirs(); + } + + /** 主网格:16 段 559 行。 */ + @Test + void debugLibraryGridMatchesReference() { + Diff.assertFullMatches(""" +-- debug 库全网格:getinfo / getlocal / upvalue / traceback / 钩子 / 元表 +-- 起因:DebugTest 只有 39 个用例,而 debug 是本项目里唯一「往回看自己的栈」 +-- 的模块——栈形状、帧种类、钩子时机三条都在这里交汇(约定 #28/#31/#32)。 +-- 手挑用例盖不住「帧种类 × 层数 × 选项字母」这个三维。 +local HEX = "0123456789ABCDEF" +local function hb(b) return HEX:sub((b // 16) + 1, (b // 16) + 1) .. HEX:sub((b % 16) + 1, (b % 16) + 1) end +local function esc(s) + return (s:gsub(".", function(c) + local b = c:byte() + if b == 91 or b == 93 then return "<" .. hb(b) .. ">" end + if b < 32 or b > 126 then return "<" .. hb(b) .. ">" end + return c + end)) +end +local function norm(s) return (s:gsub("0x%x+", "PTR")) end +local function show(v) + local t = type(v) + if t == "string" then + local s = norm(v) + return "#" .. #s .. "[" .. esc(s) .. "]" + elseif t == "number" then + if math.type(v) == "integer" then return "i" .. tostring(v) end + return "f" .. tostring(v) + elseif t == "nil" or t == "boolean" then return tostring(v) end + if t == "table" then + -- 约定 #43:键序不可比,一律排序后再打 + local nk, sk = {}, {} + for k in next, v do + if type(k) == "number" then nk[#nk + 1] = k else sk[#sk + 1] = tostring(k) end + end + table.sort(nk) + table.sort(sk) + local out = {} + for _, k in ipairs(nk) do out[#out + 1] = k .. "=" .. show(v[k]) end + for _, k in ipairs(sk) do out[#out + 1] = k .. "=" .. show(v[k]) end + return "{" .. table.concat(out, " ") .. "}" + end + local ok, s = pcall(tostring, v) + if not ok or type(s) ~= "string" then return t .. "()" end + return t .. "(" .. norm(s) .. ")" +end +local function P(tag, fn) + local r = table.pack(pcall(fn)) + local out = tag + for i = 1, r.n do out = out .. " | " .. show(r[i]) end + print(out .. " | n=" .. r.n) +end + +-- debug.getinfo 的字段按固定顺序打(约定 #43:不能 pairs) +local FIELDS = { "source", "short_src", "linedefined", "lastlinedefined", "what", + "currentline", "name", "namewhat", "istailcall", "nups", + "nparams", "isvararg", "ftransfer", "ntransfer", + "activelines", "func" } +local function gi(a, b) + local ok, info = pcall(debug.getinfo, a, b) + if not ok then return "ERR:" .. show(info) end + if info == nil then return "nil" end + local out = {} + for _, k in ipairs(FIELDS) do + if info[k] ~= nil then out[#out + 1] = k .. "=" .. show(info[k]) end + end + return table.concat(out, " ") +end + +local WHATS = { "NIL", "", "S", "l", "u", "n", "t", "r", "L", "f", + "flnSrtu", "nSl", "uL", "ft", "x", "Sx", ">", "Sl>" } +-- "NIL" 是哨兵:选项参数要传真 nil,而表里放真 nil 会让 ipairs 就地停住 +local function opt(w) + -- 不能写 (w == "NIL") and nil or w:and/or 习语在中间值为 nil 时会返回 w + if w == "NIL" then return nil end + return w +end +local LEVELS = { 0, 1, 2, 3, 4, 5, 6, 7, 100 } + +-- 把「各层」铺开:从当前位置往上数九层 +local function levels(what) + local out = {} + for _, lv in ipairs(LEVELS) do out[#out + 1] = lv .. ":" .. gi(lv, what) end + return table.concat(out, " || ") +end + +print("=== A 各层 × 各选项:主 chunk ===") +for _, w in ipairs(WHATS) do + P("A " .. tostring(w), function() return levels(opt(w)) end) +end + +print("=== B 各层 × 各选项:一层 Lua 函数里 ===") +local function inFn1(w) return levels(opt(w)) end +for _, w in ipairs(WHATS) do + P("B " .. tostring(w), function() return inFn1(w) end) +end + +print("=== C 三层嵌套 Lua 函数 ===") +local function inFn3(w) return levels(opt(w)) end +local function inFn2(w) return inFn3(w) end +local function inFn1b(w) return inFn2(w) end +for _, w in ipairs(WHATS) do + P("C " .. tostring(w), function() return inFn1b(w) end) +end + +print("=== D 从 C 函数的回调里(gsub / sort / gmatch)===") +for _, w in ipairs(WHATS) do + P("D gsub " .. tostring(w), function() + return ("abc"):gsub("b", function() return levels(opt(w)) end) + end) + P("D sort " .. tostring(w), function() + local r + table.sort({ 2, 1 }, function(x, y) r = levels(w) return x < y end) + return r + end) + P("D pcall " .. tostring(w), function() + return pcall(function() return levels(opt(w)) end) + end) +end + +print("=== E 尾调用位置 / 方法调用 / 元方法 / for 迭代器 ===") +local function tailTarget(w) return levels(opt(w)) end +for _, w in ipairs(WHATS) do + P("E tail " .. tostring(w), function() return (function() return tailTarget(w) end)() end) + P("E method " .. tostring(w), function() + local obj = setmetatable({}, { __index = { m = function(self) return levels(opt(w)) end } }) + return obj:m() + end) + P("E meta " .. tostring(w), function() + local t = setmetatable({}, { __index = function() return levels(opt(w)) end }) + return t.x + end) + P("E metaadd " .. tostring(w), function() + local t = setmetatable({}, { __add = function() return levels(opt(w)) end }) + return t + 1 + end) + P("E iter " .. tostring(w), function() + local it = function(s, c) + if c < 1 then return levels(opt(w)) end + return nil + end + for v in it, nil, 0 do return v end + end) +end + +print("=== F 协程里:主线程 / 自身 / 互看 ===") +for _, w in ipairs(WHATS) do + P("F co " .. tostring(w), function() + local co = coroutine.create(function() return levels(opt(w)) end) + return select(2, coroutine.resume(co)) + end) + P("F wrap " .. tostring(w), function() + local w2 = coroutine.wrap(function() return levels(opt(w)) end) + return w2() + end) +end +-- 协程作为第一个参数:看它自己的栈 +local COGRID = {} +for _, w in ipairs({ "NIL", "", "S", "l", "n", "u", "flnSrtu" }) do + P("F coarg " .. tostring(w), function() + local co = coroutine.create(function() coroutine.yield(1) return "done" end) + coroutine.resume(co) + local out = {} + for lv = 0, 3 do out[#out + 1] = lv .. ":" .. gi(co, lv, opt(w)) end + return table.concat(out, " || ") + end) +end + +print("=== G getinfo(函数) ===") +local function plain(a, b) return a + b end +local function vararg(a, ...) return a, select("#", ...) end +local up1 = 7 +local function withUps() return up1 end +local FNCASES = { + { "lua", plain }, { "vararg", vararg }, { "ups", withUps }, + { "print", print }, { "pcall", pcall }, { "methods", string.format }, + { "mainchunk", nil }, +} +for i = 1, #FNCASES do + local name, f = FNCASES[i][1], FNCASES[i][2] + if name == "mainchunk" then + for _, w in ipairs({ "NIL", "", "S", "l", "u", "n", "flnSrtu", "L" }) do + P("G main " .. tostring(w), function() return gi(debug.getinfo(1, "f").func, opt(w)) end) + end + else + for _, w in ipairs({ "NIL", "", "S", "l", "u", "n", "flnSrtu", "L", "f" }) do + P("G " .. name .. " " .. tostring(w), function() return gi(f, opt(w)) end) + end + end +end + +print("=== H getinfo 的错误参数 ===") +local BAD = { "nil", "true", "42", "1.5", "'x'", "{}", "print", "1.0", "-1", "0" } +for i = 1, #BAD do + local a = BAD[i] + P("H a1 " .. a, function() return debug.getinfo(load("return " .. a)()) end) + P("H a2 " .. a, function() return debug.getinfo(1, load("return " .. a)()) end) + P("H f2 " .. a, function() return debug.getinfo(print, load("return " .. a)()) end) +end +P("H n0", function() return debug.getinfo() end) +P("H n3", function() return debug.getinfo(1, "S", 3) end) +P("H neg", function() return debug.getinfo(-1) end) + +print("=== I getlocal / setlocal ===") +local function localGrid(tag) + local a1, a2 = 10, 20 + local function inner(...) + local b1 = 1 + local out = {} + for i = -3, 6 do + local ok, n, v = pcall(debug.getlocal, 1, i) + out[#out + 1] = i .. "=" .. (ok and (tostring(n) .. "/" .. show(v)) or show(n)) + end + for i = -3, 6 do + local ok, n, v = pcall(debug.getlocal, 2, i) + out[#out + 1] = "L2." .. i .. "=" .. (ok and (tostring(n) .. "/" .. show(v)) or show(n)) + end + return table.concat(out, " ") + end + return inner(1, 2, 3) +end +P("I locals", function() return localGrid("x") end) +P("I vararg", function() + local function f(...) + local out = {} + for i = -3, 4 do + local ok, n, v = pcall(debug.getlocal, 1, i) + out[#out + 1] = i .. "=" .. (ok and (tostring(n) .. "/" .. show(v)) or show(n)) + end + return table.concat(out, " ") + end + return f("a", "b") +end) +P("I fornum", function() + local out = {} + for i = 1, 2 do + for k = -3, 6 do + local ok, n, v = pcall(debug.getlocal, 1, k) + out[#out + 1] = k .. "=" .. (ok and (tostring(n) .. "/" .. show(v)) or show(n)) + end + break + end + return table.concat(out, " ") +end) +P("I forgen", function() + local out = {} + for _, v in ipairs({ 1 }) do + for k = -3, 6 do + local ok, n, v2 = pcall(debug.getlocal, 1, k) + out[#out + 1] = k .. "=" .. (ok and (tostring(n) .. "/" .. show(v2)) or show(n)) + end + end + return table.concat(out, " ") +end) +P("I setlocal", function() + local function f() + local x = 1 + local ok = debug.setlocal(1, 1, 99) + return tostring(ok) .. "/" .. tostring(x) + end + return f() +end) +P("I setlocal bad", function() + local function f() + local x = 1 + local out = {} + for i = -3, 6 do + local ok, e = pcall(debug.setlocal, 1, i, 99) + out[#out + 1] = i .. "=" .. (ok and tostring(e) or show(e)) + end + return table.concat(out, " ") + end + return f() +end) +P("I setlocal tmp", function() + -- 临时寄存器不可写 + local function f() + local out = {} + for i = 1, 4 do + local ok, e = pcall(debug.setlocal, 2, i, 99) + out[#out + 1] = i .. "=" .. (ok and tostring(e) or show(e)) + end + return table.concat(out, " ") + end + return f() +end) +P("I co", function() + local co = coroutine.create(function() + local x = 5 + coroutine.yield() + end) + coroutine.resume(co) + local out = {} + for i = 0, 4 do + local ok, n, v = pcall(debug.getlocal, co, 1, i) + out[#out + 1] = i .. "=" .. (ok and (tostring(n) .. "/" .. show(v)) or show(n)) + end + return table.concat(out, " ") +end) +P("I args bad", function() + local out = {} + for _, a in ipairs({ "nil", "true", "'x'", "{}", "1.5", "0" }) do + local ok, e = pcall(debug.getlocal, 1, load("return " .. a)()) + out[#out + 1] = a .. "=" .. (ok and tostring(e) or show(e)) + end + return table.concat(out, " ") +end) + +print("=== J getupvalue / setupvalue ===") +local uv1, uv2, uv3 = 1, 2, 3 +local function mkUps() + local a = uv1 + local b = uv2 + local c = uv3 + return function() return a + b + c end +end +local F = mkUps() +for i = -2, 5 do + P("J get " .. i, function() + local ok, n, v = pcall(debug.getupvalue, F, i) + if not ok then return show(n) end + return tostring(n) .. "/" .. show(v) + end) +end +P("J setup", function() + local ok, n = pcall(debug.setupvalue, F, 1, 100) + return tostring(ok) .. "/" .. tostring(n) .. "/" .. tostring(F()) +end) +P("J setup bad", function() + local out = {} + for i = -2, 5 do + local ok, e = pcall(debug.setupvalue, F, i, 7) + out[#out + 1] = i .. "=" .. (ok and tostring(e) or show(e)) + end + return table.concat(out, " ") +end) +P("J cups", function() + local out = {} + for i = -1, 3 do + local ok, n, v = pcall(debug.getupvalue, print, i) + out[#out + 1] = i .. "=" .. (ok and (tostring(n) .. "/" .. show(v)) or show(n)) + end + return table.concat(out, " ") +end) +P("J nups0", function() + local g = load("return 1") + local out = {} + for i = -1, 2 do + local ok, n, v = pcall(debug.getupvalue, g, i) + out[#out + 1] = i .. "=" .. (ok and (tostring(n) .. "/" .. show(v)) or show(n)) + end + return table.concat(out, " ") +end) +P("J _ENV", function() + local out = {} + for i = 1, 2 do + local ok, n, v = pcall(debug.getupvalue, mkUps, i) + out[#out + 1] = i .. "=" .. (ok and (tostring(n) .. "/" .. show(v)) or show(n)) + end + return table.concat(out, " ") +end) +for _, a in ipairs({ "nil", "true", "42", "1.5", "'x'", "{}" }) do + P("J bad " .. a, function() + return pcall(debug.getupvalue, load("return " .. a)(), 1) + end) + P("J bad2 " .. a, function() + return pcall(debug.getupvalue, print, load("return " .. a)()) + end) +end + +print("=== K traceback ===") +local TBMSG = { "nil", "'msg'", "''", "42", "1.5", "true", "{}", + "setmetatable({}, {__tostring = function() return 'TS' end})", + "setmetatable({}, {__tostring = function() return {} end})" } +for i = 1, #TBMSG do + local m = TBMSG[i] + for _, lv in ipairs({ 0, 1, 2, 3, 99 }) do + P("K " .. i .. " lvl" .. tostring(lv), function() + local msg = load("return " .. m)() + -- 回溯本身逐字节比:这里只取第一行之前的部分,行号一致即可比 + local s = debug.traceback(msg, lv) + return (s:gsub("0x%x+", "PTR")) + end) + end +end +P("K co", function() + local co = coroutine.create(function() coroutine.yield() end) + coroutine.resume(co) + return debug.traceback(co, "m", 1) +end) +P("K co0", function() + local co = coroutine.create(function() coroutine.yield() end) + return debug.traceback(co) +end) +P("K bad", function() return pcall(debug.traceback, print) end) + +print("=== L 元表 / registry / uservalue ===") +local TYPES = { "nil", "true", "42", "1.5", "'x'", "{}", "print", + "coroutine.create(function() end)", "io.stdout" } +for i = 1, #TYPES do + local a = TYPES[i] + P("L getmt " .. a, function() return debug.getmetatable(load("return " .. a)()) end) + P("L setmt " .. a, function() + local v = load("return " .. a)() + local orig = debug.getmetatable(v) -- 约定 #80:还原成原值,还成 nil 会炸 + local ok, e = pcall(debug.setmetatable, v, { __index = { z = 1 } }) + local back = tostring(ok) .. "/" .. tostring(e) + pcall(debug.setmetatable, v, orig) + return back + end) + P("L uv " .. a, function() + local v = load("return " .. a)() + local ok, e = pcall(debug.getuservalue, v) + return tostring(ok) .. "/" .. show(e) + end) + P("L suv " .. a, function() + local v = load("return " .. a)() + local ok, e = pcall(debug.setuservalue, v, 42) + return tostring(ok) .. "/" .. show(e) + end) +end +P("L setmt0", function() return pcall(debug.setmetatable, 1) end) +P("L setmtnil", function() + local ok = debug.setmetatable(42, nil) + return tostring(ok) +end) +P("L registry", function() + local r = debug.getregistry() + return type(r) .. "/" .. tostring(r ~= nil) +end) + +print("=== M 钩子 ===") +local MASKS = { "", "c", "r", "l", "cr", "cl", "rl", "crl", "lcr", "x", "L", "C" } +for i = 1, #MASKS do + local m = MASKS[i] + P("M " .. m .. " c0", function() + debug.sethook(function(...) end, m) + local r = pcall(function() return 1 + 1 end) + debug.sethook() + return r + end) + P("M " .. m .. " c1", function() + local n = 0 + debug.sethook(function(...) n = n + 1 end, m, 1) + local r = pcall(function() return ("x"):upper() end) + debug.sethook() + return r, n + end) +end +-- 钩子里读 getinfo:约定 #31,取第 3 层 +P("M gi2", function() + local out = {} + debug.sethook(function(what) + out[#out + 1] = what .. "/" .. tostring(debug.getinfo(2, "Sln").currentline) + end, "l") + local r = pcall(function() local a = 1 local b = 2 return a + b end) + debug.sethook() + return r, table.concat(out, ",") +end) +P("M gi3", function() + local out = {} + debug.sethook(function(what) + local i = debug.getinfo(3, "n") + out[#out + 1] = what .. "/" .. tostring(i and i.name) + end, "l") + local r = pcall(function() local a = 1 return a end) + debug.sethook() + return r, table.concat(out, ",") +end) +P("M gethook", function() + local f = function() end + debug.sethook(f, "l", 7) + local r1 = table.pack(debug.gethook()) + debug.sethook() + -- 没有钩子时 debug.gethook() 只返回一个 nil,不是三个值 + local r2 = table.pack(debug.gethook()) + local function fmt(r) + local out = {} + for i = 1, r.n do out[#out + 1] = tostring(r[i]) end + return "n=" .. r.n .. "[" .. table.concat(out, ",") .. "]" + end + return tostring(r1[1] == f) .. " " .. fmt(r1) .. " " .. fmt(r2) +end) +-- 主线程上「钩子里抛错」是致命的(约定 #31:脚本直接崩,pcall 接不住), +-- 所以放进协程里跑——resume 能把它收成 false, msg,脚本继续往下走。 +-- 裸的主线程那份单独成一条用例,见 DebugGridTest 的第二个方法。 +P("M errco", function() + local co = coroutine.create(function() + debug.sethook(function() error("hook") end, "l") + local r = pcall(function() local a = 1 return a end) + debug.sethook() + return r + end) + local out = {} + for i = 1, 3 do + local r = table.pack(coroutine.resume(co)) + out[#out + 1] = tostring(r[1]) .. "/" .. show(r[2]) + end + return table.concat(out, " ") .. " " .. coroutine.status(co) +end) +P("M errcco", function() + local co = coroutine.create(function() + debug.sethook(function() error("hook") end, "c") + local r = pcall(function() return ("x"):upper() end) + debug.sethook() + return r + end) + local out = {} + for i = 1, 3 do + local r = table.pack(coroutine.resume(co)) + out[#out + 1] = tostring(r[1]) .. "/" .. show(r[2]) + end + return table.concat(out, " ") .. " " .. coroutine.status(co) +end) +P("M errco2", function() + local co = coroutine.create(function() + debug.sethook(function() error(setmetatable({}, {__tostring = function() return "TS" end})) end, "l") + local r = pcall(function() local a = 1 return a end) + debug.sethook() + return r + end) + local r = table.pack(coroutine.resume(co)) + return tostring(r[1]) .. "/" .. show(r[2]) +end) +P("M yield", function() + local co = coroutine.create(function() + debug.sethook(function() coroutine.yield("Y") end, "l") + local a = 1 + debug.sethook() + return "done" + end) + local out = {} + for i = 1, 4 do + local r = table.pack(coroutine.resume(co)) + out[#out + 1] = table.concat({ tostring(r[1]), tostring(r[2]) }, "/") + end + return table.concat(out, " ") +end) +-- 事件名里的 "tail call" / "tail return" 只有尾调用才出得来 +P("M tail", function() + local out = {} + debug.sethook(function(what) out[#out + 1] = what end, "c") + local function g() return 1 end + local function f() return g() end + local r = f() + debug.sethook() + return tostring(r) .. " | " .. table.concat(out, ",") +end) +P("M allret", function() + local out = {} + debug.sethook(function(what, line) out[#out + 1] = what .. ":" .. tostring(line) end, "crl") + local r = (function() return ("x"):upper() end)() + debug.sethook() + return tostring(r) .. " | " .. table.concat(out, ",") +end) +P("M args bad", function() + local out = {} + for _, a in ipairs({ "nil", "42", "'x'", "{}" }) do + local ok, e = pcall(debug.sethook, load("return " .. a)()) + out[#out + 1] = a .. "=" .. (ok and "ok" or show(e)) + debug.sethook() + end + return table.concat(out, " ") +end) + +print("=== N 协程第二遍:整片重跑 ===") +local function body() + local out = {} + for _, w in ipairs({ "NIL", "", "S", "l", "n", "u", "flnSrtu", "L" }) do + out[#out + 1] = gi(1, opt(w)) + out[#out + 1] = gi(2, opt(w)) + end + out[#out + 1] = norm(debug.traceback("m", 1)) + local ok1, n1, v1 = debug.getlocal(1, 1) + out[#out + 1] = tostring(ok1) .. "/" .. tostring(n1) .. "/" .. show(v1) + local f = function() return 1 end + local ok2, n2, v2 = debug.getupvalue(f, 1) + out[#out + 1] = tostring(ok2) .. "/" .. tostring(n2) .. "/" .. show(v2) + out[#out + 1] = show(debug.getmetatable(1)) + out[#out + 1] = tostring(debug.getregistry() ~= nil) + return table.concat(out, ";") +end +P("N main", function() return body() end) +P("N co", function() + local c = coroutine.create(function() return body() end) + return select(2, coroutine.resume(c)) +end) +P("N wrap", function() + local w = coroutine.wrap(function() return body() end) + return w() +end) + +print("=== O chunkid 边界:source / short_src / 位置前缀 ===") +-- 约定 #84:出口是 %s,在第一个 \\\\0 处砍断;#85:高位字节原样留着。 +-- chunkid 自己还有一层「超长截成 ...」的语义,一起钉住。 +local NAMES = { "", "@f.lua", "=lit", "[C]", "a\\0b", "\\255\\254", + string.rep("x", 58), string.rep("x", 60), string.rep("x", 100), + "@" .. string.rep("y", 70), "=a\\0b", "\\0abc" } +for i = 1, #NAMES do + local nm = NAMES[i] + P("O info " .. i, function() + return gi(load("return 1", nm), "S") + end) + P("O err " .. i, function() + return load("error('x')", nm)() + end) + P("O lvl " .. i, function() + return load("return debug.getinfo(1, 'Sl')", nm)() + end) + P("O tb " .. i, function() + return load("return debug.traceback('m')", nm)() + end) +end + +print("=== P / / 长函数的 activelines ===") +P("P close", function() + return load([[ + local mt = {__close = function() end} + local a = setmetatable({}, mt) + local b = 2 + local c = 3 + local out = {} + for i = -2, 6 do + local ok, n, v = pcall(debug.getlocal, 1, i) + out[#out + 1] = i .. "=" .. (ok and (tostring(n) .. "/" .. type(v)) or tostring(n)) + end + return table.concat(out, " ") + ]])() +end) +P("P actl", function() + local src = "local x = 1\\n" + for i = 1, 12 do src = src .. "local y" .. i .. " = " .. i .. "\\n" end + src = src .. "return x\\n" + local f = load(src, "=actl") + return gi(f, "SL") +end) +P("P actl2", function() + return gi(debug.getinfo(1, "f").func, "SL") +end) + """); + } + + /** 行钩子里抛错:主线程上致命,pcall 接不住(约定 #31)。 */ + @Test + void lineHookErrorIsFatalOnMainThread() { + Diff.assertFullMatches(""" +-- 主线程上「行钩子里抛错」:脚本直接崩,pcall 接不住(约定 #31)。 +-- 这类用例会终止脚本,所以必须单独成条——塞进大网格里会把后面几百行全吃掉。 +print("before") +local r = pcall(function() local a = 1 return a + 1 end) +print("after", r) +debug.sethook(function(what) error("hook:" .. tostring(what)) end, "l") +local r2 = pcall(function() local a = 1 return a + 1 end) +print("unreachable", r2) +debug.sethook() +print("still unreachable") + """); + } + + /** 调用钩子里抛错:同样致命,触发点与行钩子不同。 */ + @Test + void callHookErrorIsFatalOnMainThread() { + Diff.assertFullMatches(""" +-- 主线程上「调用钩子里抛错」:同样致命,但触发点与行钩子不同(约定 #31)。 +print("before") +local r = pcall(function() return ("x"):upper() end) +print("after", r) +debug.sethook(function(what) error("hook:" .. tostring(what)) end, "c") +local r2 = pcall(function() return ("x"):upper() end) +print("unreachable", r2) +debug.sethook() +print("still unreachable") + """); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/DebugTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/DebugTest.java new file mode 100644 index 0000000..ec5165e --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/DebugTest.java @@ -0,0 +1,1061 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * {@code debug} 库的运行期差分 —— 对应 {@code ldblib.c} + {@link DebugLib}。 + *

+ * 这批用例全部走 {@link Diff#assertFullMatches}(不剥回溯): + * {@code debug.traceback} 的返回值本身就是回溯,走默认的剥回溯规则会把 + * 用例整段抹成空串,然后因「两边都空」而假绿。 + *

+ * 三个地方不可比,用例里主动避开: + *

    + *
  • 地址:{@code debug.upvalueid} 返回的是指针,只能比 + * {@code type} 与「两个 id 是否相等」,不能直接 {@code print};
  • + *
  • activelines 的具体内容:依赖每条指令的行号,本实现的行号表 + * 与上游一致所以可写,但一旦改代码生成器就得重校。
  • + *
+ */ +final class DebugTest { + + @BeforeAll + static void requireRef() { + assumeTrue(ReferenceLua.find() != null, "参考实现二进制不存在,跳过一致性测试"); + } + + /* ==================== getinfo:层号 ==================== */ + + /** + * 层号从 0 开始,0 是 {@code getinfo} 自己的 C 帧——这一点最容易搞错。 + *

+ * 参考实测:{@code what=C, currentline=-1, name=getinfo}。名字是 + * {@code funcnamefromcall} 在上一帧({@code g})的调用指令上查出来的, + * 所以问的是「{@code g} 当时在调谁」,不是「这一帧叫什么」。 + */ + @Test + void levels() { + Diff.assertFullMatches("" + + "local function g()\n" + + " for i = 0, 6 do\n" + + " local t = debug.getinfo(i)\n" + + " print(i, t and (t.what .. \" \" .. tostring(t.currentline)\n" + + " .. \" \" .. tostring(t.name) .. \" \" .. t.namewhat))\n" + + " end\n" + + "end\n" + + "g()\n"); + } + + /** {@code what} / {@code source} / {@code short_src} / 行号范围 */ + @Test + void sourceAndWhat() { + Diff.assertFullMatches("" + + "local function outer()\n" + + " local function inner()\n" + + " local i = debug.getinfo(1)\n" + + " print(i.source, i.short_src, i.linedefined, i.lastlinedefined,\n" + + " i.what, i.currentline, i.nups, i.nparams, i.isvararg,\n" + + " i.name, i.namewhat, i.istailcall)\n" + + " local j = debug.getinfo(2)\n" + + " print(j.what, j.currentline, j.name, j.namewhat)\n" + + " local k = debug.getinfo(3)\n" + + " print(k.what, k.currentline, tostring(k.name), k.namewhat)\n" + + " end\n" + + " inner()\n" + + "end\n" + + "outer()\n"); + } + + /** 以函数为参数(走 {@code >} 分支):此时没有活动帧,{@code currentline} 是 -1 */ + @Test + void infoFromFunction() { + Diff.assertFullMatches("" + + "local function h(a, b) return a + b end\n" + + "print(debug.getinfo(h).what, debug.getinfo(h).currentline,\n" + + " debug.getinfo(h).nparams, debug.getinfo(h).isvararg,\n" + + " debug.getinfo(h).nups)\n" + + "print(debug.getinfo(print).what, debug.getinfo(print).currentline,\n" + + " debug.getinfo(print).nups)\n" + + "print(debug.getinfo(print).source, debug.getinfo(print).short_src,\n" + + " debug.getinfo(print).linedefined)\n" + + "local i = debug.getinfo(h, \"Slnu\")\n" + + "print(i.what, i.short_src, i.linedefined, i.lastlinedefined,\n" + + " i.currentline, i.nups, i.nparams, i.isvararg)\n"); + } + + /** {@code 'f'} 与 {@code 'L'}:两个要往栈上推东西的选项 */ + @Test + void infoFuncAndLines() { + Diff.assertFullMatches("" + + "local function h(a) return a end\n" + + "local i = debug.getinfo(h, \"f\")\n" + + "print(i.func == h)\n" + + "local j = debug.getinfo(1, \"f\")\n" + + "print(type(j.func))\n" + + "local k = debug.getinfo(h, \"L\")\n" + + "print(type(k.activelines))\n" + + "local n = 0\n" + + "for line in pairs(k.activelines) do n = n + 1 end\n" + + "print(n > 0)\n" + + "print(debug.getinfo(print, \"L\").activelines)\n"); + } + + /** 选项字符串的边界:空串合法、非法字符报错、{@code >} 不能由脚本给 */ + @Test + void infoOptions() { + Diff.assertFullMatches("" + + "print(type(debug.getinfo(1, \"\")))\n" + + "print(debug.getinfo(1, \"\").currentline)\n" + + "print(pcall(debug.getinfo, 1, \"z\"))\n" + + "print(pcall(debug.getinfo, 1, \">f\"))\n" + + "print(pcall(debug.getinfo, 99))\n" + + "print(pcall(debug.getinfo, -1))\n" + + "print(pcall(debug.getinfo))\n" + + "print(pcall(debug.getinfo, 1, 5))\n"); + } + + /* ==================== getlocal / setlocal ==================== */ + + @Test + void locals() { + Diff.assertFullMatches("" + + "local t = {}\n" + + "function t.method(a, b, ...)\n" + + " local p, q = 1, 2\n" + + " local i = debug.getinfo(1)\n" + + " print(\"n1\", i.name, i.namewhat, i.nparams, i.isvararg, i.nups)\n" + + " print(\"n2\", debug.getinfo(2).name, debug.getinfo(2).namewhat)\n" + + " print(\"loc\", debug.getlocal(1, 1))\n" + + " print(\"loc\", debug.getlocal(1, 2))\n" + + " print(\"loc\", debug.getlocal(1, 3))\n" + + " print(\"loc\", debug.getlocal(1, 4))\n" + + " print(\"loc\", debug.getlocal(1, 5))\n" + + " print(\"loc\", debug.getlocal(1, 6))\n" + + " print(\"loc0\", debug.getlocal(1, 0))\n" + + " print(\"varg\", debug.getlocal(1, -1))\n" + + " print(\"varg\", debug.getlocal(1, -2))\n" + + " print(\"varg\", debug.getlocal(1, -3))\n" + + " print(\"set\", debug.setlocal(1, 1, 99), p)\n" + + " print(\"set\", debug.setlocal(1, 99, 1))\n" + + "end\n" + + "t.method(10, 20, 30, 40)\n"); + } + + /** + * 以函数为参数的 {@code getlocal}:只看入口处还活着的参数, + * 而且不返回值({@code db_getlocal} 在这里 {@code return 1})。 + */ + @Test + void localFromFunction() { + Diff.assertFullMatches("" + + "local function h(a, b, c) return a end\n" + + "print(debug.getlocal(h, 1))\n" + + "print(debug.getlocal(h, 2))\n" + + "print(debug.getlocal(h, 3))\n" + + "print(debug.getlocal(h, 4))\n" + + "print(debug.getlocal(print, 1))\n" + + "print(pcall(debug.getlocal, 1))\n" + + "print(pcall(debug.getlocal, 99, 1))\n"); + } + + /* ==================== upvalue 四件套 ==================== */ + + @Test + void upvalues() { + Diff.assertFullMatches("" + + "local up = 1\n" + + "local function f() return up end\n" + + "local function g() up = up + 1 end\n" + + "print(debug.getupvalue(f, 1))\n" + + "print(debug.getupvalue(f, 2))\n" + + "print(debug.getupvalue(print, 1))\n" + + "print(debug.setupvalue(f, 1, 42), f())\n" + + "print(debug.setupvalue(f, 9, 1))\n" + + "print(debug.setupvalue(print, 1, 1))\n" + + "print(type(debug.upvalueid(f, 1)))\n" + + "print(debug.upvalueid(f, 1) == debug.upvalueid(g, 1))\n" + + "print(debug.upvalueid(f, 1) == debug.upvalueid(f, 1))\n" + + "print(debug.upvalueid(print, 1))\n" + + "print(debug.upvalueid(f, 9))\n"); + } + + /** {@code _ENV} 是主 chunk 的第 1 个 upvalue,{@code load} 出来的也是 */ + @Test + void envUpvalue() { + Diff.assertFullMatches("" + + "local u = load(\"return 1\")\n" + + "print(debug.getupvalue(u, 1))\n" + + "local t = {}\n" + + "print(debug.setupvalue(u, 1, t))\n" + + "print(u())\n" + + "print(debug.getupvalue(u, 1))\n"); + } + + /** + * {@code upvaluejoin} 换的是「upvalue 指针」而不是值——换完两边共享同一个。 + *

+ * 参差:{@code upvaluejoin(print, 1, k, 1)} 报的是 #2 越界 + * ({@code checkupval} 先跑),不是 #1 类型错。 + */ + @Test + void upvalueJoin() { + Diff.assertFullMatches("" + + "local a, b = 10, 20\n" + + "local function h() return a end\n" + + "local function k() return b end\n" + + "print(h(), k())\n" + + "print(debug.upvalueid(h, 1) == debug.upvalueid(k, 1))\n" + + "debug.upvaluejoin(h, 1, k, 1)\n" + + "print(h(), k())\n" + + "print(debug.upvalueid(h, 1) == debug.upvalueid(k, 1))\n" + + "print(pcall(debug.upvaluejoin, h, 1, k, 1))\n" + + "print(pcall(debug.upvaluejoin, print, 1, k, 1))\n" + + "print(pcall(debug.upvaluejoin, h, 1, print, 1))\n" + + "print(pcall(debug.upvaluejoin, h, 9, k, 1))\n"); + } + + /* ==================== registry / metatable / userdata ==================== */ + + @Test + void registryAndMetatable() { + Diff.assertFullMatches("" + + "print(type(debug.getregistry()))\n" + + "print(debug.getregistry() == debug.getregistry())\n" + + "print(debug.getmetatable(1))\n" + + "print(type(debug.getmetatable(\"x\")))\n" + + "print(debug.getmetatable({}))\n" + + "print(debug.setmetatable(1, {__index = function() return \"one\" end}))\n" + + "print((1).foo)\n" + + "print(debug.setmetatable(1, nil))\n" + + "print((1).foo)\n" + + "print(debug.setmetatable({}, {}))\n" + + "print(debug.getmetatable(debug.setmetatable({}, {__index = {x = 7}})).__index.x)\n" + + "print(pcall(debug.setmetatable, 1, 5))\n" + + "print(pcall(debug.setmetatable))\n" + + "print(pcall(debug.getmetatable))\n"); + } + + @Test + void userValues() { + Diff.assertFullMatches("" + + "print(debug.getuservalue(1))\n" + + "print(debug.getuservalue({}, 3))\n" + + "print(debug.getuservalue(io.stdout))\n" + + "print(pcall(debug.setuservalue, 1, 5))\n" + + "print(pcall(debug.setuservalue, {}, 5))\n" + + "print(pcall(debug.getuservalue, 1, \"x\"))\n"); + } + + /* ==================== traceback ==================== */ + + @Test + void traceback() { + Diff.assertFullMatches("" + + "local function a() error(\"boom\") end\n" + + "local function b() a() end\n" + + "local function c() b() end\n" + + "print(xpcall(c, debug.traceback))\n"); + } + + @Test + void tracebackMessage() { + Diff.assertFullMatches("" + + "print(debug.traceback(\"msg\"))\n" + + "print(debug.traceback(42))\n" + + "print(debug.traceback({}))\n" + + "print(debug.traceback(nil))\n"); + } + + /** 协程参数:此时默认 level 是 0,不是 1 */ + @Test + void tracebackCoroutine() { + Diff.assertFullMatches("" + + "local co = coroutine.create(function() error(\"inner\") end)\n" + + "print(coroutine.resume(co))\n" + + "print(debug.traceback(co))\n" + + "print(debug.traceback(co, \"pre\"))\n" + + "print(debug.traceback(co, \"pre\", 1))\n"); + } + + /** 深栈:{@code LEVELS1 + LEVELS2} 之外的层会被折成 {@code (skipping N levels)} */ + @Test + void tracebackDeep() { + Diff.assertFullMatches("" + + "local function f(n)\n" + + " if n == 0 then print(debug.traceback()) else f(n - 1) end\n" + + "end\n" + + "f(30)\n"); + } + + /** + * 消息处理函数的边界。 + *

+ * 处理函数自己出错时,C 版会递归进 {@code luaG_errormsg} 直到 C 栈 + * 溢出,被兜成 {@code error in error handling}——不是 {@code 'b'}, + * 也不是原来的 {@code 'a'}。 + */ + @Test + void handlerErrors() { + Diff.assertFullMatches("" + + "print(xpcall(function() error('a') end,\n" + + " function(e) error('b') end))\n" + + "print(xpcall(function() error('a') end,\n" + + " function(e) return e .. '!' end))\n" + + "print(pcall(function() error(setmetatable({},\n" + + " {__tostring = function() return 'TS' end})) end))\n" + + "print(error(setmetatable({}, {__tostring = function() return 'TS' end})))\n"); + } + + /* ==================== 杂项 ==================== */ + + @Test + void cStackLimit() { + Diff.assertFullMatches("" + + "print(debug.setcstacklimit(10))\n" + + "print(debug.setcstacklimit(1000000))\n" + + "print(pcall(debug.setcstacklimit))\n" + + "print(pcall(debug.setcstacklimit, \"x\"))\n"); + } + + /** + * 键的集合:本轮搬的 14 个。 + *

+ * 脚本里显式滤掉 {@code sethook} / {@code gethook}:那两个要等解释器 + * 主循环里插好 {@code luaD_hook} 的调用点才能搬,不滤掉这条用例会一直红在 + * 一件已知未做、且不是本轮目标的事情上——而一直红的门禁等于没有门禁。 + */ + @Test + void keys() { + Diff.assertFullMatches("" + + "local ks = {}\n" + + "for k in pairs(debug) do\n" + + " ks[#ks + 1] = k\n" + + "end\n" + + "table.sort(ks)\n" + + "print(#ks)\n" + + "print(table.concat(ks, \",\"))\n"); + } + + /** 错误对象不是字符串时,{@code traceback} 原样返回 */ + @Test + void tracebackNonString() { + Diff.assertFullMatches("" + + "print(debug.traceback(true))\n" + + "print(debug.traceback(print))\n" + + "print(pcall(function() return debug.traceback(setmetatable({}, {})) end))\n"); + } + + /* ==================== debug.debug:那个读 stdin 的循环 ==================== */ + + /** + * {@code ldblib.c: db_debug} 的三条硬规矩,少一条逐字节比对就红。 + *

+ * ①每次读之前先往 stderr 打 {@code "lua_debug> "}(无换行)。漏了它, + * 参考实现的输出里就凭空多十一字节。②是循环不是读一行就返回:一直读到 + * EOF 或某一行正好是 {@code "cont\n"},中间每行都当 chunk 执行、出错也继续。 + * ③命令的 chunk 名是 {@code =(debug command)},报错里显示成 + * {@code (debug command):1:}。 + *

+ * 顺带记一条流顺序上的事:C 的 {@code print} 走 + * {@code lua_writeline},它每次都 fflush(stdout),所以 stdout 与 + * stderr 在合并捕获时是严格按程序顺序交错的 —— 不是「stdout 攒到最后」。 + * 本实现的 {@code L.out} 与 {@code L.err} 指到同一条缓冲上,顺序天然一致。 + *

+ * 这条用例必须走 {@code Diff.assertMatchesWithStdin}:只喂 + * {@code /dev/null} 的话循环体、出错分支、{@code cont} 三条路一条也走不到。 + */ + @Test + void interactiveDebuggerMatchesReference() { + // 空 stdin:打一个提示符,第一次 fgets 就 EOF,直接返回 + Diff.assertMatchesWithStdin("debug.debug()\nprint(\"after\")\n", ""); + // 只一行 cont:打一个提示符就退出 + Diff.assertMatchesWithStdin("debug.debug()\nprint(\"after\")\n", "cont\n"); + // 完整循环:执行 / 语法错 / 再执行 / cont 退出 + Diff.assertMatchesWithStdin("debug.debug()\nprint(\"after\")\n", + "print(\"one\")\n1+\nprint(\"two\")\ncont\n"); + // 末尾没有换行也照样执行,然后 EOF 退出 + Diff.assertMatchesWithStdin("debug.debug()\nprint(\"after\")\n", "print(\"nl\")"); + // 运行期错误同样打到 stderr 并继续 + Diff.assertMatchesWithStdin("debug.debug()\nprint(\"after\")\n", + "error(\"boom\")\nprint(\"ok\")\ncont\n"); + // fgets 的 250 字节缓冲:超长一行会被切成多段,每段各自当 chunk 跑 + Diff.assertMatchesWithStdin("debug.debug()\nprint(\"after\")\n", + "print(\"\" .. string.rep(\"x\", 300))\ncont\n"); + } + + /* ==================== 钩子 ==================== */ + + /** + * 调用/返回钩子。 + *

+ * 第一个事件是 {@code return} 而不是 {@code call}——那是从 + * {@code debug.sethook} 这个 C 函数返回时报的。它证明调用钩子必须挂在 + * {@code precallC} 里「真正调 C 函数」之前、返回钩子挂在 + * {@code poscall} 里:挂在别处就少了这一条。 + *

+ * 末尾那个孤零零的 {@code call} 是 {@code debug.sethook()}(关钩子的那次) + * 自己的调用事件:钩子在它建帧时就报了,它才在里头把钩子关掉。 + */ + @Test + void hookCallReturn() { + Diff.assertFullMatches("" + + "local n = 0\n" + + "local function f(a) return a + 1 end\n" + + "debug.sethook(function(ev, ln)\n" + + " n = n + 1\n" + + " print(n, ev, ln)\n" + + "end, \"cr\")\n" + + "local r = f(1)\n" + + "local g = function() return f(2) end\n" + + "local r2 = g()\n" + + "debug.sethook()\n" + + "print(\"done\", r, r2)\n"); + } + + /** 行钩子:循环里同一行会重复报(往回跳也算换过行) */ + @Test + void hookLine() { + Diff.assertFullMatches("" + + "debug.sethook(function(ev, ln) print(ev, ln) end, \"l\")\n" + + "local s = 0\n" + + "for i = 1, 2 do s = s + i end\n" + + "debug.sethook()\n" + + "print(s)\n"); + } + + /** 计数钩子:周期由第 3 个参数给 */ + @Test + void hookCount() { + Diff.assertFullMatches("" + + "local n = 0\n" + + "debug.sethook(function(ev, ln) n = n + 1 end, \"\", 5)\n" + + "local s = 0\n" + + "for i = 1, 20 do s = s + i end\n" + + "debug.sethook()\n" + + "print(\"count\", n, s)\n"); + } + + /** 尾调用报 {@code tail call},不是 {@code call} */ + @Test + void hookTailCall() { + Diff.assertFullMatches("" + + "debug.sethook(function(ev) print(ev) end, \"c\")\n" + + "local function a() return b() end\n" + + "function b() return 3 end\n" + + "print(a())\n" + + "debug.sethook()\n"); + } + + /** + * {@code gethook} 的三个返回值。 + *

+ * 没有钩子时它只返回一个 {@code nil}——写成返回三个会让 + * {@code print(debug.gethook())} 多打两个空字段。 + */ + @Test + void hookGetSet() { + Diff.assertFullMatches("" + + "print(debug.gethook())\n" + + "debug.sethook(function() end, \"crl\", 7)\n" + + "print(debug.gethook())\n" + + "local h, m, c = debug.gethook()\n" + + "print(type(h), m, c)\n" + + "debug.sethook()\n" + + "print(debug.gethook())\n" + + "debug.sethook(function() end, \"l\")\n" + + "print(debug.gethook())\n" + + "debug.sethook(function() end, \"\", 3)\n" + + "print(debug.gethook())\n" + + "debug.sethook()\n"); + } + + /** + * 钩子函数里出错。 + *

+ * 这个错误抓不到:调用钩子是在 {@code pcall} 的 C 帧建起来之前 + * 报的,{@code pcall} 的恢复点还没生效。同时回溯里那一层要印成 + * {@code in hook '?'}——这要求 {@code CIST_HOOKED} 在钩子抛错时不清, + * 因为 C 版的 {@code luaD_throw} 会把 {@code luaD_hook} 的收尾整段跳过去。 + */ + @Test + void hookError() { + Diff.assertFullMatches("" + + "debug.sethook(function(ev) error(\"in hook \" .. ev) end, \"c\")\n" + + "local ok, e = pcall(function() local t = {} return t.x end)\n" + + "debug.sethook()\n" + + "print(\"ok\", ok, e)\n"); + } + + /** + * 钩子函数里 yield 一律报错,主线程上是「不在协程里」。 + *

+ * 这条不是「没实现 yield-in-hook」的托辞,而是参考实现的行为: + * {@code hookf} 用 {@code lua_call} 调钩子函数,走 + * {@code luaD_callnoyield},「不可 yield」层数加一,于是钩子永远不可 yield。 + */ + @Test + void hookYieldFails() { + Diff.assertFullMatches("" + + "debug.sethook(function(ev) coroutine.yield() end, \"l\")\n" + + "print(\"A\")\n" + + "print(pcall(function() print(\"B\") print(\"C\") end))\n" + + "debug.sethook()\n" + + "print(\"D\")\n"); + } + + /** 协程里 yield 报的是另一条:跨 C 调用边界 */ + @Test + void hookYieldInCoroutineFails() { + Diff.assertFullMatches("" + + "local co = coroutine.create(function()\n" + + " print(\"in co 1\")\n" + + " print(\"in co 2\")\n" + + " return \"end\"\n" + + "end)\n" + + "debug.sethook(co, function(ev, ln)\n" + + " print(\"H\", ev, ln)\n" + + " coroutine.yield()\n" + + "end, \"l\")\n" + + "print(coroutine.resume(co))\n" + + "print(coroutine.resume(co))\n" + + "print(coroutine.resume(co))\n"); + } + + /** 给协程单独设钩子:只跟踪那个协程,且能随时关掉 */ + @Test + void hookOnCoroutine() { + Diff.assertFullMatches("" + + "local co = coroutine.create(function()\n" + + " local s = 0\n" + + " for i = 1, 3 do s = s + i end\n" + + " coroutine.yield(s)\n" + + " s = s * 2\n" + + " return s\n" + + "end)\n" + + "debug.sethook(co, function(ev, ln) print(\"H\", ev, ln) end, \"l\")\n" + + "print(coroutine.resume(co))\n" + + "print(debug.gethook(co))\n" + + "debug.sethook(co)\n" + + "print(coroutine.resume(co))\n" + + "print(coroutine.resume(co))\n"); + } + + /** + * 返回钩子的传递信息:{@code debug.getinfo(2, "r")}。 + *

+ * 层号 2 是「引发事件的那一帧」——钩子自己是 1。{@code ftransfer / ntransfer} + * 只在返回钩子期间非零({@code CIST_TRAN}),普通代码里一律 0/0。 + */ + @Test + void hookTransferInfo() { + Diff.assertFullMatches("" + + "debug.sethook(function(ev, ln)\n" + + " local t = debug.getinfo(2, \"rSn\")\n" + + " print(ev, ln, t.ftransfer, t.ntransfer, t.what, t.name)\n" + + "end, \"cr\")\n" + + "local function f() return 1, 2, 3 end\n" + + "local a, b = f()\n" + + "debug.sethook()\n" + + "print(debug.getinfo(1, \"r\").ntransfer)\n"); + } + + /** 嵌套调用:调用与返回必须成对,顺序按「先进后出」 */ + @Test + void hookNested() { + Diff.assertFullMatches("" + + "debug.sethook(function(ev, ln) print(ev, ln) end, \"cr\")\n" + + "local function inner() return 7 end\n" + + "local function outer() return inner() + 1 end\n" + + "print(outer())\n" + + "debug.sethook()\n"); + } + + /** + * 变参函数:调用钩子推迟到 {@code OP_VARARGPREP} 之后才报。 + *

+ * 那一步是 {@code luaG_tracecall} 返回 0(关 trap)再由 + * {@code OP_VARARGPREP} 后的 {@code updatetrap} 读回 1 实现的; + * 少了这个来回,变参函数的调用事件就会凭空多一个或少一个。 + */ + @Test + void hookVararg() { + Diff.assertFullMatches("" + + "debug.sethook(function(ev, ln) print(ev, ln) end, \"cr\")\n" + + "local function v(...)\n" + + " local n = select('#', ...)\n" + + " return n\n" + + "end\n" + + "print(v(1, 2, 3))\n" + + "local function w(a, ...) return a end\n" + + "print(w(9))\n" + + "debug.sethook()\n"); + } + + /** 行钩子 + 调用钩子同时开:{@code oldpc} 的更新必须跟得上 */ + @Test + void hookLineAndCall() { + Diff.assertFullMatches("" + + "debug.sethook(function(ev, ln) print(ev, ln) end, \"cl\")\n" + + "local function f(a)\n" + + " local x = a\n" + + " return x\n" + + "end\n" + + "print(f(1))\n" + + "debug.sethook()\n"); + } + + /** 钩子函数里再改钩子:{@code allowhook} 为 0 时嵌套的钩子不该触发 */ + @Test + void hookInsideHook() { + Diff.assertFullMatches("" + + "local n = 0\n" + + "debug.sethook(function(ev)\n" + + " if n == 0 then\n" + + " n = 1\n" + + " debug.sethook(function(e2) print(\"inner\", e2) end, \"cr\")\n" + + " local t = {} -- 这几行的内钩子不该来\n" + + " t.x = 1\n" + + " debug.sethook()\n" + + " end\n" + + " print(\"outer\", ev)\n" + + "end, \"cr\")\n" + + "local function q() return 1 end\n" + + "q()\n" + + "debug.sethook(function(ev) print(\"after\", ev) end, \"c\")\n" + + "q()\n" + + "debug.sethook()\n"); + } + + /** 执行到一半开/关钩子:{@code settraps} 与 {@code OP_CALL} 后的 updatetrap */ + @Test + void hookToggleMidRun() { + Diff.assertFullMatches("" + + "local function f() return 1 end\n" + + "f()\n" + + "debug.sethook(function(ev, ln) print(\"on\", ev, ln) end, \"l\")\n" + + "local a = 1\n" + + "local b = 2\n" + + "debug.sethook()\n" + + "local c = 3\n" + + "debug.sethook(function(ev, ln) print(\"again\", ev, ln) end, \"cl\")\n" + + "f()\n" + + "debug.sethook()\n" + + "print(a, b, c)\n"); + } + + /** + * 钩子的掩码 × count × 线程格子。 + *

+ * 已有的 {@link #hookCallReturn} / {@link #hookLine} / {@link #hookCount} / + * {@link #hookTailCall} 是一个掩码一片,这片是八个掩码的全组合 + * 配上 count 与协程 —— 因为好几个语义只有在格子里才看得出来: + *

    + *
  • {@code makemask} 用 {@code strchr} 扫,只认 c / r / l: + * {@code "x"} 是空掩码(一个事件都没有),{@code "CC"} 也是空的 + * (大小写敏感)。单看 {@code "crl"} 永远不会发现。
  • + *
  • {@code count} 只有 {@code > 0} 才并进 {@code LUA_MASKCOUNT}: + * 传 0 与传 -1 都没有 count 事件,而掩码 {@code ""} + count 1 + * 是只有 count 事件。
  • + *
  • 钩子是按线程装的:给主线程装的行钩子,协程里一行都不触发; + * 反过来 {@code debug.sethook(co, ...)} 只管那个协程。
  • + *
  • {@code gethook} 的掩码不含 count({@code unmakemask} 只输出 + * c / r / l),count 是第三个返回值单独给的。
  • + *
+ *

+ * 形状上有一样必须照做:事件收集起来最后统一打印。钩子开着的时候每一个 + * {@code print} 都会自己产生一对 call / return 事件,混进结果里就什么也看不出了。 + * 钩子体本身跨好几行、还调函数 —— 重入({@code allowhook})失效的话这里就是 + * 无限递归,用例会直接炸而不是比对失败。 + */ + @Test + void hookMaskMatrix() { + Diff.assertFullMatches(""" + local out = {} + local function say(s) out[#out + 1] = s end + + local function collect(tag, mask, count, body) + local ev = {} + local on = true + local function h(what, line) + if not on then return end + -- 钩子体跨多行且调函数:重入的话这里就是无限递归 + local t = what + if what == "line" then t = "l" .. tostring(line) end + ev[#ev + 1] = t + end + debug.sethook(h, mask, count or 0) + local ok, err = pcall(body) + on = false + debug.sethook() + say(tag .. " ok=" .. tostring(ok) .. " err=" .. tostring(err) + .. " [" .. table.concat(ev, " ") .. "]") + end + + local function b_empty() end + local function b_two() local x = 1 x = x + 1 return x end + local function b_call() local r = b_empty() return r end + local function b_tail() return b_two() end + local function b_err() error("BOOM") end + local function b_loop() local s = 0 for i = 1, 3 do s = s + i end return s end + local function b_pc() return pcall(function() return 1 end) end + + -- 1) 八个掩码 + collect("m0", "", 0, b_two) + collect("mc", "c", 0, b_two) + collect("mr", "r", 0, b_two) + collect("ml", "l", 0, b_two) + collect("mcr", "cr", 0, b_two) + collect("mcl", "cl", 0, b_two) + collect("mrl", "rl", 0, b_two) + collect("mcrl", "crl", 0, b_two) + -- 2) 未知字母与大写:makemask 只认 c / r / l + collect("mx", "x", 0, b_two) + collect("mcrlx", "crlx", 0, b_two) + collect("mCC", "CC", 0, b_two) + -- 3) 尾调用 vs 普通调用 + collect("t1", "crl", 0, b_tail) + collect("t2", "c", 0, b_tail) + collect("t3", "crl", 0, b_call) + -- 4) 出错与 pcall + collect("e1", "crl", 0, b_err) + collect("e2", "crl", 0, b_pc) + -- 5) count:0 与负数都不并进 LUA_MASKCOUNT + collect("n1", "", 1, b_loop) + collect("n2", "l", 2, b_loop) + collect("n3", "", 0, b_loop) + collect("n4", "", -1, b_loop) + collect("n5", "crl", 3, b_loop) + -- 6) gethook:掩码里不含 count + say("g1 " .. tostring(debug.gethook())) + local h1 = function() end + debug.sethook(h1, "cl", 7) + local a, b, c = debug.gethook() + say("g2 " .. tostring(a == h1) .. " '" .. tostring(b) .. "' " .. tostring(c)) + debug.sethook(h1, "", 0) + a, b, c = debug.gethook() + say("g3 " .. tostring(a == h1) .. " '" .. tostring(b) .. "' " .. tostring(c)) + debug.sethook() + say("g4 " .. tostring(debug.gethook())) + -- 7) 报错顺序:mask(第 2 参)在 function(第 1 参)之前查 + say("s1 " .. tostring(pcall(debug.sethook, 42, "l"))) + say("s2 " .. tostring(pcall(debug.sethook, print))) + say("s3 " .. tostring(pcall(debug.sethook, print, "l", "x"))) + say("s4 " .. tostring(pcall(debug.sethook, print, {}))) + say("s5 " .. tostring(pcall(debug.sethook, print, 42))) + -- 8) 钩子按线程装:主线程的行钩子管不到协程里 + local co = coroutine.create(function() + local s = 0 + for i = 1, 3 do s = s + i coroutine.yield(s) end + return s + end) + local coev = {} + local on2 = true + local function ch(what, line) + if not on2 then return end + coev[#coev + 1] = (what == "line") and ("l" .. tostring(line)) or what + end + debug.sethook(ch, "l", 0) + say("c1 " .. tostring(coroutine.resume(co))) + say("c2 [" .. table.concat(coev, " ") .. "]") + on2 = false + debug.sethook() + -- 反过来:装到协程上,只管那个协程 + local co2 = coroutine.create(function() return 1 end) + debug.sethook(co2, ch, "cl", 0) + say("c4 " .. tostring(debug.gethook(co2) ~= nil)) + on2 = true + say("c5 " .. tostring(coroutine.resume(co2))) + say("c6 [" .. table.concat(coev, " ") .. "]") + on2 = false + debug.sethook(co2) + print(table.concat(out, "\\n")) + """); + } + + /** + * {@code sethook} 的参数检查顺序。 + *

+ * 掩码(第 2 个参数)先于类型(第 1 个参数)检查,所以 + * {@code debug.sethook(1, 2)} 报的是「#2 不是字符串」。 + */ + @Test + void hookArgErrors() { + Diff.assertFullMatches("" + + "print(pcall(debug.sethook))\n" + + "print(pcall(debug.sethook, 1, 2))\n" + + "print(pcall(debug.sethook, print))\n" + + "print(pcall(debug.sethook, print, \"x\"))\n" + + "print(pcall(debug.sethook, print, \"crl\", \"z\"))\n" + + "print(pcall(debug.gethook, 1))\n"); + } + + /** + * {@code debug.getinfo} 的 name / namewhat 网格(77 行)。 + *

+ * {@code name} 不是存在函数里的,而是从调用点反推出来的 + * ({@code funcnamefromcall} → {@code funcnamefromcode} → {@code getobjname}): + * 看调用者那条指令是 {@code OP_GETFIELD} / {@code OP_GETTABUP} / + * {@code OP_GETUPVAL} / {@code OP_MOVE} / {@code OP_SELF} 里的哪一种, + * 再从常量表或寄存器里把名字抠出来。所以同一个函数在不同语法位置上被调用, + * {@code name} 与 {@code namewhat} 都不同 —— 只能铺成矩阵逐个比对。 + *

+ * 网格覆盖九种 {@code namewhat}:{@code global} / {@code local} / + * {@code field} / {@code method} / {@code upvalue} / {@code metamethod} / + * {@code for iterator} / {@code ""}(推不出来)/ 以及取不到时的 {@code nil}。 + * 几个不看参考实现想不到的形状: + *

    + *
  • {@code t[1]()} 的名字是 {@code "integer index"}, + * 不是 {@code "?"}。整数键在 {@code getobjname} 里是单独一支; + * 只有浮点键与布尔键才落到 {@code "?"}。
  • + *
  • {@code "?"} 的 {@code namewhat} 仍然是 {@code "field"} —— + * 名字推不出来,调用方式推得出来,两者不挂钩。
  • + *
  • metamethod 那一路的 {@code name} 是去掉 {@code __} 的 + * 元方法名({@code index} / {@code newindex} / {@code add} / {@code len} / + * {@code close} …),而 {@code __call} 走的是普通调用,给的是 + * {@code name="a1", namewhat="local"}。
  • + *
  • {@code __call} 那一路给 {@code name="t", namewhat="local"}, + * 不是 {@code "method"}。
  • + *
  • 取不到名字时 {@code name} 是 {@code nil} 而 {@code namewhat} 是 + * 空串,不是 {@code nil}。
  • + *
+ * 这一片第一跑就逐字节一致(除脚本路径),所以敏感性是量过的:把 + * {@code "integer index"} 改成 {@code "?"} 红 5 行;把 {@code namewhat} 的 + * {@code "field"} 改大小写红 1 行。 + */ + @Test + void functionNameShapes() { + Diff.assertMatches(""" + + -- debug.getinfo 的 name / namewhat 网格:让同一个函数在各种语法位置上被调用 + local function p() + local i = debug.getinfo(1, "n") + print(tostring(i.name), "[" .. tostring(i.namewhat) .. "]") + end + local function q() + local i = debug.getinfo(1, "nS") + print(tostring(i.name), "[" .. tostring(i.namewhat) .. "]", + i.what, i.short_src, i.linedefined, i.lastlinedefined) + end + + -- 全局 + g = p + g() + -- 局部 + local l = p + l() + do local l2 = p l2() end + -- 表字段(字符串键) + local t = {} + t.x = p + t.x() + t["y z"] = p + t["y z"]() + -- 表字段(非字符串键):C 里 getobjname 给 "?" + t[1] = p + t[1]() + t[1.5] = p + t[1.5]() + t[true] = p + t[true]() + -- 方法 + local obj = {} + function obj:m() return p() end + obj:m() + local mt = { __index = { mm = p } } + setmetatable(obj, mt) + obj:mm() + -- upvalue + do + local up = p + local function inner() return up() end + inner() + end + -- 直接调用、作为参数、返回值 + p() + local function callit(f) return f() end + callit(p) + local function retit() return p end + retit()() + -- 尾调用 + local function tail() return p() end + tail() + -- 主 chunk / getinfo 的其它层 + do + local i = debug.getinfo(1, "n") + print(tostring(i.name), "[" .. tostring(i.namewhat) .. "]") + end + print(tostring(debug.getinfo(p).name), "[" .. tostring(debug.getinfo(p).namewhat) .. "]") + print(tostring(debug.getinfo(print).name), "[" .. tostring(debug.getinfo(print).namewhat) .. "]") + print(debug.getinfo(p).what) + print(debug.getinfo(p).source, debug.getinfo(p).short_src) + print(debug.getinfo(p).nups, debug.getinfo(p).nparams, debug.getinfo(p).isvararg) + print(debug.getinfo(1).currentline) + + -- 匿名函数的名字 + local anon = function() end + print(tostring(debug.getinfo(anon).name), "[" .. tostring(debug.getinfo(anon).namewhat) .. "]") + local mt2 = {} + mt2.__call = function(self) return debug.getinfo(1, "n") end + setmetatable(t, mt2) + local r = t() + print(tostring(r.name), "[" .. tostring(r.namewhat) .. "]") + + -- getlocal / getupvalue 的名字 + local function upfn() + local a, b, c = 1, 2, 3 + return function() + local names = {} + for i = 1, 3 do + local n = debug.getupvalue(debug.getinfo(1, "f").func, i) + names[#names + 1] = tostring(n) + end + print(table.concat(names, ",")) + local ln = {} + for i = 1, 3 do + ln[#ln + 1] = tostring(debug.getlocal(1, i)) + end + print(table.concat(ln, ",")) + end + end + upfn()() + + -- 协程里的名字 + local co = coroutine.create(function() + local i = debug.getinfo(1, "n") + print(tostring(i.name), "[" .. tostring(i.namewhat) .. "]") + coroutine.yield() + end) + coroutine.resume(co) + + -- namewhat 在 C 函数上 + print(tostring(debug.getinfo(2, "n").name), "[" .. tostring(debug.getinfo(2, "n").namewhat) .. "]") + + -- 全局表上的字段/方法:走 OP_GETTABUP + 常量键,也就是 kName 那条路 + G = {} + G.f = p + G.f() + G["g h"] = p + G["g h"]() + G[1] = p + G[1]() + G[1.5] = p + G[1.5]() + G[true] = p + G[true]() + G[0] = p + G[0]() + G[-3] = p + G[-3]() + G[2^53] = p + G[2^53]() + function G:m() return p() end + G:m() + -- upvalue 上的表:OP_GETTABUP 但基址是 upvalue 不是 _ENV + do + local u = {} + u.f = p + u.m = function(self) return p() end + local function inner() + u.f() + u:m() + u[1]() + u[1.5]() + end + u[1] = p + u[1.5] = p + inner() + end + -- 通过 metamethod 与迭代器调用 + local mm = setmetatable({}, { __index = function() return p end }) + mm.anything() + local co2 = coroutine.wrap(p) + co2() + for kv in p do break end + print(pcall(p)) + print(select(1, p())) + -- 函数在 C 边界上:debug.getinfo 取 C 函数与 Lua 函数的名 + print(tostring(debug.getinfo(math.max, "n").name), + "[" .. tostring(debug.getinfo(math.max, "n").namewhat) .. "]") + print(debug.getinfo(math.max).what, debug.getinfo(math.max).nups) + print(pcall(debug.getupvalue, math.max, 1)) + print(pcall(debug.getlocal, math.max, 1)) + -- upvalue / local 名字的边界 + local function f2(x, y, ...) + local z = 1 + return function() + local a = {} + for i = 1, 4 do + local ok, n = pcall(debug.getupvalue, debug.getinfo(1, "f").func, i) + a[#a + 1] = ok and tostring(n) or "ERR" + end + print(table.concat(a, ",")) + local b = {} + for i = 1, 4 do + local ok, n = pcall(debug.getlocal, 1, i) + b[#b + 1] = ok and tostring(n) or "ERR" + end + print(table.concat(b, ",")) + end + end + f2(1, 2, 3)() + -- 越界的 upvalue 下标:返回空(tostring 会报 value expected),所以用 pcall 兜 + print(pcall(debug.getupvalue, p, 0)) + print(pcall(debug.getupvalue, p, 99)) + print(pcall(debug.getupvalue, p, -1)) + print(pcall(debug.getlocal, 1, 0)) + print(pcall(debug.getlocal, 1, 99)) + print(pcall(debug.getlocal, 99, 1)) + print(pcall(debug.getupvalue, print, 1)) + + + -- namewhat 的另外三种:for iterator / hook / metamethod + local function pm() + local i = debug.getinfo(1, "n") + print(tostring(i.name), "[" .. tostring(i.namewhat) .. "]") + return 1 + end + -- metamethod:name 是去掉 "__" 的元方法名 + local mmt = { + __index = pm, __newindex = pm, __add = pm, __concat = pm, __len = pm, + __eq = pm, __lt = pm, __le = pm, __unm = pm, __bnot = pm, __call = pm, + } + local a1 = setmetatable({}, mmt) + local a2 = setmetatable({}, mmt) + local ok1 = a1.x + a1.x = 1 + local ok3 = a1 + 1 + local ok4 = a1 .. "z" + local ok5 = #a1 + local ok6 = (a1 == a2) + local ok7 = (a1 < a2) + local ok8 = (a1 <= a2) + local ok9 = -a1 + local ok10 = ~a1 + a1() + -- 全局表访问触发 __index:走 OP_GETTABUP 那条路 + GM = setmetatable({}, mmt) + local ok11 = GM.x + -- __close + do + local cv = setmetatable({}, { __close = pm }) + end + -- for iterator + for k in pm do break end + -- hook:在钩子内部调用的函数 + do + local once = false + debug.sethook(function() + if not once then once = true pm() end + end, "c") + local function zz() return 1 end + zz() + debug.sethook() + end + + print("done") + """); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/Diff.java b/src/test/java/com/github/esrrhs/fakescript/lua/Diff.java new file mode 100644 index 0000000..bb3d02a --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/Diff.java @@ -0,0 +1,635 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +/** + * 运行期差分的公共驱动 —— 把「一段脚本跑两遍、比对输出」这件事收在一处。 + *

+ * 阶段 3 建 {@code RuntimeTest} 时这套逻辑是写在测试类里的;阶段 4 要再加一批 + * 针对闭包/upvalue/goto/变长参数的用例,再抄一遍就会出现两份略有差异的 + * 归一化规则——而归一化规则不一致本身就是一类假失败(一边抹了地址另一边没抹)。 + * 所以抽出来共用。 + *

+ * 归一化抹掉三样东西,它们都不属于「脚本语义」: + *

    + *
  • 回溯:依赖调试信息的层数,本实现还没有完整的 {@code ldebug.c} + * 回溯;
  • + *
  • 地址:{@code table: 0x...} 每次运行都不同;
  • + *
  • 程序名前缀:参考实现的程序名是二进制路径。
  • + *
+ */ +final class Diff { + + /** + * 脚本路径固定为 {@code target/rt/case.lua}。 + *

+ * 不能换成随机临时文件:参考实现的 {@code luaO_chunkid} 会把过长的源名 + * 从左边截成 {@code ...xxx},两边一截就没得比了。 + */ + static final String CASE_PATH = "target/rt/case.lua"; + + /** + * 传给本实现的 chunk 名带 {@code @},与参考实现一致。 + *

+ * 参考实现的 {@code lua.c} 拿到文件名后会自己加上 {@code @} 再交给 + * {@code luaL_loadfilex},打印时 {@code luaO_chunkid} 又把它去掉,所以 + * 两边看到的都是裸路径。这里同样加 {@code @} 再让 {@code chunkId} 剥掉—— + * 加载期与运行期错误走的是同一个 {@code luaO_chunkid}(见 + * {@code Lexer.lexError} 与 {@code LuaState.chunkId}),不能只有一边剥。 + */ + static final String CHUNK = "@" + CASE_PATH; + + private Diff() { + } + + /** + * 断言一段脚本在本实现与参考实现上的输出一致。 + *

+ * 顺带挡掉「空用例」:两边都输出空串的用例会**无条件通过**,什么也没证明。 + * 打错一个函数名、或者脚本在开头就静默结束了,都会变成这种空用例—— + * 而它看起来是全绿的一部分。 + */ + static void assertMatches(String src) { + requireReference(); + String raw = refRaw(src); + assertEquals(true, !raw.isEmpty(), + "用例在参考实现上没有产生任何输出,等于没测。script:\n" + src); + /* + * 挡掉「回溯把后半段吃掉」。 + *

+ * 归一化会从第一个 {@code stack traceback:} 处把后面全部砍掉。 + * 对「脚本先打印若干行再崩」这类用例,砍的正是错误信息本身,是有意的; + * 但对回溯属于预期输出的用例({@code debug.traceback} 的返回值、 + * 未捕获错误被 {@code msghandler} 装裱后的消息),砍掉的是脚本后半段 + * 的全部输出 —— 于是后面几十行分歧一条也发现不了,而用例照样是绿的。 + *

+ * 实测:{@code debug} 库矩阵第一版走的就是 {@code assertMatches}, + * 脚本第 5 段打印了 {@code debug.traceback()},后面 180 行(含咬出 + * {@code xpcall} 那处真分歧的行)全被砍掉,回退实现居然全绿。 + * 改成 {@code assertFullMatches} 才红。这种「静默少测」比假红危险得多, + * 所以这里直接判失败而不是默默砍。 + */ + assertEquals(true, !hasContentAfterTraceback(raw), + "输出里回溯后面还有脚本输出:assertMatches 会从回溯处把后面全砍掉," + + "那一段分歧一条也发现不了。回溯属于预期输出时请改用 " + + "assertFullMatches。script:\n" + src); + assertEquals(normalize(raw, true), mine(src), "script:\n" + src); + } + + /** + * 回溯之后是否还有不属于回溯的输出。 + *

+ * 判据是「回溯块后面出现了不以制表符开头的非空行」。回溯自己的每一行都以 + * 制表符开头({@code luaL_traceback} 的格式),所以: + *

    + *
  • 脚本自己崩了(未捕获错误经 {@code msghandler}):回溯在最后, + * 后面什么都没有 —— 这正是 {@code assertMatches} 存在的理由, + * 砍掉它是对的,放过;
  • + *
  • 回溯是脚本打印出来的({@code debug.traceback()} 的返回值): + * 回溯在中间,后面还有脚本自己的输出 —— 那一段会被静默丢掉, + * 必须挡住。
  • + *
+ */ + private static boolean hasContentAfterTraceback(String s) { + int tb = s.indexOf("stack traceback:"); + if (tb < 0) { + return false; + } + boolean started = false; + for (String line : s.substring(tb).split("\n", -1)) { + if (line.isEmpty()) { + continue; + } + if (!started) { + started = true; // "stack traceback:" 那一行本身 + continue; + } + if (line.charAt(0) == '\t') { + continue; // 回溯自己的一行 + } + return true; + } + return false; + } + + /** + * 与 {@link #assertMatches} 的差别:不剥回溯。 + *

+ * 剥回溯是给「脚本先打印若干行再崩」这类用例准备的兜底——本实现一度没有完整的 + * {@code ldebug.c},层数对不齐。但 {@code debug.traceback} 的返回值本身就是 + * 回溯,砍掉它就什么都不剩了,用例会退化成「两边都输出空串」的假绿。 + * 所以凡是回溯属于预期输出的用例都走这条。 + */ + static void assertFullMatches(String src) { + requireReference(); + String r = normalize(refRaw(src), false); + assertEquals(true, !r.isEmpty(), + "用例在参考实现上没有产生任何输出,等于没测。script:\n" + src); + assertEquals(r, normalize(mineRaw(src), false), "script:\n" + src); + } + + /** + * 断言一个文件在本实现与参考实现上的输出一致。 + *

+ * 与 {@link #assertFullMatches} 的差别是输入来自磁盘而不是字符串:上游自带的 + * 测试套件是一堆 {@code .lua} 文件,而且它们互相 {@code require} + * ({@code bitwise.lua} 要 {@code require "bwcoercion"}, + * {@code cstack.lua} 要 {@code require "tracegc"}),只能按文件跑。 + * + * @param dir 用例所在目录(同时是 {@code require} 的搜索起点与工作目录) + * @param name 文件名 + */ + static void assertFileMatches(File dir, String name) { + requireReference(); + String r = normalize(refFileRaw(dir, name), false); + assertEquals(true, !r.isEmpty(), + "用例在参考实现上没有产生任何输出,等于没测。file: " + name); + assertEquals(r, normalize(mineFileRaw(dir, name), false), + "file: " + name + "\n见 target/rt/" + name + ".ref 与 .mine"); + } + + /** + * 与 {@link #assertMatches} 的差别:stdin 由调用方喂,用来验 + * {@code debug.debug} 那条读 stdin 的循环。 + *

+ * 两边都喂同样的字节:参考实现重定向到临时文件,本实现设 {@code LuaState#in} + * (默认是空流,与 {@code /dev/null} 等价)。 + */ + static void assertMatchesWithStdin(String src, String stdin) { + requireReference(); + String r = normalize(refRawWithStdin(src, stdin), false); + assertEquals(true, !r.isEmpty(), + "用例在参考实现上没有产生任何输出,等于没测。script:\n" + src); + assertEquals(r, normalize(mineRawWithStdin(src, stdin), false), + "script:\n" + src); + } + + private static String refRawWithStdin(String src, String stdin) { + String out = ReferenceLua.runWithStdin(src.getBytes(StandardCharsets.UTF_8), + stdin.getBytes(StandardCharsets.UTF_8)).output; + String bin = ReferenceLua.find(); + if (bin != null) { + out = out.replace(new File(bin).getAbsolutePath() + ": ", ""); + } + return out; + } + + /** 只比对第一行(回溯与后续内容不可比时用) */ + static void assertFirstLineMatches(String src) { + requireReference(); + String r = ref(src); + assertEquals(true, !r.isEmpty(), + "用例在参考实现上没有产生任何输出,等于没测。script:\n" + src); + assertEquals(firstLine(r), firstLine(mine(src)), "script:\n" + src); + } + + /** + * 没有参考实现二进制就跳过,而不是报错。 + *

+ * 守卫放在这一处而不是每个测试类各写一个 {@code @BeforeAll}: + * 十个类里只有几个写了,漏掉的那几个(全是 {@code @ParameterizedTest}) + * 在没有 {@code lua5.4} 的机器上会把 {@code ./mvnw clean test} 变成红的—— + * 而 README 里写的构建命令就是它。JUnit 的 {@code assumeTrue} 在这里抛的是 + * {@code TestAbortedException},整条用例记成 skipped,正是想要的结果。 + */ + private static void requireReference() { + assumeTrue(ReferenceLua.find() != null, + "参考实现二进制不存在,跳过一致性测试;设 LUA54_BIN 或从源码构建到 " + + "target/lua54/src/lua。已找过:" + ReferenceLua.describe()); + } + + /** 在参考实现上跑一段脚本,返回归一化后的输出(剥回溯) */ + static String ref(String src) { + return normalize(refRaw(src), true); + } + + /** 在参考实现上跑一段脚本,返回原始输出 */ + private static String refRaw(String src) { + ReferenceLua.Result r = ReferenceLua.runBytes(src.getBytes(StandardCharsets.UTF_8), + CASE_PATH).result; + String out = r.output; + String bin = ReferenceLua.find(); + if (bin != null) { + /* + * 程序名是 {@code lua.c: l_message} 加的({@code "%s: "}), + * 每次打印错误都有。所以要全文替换而不是只去开头: + * stdout 与 stderr 是分别捕获后拼起来的,脚本先打印几行再崩时, + * 程序名出现在中间,不在开头。 + */ + out = out.replace(new File(bin).getAbsolutePath() + ": ", ""); + } + return out; + } + + /** + * 在本实现上跑一段脚本,返回归一化后的输出(剥回溯)。 + *

+ * 归一化留在这一层而不是抛给调用方:{@code FuzzTest} 之类的用例直接拿 + * {@code ref} 与 {@code mine} 比,让它们自己记着归一化迟早会漏一次—— + * 漏的那次表现为「只有地址不同」的假红。 + */ + static String mine(String src) { + return normalize(mineRaw(src), true); + } + + /** 在参考实现上跑一个文件:工作目录设为用例目录,参考实现才有 {@code ./?.lua} */ + private static String refFileRaw(File dir, String name) { + String out = ReferenceLua.runFile(new File(dir, name).getAbsolutePath(), dir).output; + String bin = ReferenceLua.find(); + if (bin != null) { + out = out.replace(new File(bin).getAbsolutePath() + ": ", ""); + } + return out; + } + + /** + * 只在本实现上跑一个文件,返回原始输出——不碰参考实现。 + *

+ * 给「输出不该逐字节比、但要保证能跑完」的那批用例用。它们要么打印随实现 + * 而变的数字({@code cstack.lua} 的 {@code final count}),要么依赖本实现 + * 没有的设施({@code gc.lua} 的增量收集器参数)。放进严格比对表会永远红, + * 完全不跑又会漏掉「栈上限被改坏导致死循环/OOM」这类回归—— + * 而这类回归真的发生过:{@code cstack.lua} 一度把 + * surefire 的 forked JVM 撑到 {@code Java heap space}, + * {@code db.lua} 一度死循环等一个永远不跑的 {@code __gc}。 + */ + static String mineFileOnly(File dir, String name) { + return mineFileRaw(dir, name); + } + + /** 在本实现上跑一个文件,返回原始输出 */ + private static String mineFileRaw(File dir, String name) { + File f = new File(dir, name); + byte[] bytes; + try { + bytes = Files.readAllBytes(f.toPath()); + } catch (java.io.IOException e) { + throw new AssertionError("读不到用例文件:" + f, e); + } + LuaState L = new LuaState(); + LuaLibs.openAll(L); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(bos, true, StandardCharsets.UTF_8); + L.out = ps; + L.err = ps; + LuaValue err; + try { + err = runFileLikeLuaC(L, dir, f.getAbsolutePath(), bytes); + } catch (LuaError e) { + return messageOf(e) + "\n"; + } + L.close(); + String stdout = latin1(bos.toByteArray()); + return (err == null) ? stdout : stdout + messageOf(err) + "\n"; + } + + /** + * 照 {@code lua.c} 的形状驱动一个文件。 + *

+ * 比 {@link #runLikeLuaC} 多一步:把用例目录前置到 {@code package.path}。 + * 参考实现靠的是「进程工作目录就是用例目录」—— {@code LUA_PATH_DEFAULT} 里的 + * {@code ./?.lua} 因此能找到 {@code bwcoercion}、{@code tracegc} 这些被 + * {@code require} 的同伴文件。JVM 的工作目录改不了(它是整个 Maven 进程共享 + * 的),所以只能在 {@code package.path} 上补一条绝对路径——输出不受影响, + * 这批用例里没有一个打印 {@code package.path}。 + */ + private static LuaValue runFileLikeLuaC(LuaState L, File dir, String absPath, byte[] bytes) { + LuaValue pkg = L.getGlobals().get(LuaString.latin1("package")); + if (pkg != null && pkg.tt == Lua.TTABLE) { + LuaTable t = pkg.asTable(); + LuaValue p = t.get(LuaString.latin1("path")); + if (p != null && p.isString()) { + t.set("path", LuaValue.ofString(LuaString.utf8( + dir.getAbsolutePath() + "/?.lua;" + p.asString().toJavaString()))); + } + } + int base = L.top; + L.push(LuaValue.ofFunction(Diff::pmainFile)); + L.push(LuaValue.ofString(LuaString.of(bytes))); + L.push(LuaValue.ofString(LuaString.utf8("@" + absPath))); + return L.pcall(base, 0); + } + + /** + * {@code lua.c: pmain} 跑文件的形态:chunk 名是 {@code @} 加路径。 + *

+ * 消息处理函数是压在栈上再交给 {@code lua_pcall} 的,形状照 + * {@code lua.c: docall}: + *

+     * int base = lua_gettop(L) - narg;      // 函数所在的下标
+     * lua_pushcfunction(L, msghandler);     // 压在 chunk 之上
+     * lua_insert(L, base);                  // 再插到 chunk 下面
+     * lua_pcall(L, narg, nres, base);
+     * 
+ * 这一步不只是「多占一格」:栈上因此多了一个槽,递归深度就少一层。上游 + * {@code errors.lua} 打印的「expected stack overflow after N calls」是 + * 照着参考实现那个数字断言的,少这一格就会多报一层。 + */ + private static int pmainFile(LuaState L) { + byte[] bytes = L.arg1(1).asString().bytes; + String chunk = L.arg1(2).asString().toJavaString(); + createArgTable(L, chunk.substring(1)); // 去掉 chunk 名前面的 '@' + int base = L.top; + L.pushClosure(L.load(bytes, chunk)); + int func = pushMsgHandler(L, base); + LuaValue err = L.pcall(func, 0, L.stack[base]); + if (err != null) { + throw new LuaError(err, ""); + } + return 0; + } + + /** + * {@code lua.c: docall} 那两行:把 {@code msghandler} 压到栈顶,再插到 + * {@code base} 处(chunk 下面)。 + * + * @return 插完之后 chunk 所在的下标 + */ + private static int pushMsgHandler(LuaState L, int base) { + L.push(LuaValue.ofFunction(Diff::msghandler)); + LuaValue tmp = new LuaValue(L.stack[base]); + L.stack[base].set(L.stack[base + 1]); + L.stack[base + 1].set(tmp); + return base + 1; + } + + /** + * 照 {@code lua.c} 的形状驱动:{@code main} 里 {@code lua_pcall(pmain)}, + * 脚本是 {@code pmain} 这个函数里面加载并调用的。 + *

+ * 这一层不是形式主义——{@code pmain} 本身是一个 C 函数帧,它会实实在在 + * 出现在栈上。实测 {@code debug.traceback()} 在参考实现里最后一行是 + * {@code [C]: in ?},{@code debug.getinfo(3).what == "C"},都是这一帧。 + * 少了它,回溯与层号就整体差一层,而 {@code debug} 库的用例会假红在 + * 一件跟 {@code debug} 库无关的事情上。 + *

+ * 至于引擎本身:宿主直接 {@code luaL_dostring} 时没有这一帧, + * {@code debug.traceback()} 也就不会多出那一行。所以这属于驱动方式 + * 的差异,不该靠改引擎去凑。 + */ + private static LuaValue runLikeLuaC(LuaState L, String src) { + int base = L.top; + L.push(LuaValue.ofFunction(Diff::pmain)); + L.push(LuaValue.ofString(LuaString.of(src.getBytes(StandardCharsets.UTF_8)))); + /* + * 第二个实参不是凑数的:{@code lua.c: main} 给 {@code pmain} 传的是 + * {@code (argc, argv)} 两个实参,栈的初始 {@code top} 因此是 3。 + * 只传一个就比参考实现少占一格,而少一格会一路带到底 —— + * 脚本栈溢出时的递归深度差一层,最深那一帧也就换了个函数, + * 报错行号随之不同(fuzz seed 7984 撞的就是这个:参考报第 67 行, + * 我们报第 65 行)。按帧大小铺开量过:补上这一格之后 + * 0/1/2/3/4/6/8/16/32 个局部变量的九种递归深度全部与参考一致。 + */ + L.push(LuaValue.ofInteger(2)); + return L.pcall(base, 0); + } + + /** + * {@code lua.c: createargtable}:脚本的命令行参数表 {@code arg}。 + *

+ * 它是 lua.c 造的,不属于任何库 —— 参考实现在 + * {@code luaL_openlibs} 之后、跑脚本之前建表,于是 {@code arg[0]} 是脚本 + * 路径、{@code arg[-1]} 是解释器自己的路径,正整数下标才是脚本参数。 + *

+ * 本实现还没有独立的解释器入口,但差分驱动整体就是在模仿 lua.c 的形状 + * ({@code pmain}、{@code msghandler}、栈上多占的那一格都是),少这一张表 + * 等于所有读 {@code arg} 的脚本都进不了比对。真实项目里 {@code arg[1]} + * 是命令行工具最常见的入口,值得为它留个位置。 + */ + private static void createArgTable(LuaState L, String script) { + String bin = ReferenceLua.find(); + LuaTable t = new LuaTable(); + /* + * {@code bin} 可能是 null:{@link #mineFileOnly} 那条路不要求参考 + * 实现存在(它是给「不该逐字节比、但要保证能跑完」的用例用的),而 + * {@code pmainFile} 照样会走到这里。直接 {@code new File(null)} 是 NPE, + * 而 NPE 不是 {@code LuaError} —— 它会把整个 fork 的 JVM 打红,症状是 + * 「一堆用例同时失败」,看不出来是谁的错。 + */ + t.set(-1L, LuaValue.ofString(LuaString.utf8( + (bin == null) ? "lua" : new File(bin).getAbsolutePath()))); + t.set(0L, LuaValue.ofString(LuaString.utf8(script))); + L.globals.set("arg", LuaValue.ofTable(t)); + } + + /** {@code lua.c: pmain}:加载并执行脚本 */ + private static int pmain(LuaState L) { + byte[] bytes = L.arg1(1).asString().bytes; + createArgTable(L, CASE_PATH); + // lua.c: docall —— 未捕获的错误会经过 msghandler,回溯就是它加的 + int base = L.top; + L.pushClosure(L.load(bytes, CHUNK)); + int func = pushMsgHandler(L, base); + LuaValue err = L.pcall(func, 0, L.stack[base]); + if (err != null) { + throw new LuaError(err, ""); + } + return 0; + } + + /** + * {@code lua.c: msghandler}:{@code docall} 给脚本装上的默认消息处理函数。 + *

+ * 不装它,脚本里未捕获的错误就只有一行消息,而参考实现还带着一整段 + * {@code luaL_traceback}——两个用例(一个是「先打印几行再崩」)会在 + * 「回溯之后的内容」上差一截,恰好又是被归一化抹掉的那截,于是没人发现。 + */ + private static int msghandler(LuaState L) { + LuaValue v = L.arg1(1); + // 字节串:错误对象可以是任意字节(error("\xff")),走 Java String 再 + // 编码回去会把非法字节换成 U+FFFD,回溯就没得比了 + LuaString msg; + if (v.isString()) { + msg = v.asString(); + } else if (callMeta(L, v, "__tostring") + && L.stack[L.top - 1].isString()) { + /* + * 看的是栈顶({@code __tostring} 的结果),不是第 1 个参数—— + * 第 1 个参数是错误对象本人,它当然不是字符串。看错位置的症状是 + * {@code error(setmetatable({}, {__tostring = f}))} 的未捕获错误 + * 打成了 {@code (error object is a table value)} 而不是 {@code f} 的 + * 结果。C 版这里是 {@code lua_type(L, -1) == LUA_TSTRING},看的就是 -1。 + */ + return 1; + } else { + msg = LuaString.latin1("(error object is a " + Lua.typeName(v.baseType()) + " value)"); + } + L.push(LuaValue.ofString(DebugLib.traceback(L, L, msg, 1))); + return 1; + } + + /** + * {@code lauxlib.c: luaL_callmeta}。 + *

+ * 只查对象自己的元表这一层(不走 {@code __index} 链),且调用是 + * 不保护的——{@code __tostring} 自己出错时错误会继续冒,正好被 + * {@code applyErrMsg} 那层兜成 {@code error in error handling}。 + */ + private static boolean callMeta(LuaState L, LuaValue v, String event) { + LuaTable mt = LuaTm.metatableOf(L, v); + if (mt == null) { + return false; + } + LuaValue tm = mt.get(LuaString.latin1(event)); + if (tm == null || tm.isNil()) { + return false; + } + int base = L.top; + L.push(tm); + L.push(new LuaValue(v)); + L.call(base, 1); + return true; + } + + /** + * 在本实现上跑一段脚本,返回原始输出。 + *

+ * 出错时不带程序名前缀:参考实现的 {@code lua.c} 直接写消息本身 + * ({@code lua_writestringerror("%s\n", msg)}),程序名是 shell 加的。 + */ + private static String mineRaw(String src) { + LuaState L = new LuaState(); + LuaLibs.openAll(L); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(bos, true, StandardCharsets.UTF_8); + L.out = ps; + // stderr 也指到同一条缓冲上:参考实现是合并捕获的 + L.err = ps; + return mineRawOn(L, bos, src); + } + + private static String mineRawWithStdin(String src, String stdin) { + LuaState L = new LuaState(); + LuaLibs.openAll(L); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(bos, true, StandardCharsets.UTF_8); + L.out = ps; + // stderr 也指到同一条缓冲上:参考实现是合并捕获的 + L.err = ps; + L.in = new java.io.ByteArrayInputStream(stdin.getBytes(StandardCharsets.UTF_8)); + return mineRawOn(L, bos, src); + } + + private static String mineRawOn(LuaState L, ByteArrayOutputStream bos, String src) { + LuaValue err; + try { + err = runLikeLuaC(L, src); + } catch (LuaError e) { + /* + * 加载期错误(词法/语法)在参考实现里也不可被 pcall 捕获: + * lua.c 加载失败就直接打印并退出。所以这里同样让它冒出来, + * 只是转成与参考实现同形的消息参与比对——否则一个生成器 bug + * 会变成看不懂的堆栈。 + */ + return messageOf(e) + "\n"; + } + /* + * 参考实现的 lua.c 在退出前会 lua_close,而 lua_close 会把还没跑的 __gc + * 全跑一遍。不补这一步,「不调 collectgarbage 的 __gc 用例」两边就差一截 + * 输出——而且差的是脚本结束之后才打印的那部分,很容易误判成 + * 「__gc 根本没实现」。 + */ + L.close(); + String stdout = latin1(bos.toByteArray()); + /* + * 出错时不能把 stdout 丢掉。参考实现的 stdout 与 stderr 被合并 + * 捕获,所以「先打印十几行、然后崩」的脚本,参考输出是那十几行加错误 + * 消息;只返回消息就会把这类用例整片判红——而且红得毫无道理, + * 因为 VM 本身是对的。这个坑是 fuzzer 抓出来的。 + */ + if (err == null) { + return stdout; + } + return stdout + messageOf(err) + "\n"; + } + + private static String messageOf(LuaValue err) { + if (err.isString()) { + // 与捕获输出同一套解码,否则消息里的非 UTF-8 字节会被换成 U+FFFD + return latin1(err.asString().bytes); + } + return "(error object is a " + err.typeName() + " value)"; + } + + /** + * 字节摊成字符:Latin-1 是字节↔字符的一一映射,比对因此是逐字节的。 + *

+ * 按 UTF-8 解码的话非法字节会变成 U+FFFD,而参考实现那边的同一批字节也变成 + * U+FFFD —— 于是「一边 0x80、另一边 0xFF」被判成一致。差分闸门对整片非 + * UTF-8 输出是瞎的,这本身就是一类假绿。 + */ + private static String latin1(byte[] b) { + return new String(b, StandardCharsets.ISO_8859_1); + } + + private static String messageOf(LuaError e) { + return (e.value == null) ? "" : messageOf(e.value); + } + + private static String normalize(String s, boolean stripTraceback) { + if (stripTraceback) { + int tb = s.indexOf("stack traceback:"); + if (tb >= 0) { + s = s.substring(0, tb); + } + } + s = s.replaceAll("0x[0-9a-fA-F]+", "0xADDR"); + /* + * NaN 的符号:libc 说了算,两边不一致(约定 #96)。 + *

+ * x86 的「默认 NaN」符号位是 1,所以 C 里 {@code 0/0}、{@code sqrt(-1)} + * 得到的那个 NaN 是负的,glibc 的 {@code printf("%.14g")} 打 + * {@code -nan}({@code %+f} 则是 {@code +nan});Apple libc 一律打 + * {@code nan}。C99 把 NaN 的符号留作 unspecified,两边都合法。 + *

+ * 这一条出现在所有会打印 NaN 的用例里({@code tostring}、 + * {@code print}、{@code string.format}、{@code warn}、数学库返回值、 + * 差分 fuzz),没有「局部挪出差分表」这个选项,只能归一化后比对。 + * 归一化的是 两边同一种写法,不是把差异抹掉:{@code nan} 与 + * {@code -inf} 之类的真差异照样会红。 + */ + s = s.replace("-nan", "nan").replace("-NAN", "NAN"); + s = s.replace("+nan", "nan").replace("+NAN", "NAN"); + /* + * {@code math.fmod} / {@code package.searchpath} 报错的参数序号: + * 编译器说了算,两边不一致。 + *

+ * 这两处是 C 里唯一「同一个调用表达式里塞了两个 {@code luaL_check*}」 + * 的地方: + *

+         * l_mathop(fmod)(luaL_checknumber(L, 1), luaL_checknumber(L, 2));
+         * searchpath(L, luaL_checkstring(L, 1), luaL_checkstring(L, 2), ...);
+         * 
+ * 而 C 的实参求值顺序是 unspecified:GCC 从右往左(先查 #2), + * clang 从左往右(先查 #1)。于是 {@code math.fmod(nil, 1)} 在 + * ubuntu(GCC 编的参考实现)上报 {@code bad argument #2 ... got no + * value}(#2 根本不存在),在 macOS(clang 编的)上报 + * {@code bad argument #1 ... got nil}。 + * 其余函数两个 {@code check} 都在独立语句里,顺序是源码顺序, + * 不受影响——所以只有这两个名字需要归一化。 + *

+ * 归一的是「序号 + 描述的是哪个参数」,连 {@code (number expected, + * got X)} 一起归——因为报的是不同参数,那个 {@code got X} + * 描述的压根不是同一个东西,没法只归序号。剩下的(有没有报错、 + * 函数名、{@code number expected} 之外的消息)两边照比。 + */ + s = s.replaceAll( + "bad argument #\\d+ to '(?:math\\.)?(fmod)' \\([^)]*\\)", + "bad argument #? to '$1' (...)"); + return s.replaceAll( + "bad argument #\\d+ to '(?:package\\.)?(searchpath)' \\([^)]*\\)", + "bad argument #? to '$1' (...)"); + } + + static String firstLine(String s) { + int i = s.indexOf('\n'); + return (i < 0) ? s : s.substring(0, i); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/DumpTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/DumpTest.java new file mode 100644 index 0000000..da9d44d --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/DumpTest.java @@ -0,0 +1,577 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.Test; + +import java.io.File; + +/** + * {@code string.dump} 与二进制 chunk 的差分用例 —— 阶段 8。 + *

+ * 这里比 {@link LoadTest} 更进一步,把 {@code ldump.c} / {@code lundump.c} + * 这一对也拉进闸门,而且用两种不同粒度的比法: + *

    + *
  • 运行期:脚本的 stdout 与参考实现逐字节比({@link Diff});
  • + *
  • 产物:{@code string.dump} 出来的字节本身与参考实现逐字节比。
  • + *
+ * 第二种是这一阶段的硬闸门——它同时校验了编译产物(常量顺序、行号表、 + * 局部变量与 upvalue 的调试信息、{@code maxstacksize})与序列化格式。 + * 只有第一种的话,「编出来的东西不对、但恰好能跑」是漏得过去的。 + *

+ * 盯住的几处: + *

    + *
  • {@code string.dump} 只吃 Lua 函数:不给参数报 + * 「function expected」,给 C 函数报「unable to dump given function」—— + * 两条消息来自两个不同的检查,不能合并;
  • + *
  • {@code strip} 剥掉行号与源名,于是错误不再带位置前缀;
  • + *
  • 加载出来的函数,第 0 号 upvalue 会被 {@code load} 换成 {@code _ENV}。 + * 这是 C 版的既有行为,不是 bug:dump 一个带非 {@code _ENV} upvalue 的 + * 函数再 load 回来,那个 upvalue 就变成了全局表。
  • + *
+ */ +class DumpTest { + + @BeforeAll + static void needReference() { + assumeTrue(ReferenceLua.available(), + "no lua5.4 reference binary; set LUA54_BIN"); + } + + @TestFactory + List dumpScriptsMatchReference() { + List tests = new ArrayList<>(); + add(tests, "dump", DUMP); + add(tests, "err", ERRORS); + add(tests, "bad", BAD); + return tests; + } + + private static void add(List tests, String tag, String[] srcs) { + for (int i = 0; i < srcs.length; i++) { + String src = srcs[i]; + int n = i; + tests.add(DynamicTest.dynamicTest(tag + "[" + n + "]", () -> Diff.assertMatches(src))); + } + } + + /** + * 手工构造二进制 chunk 的公共前奏。 + *

+ * 头部直接取自 {@code string.dump(function() end)} 的前 31 个字节 + * (4 签名 + 1 版本 + 1 格式 + 6 校验 + 3 尺寸 + 8 整数 + 8 浮点)—— + * 比在测试里硬写一串转义更不容易抄错,也顺带把头部长度钉住。 + */ + private static final String HAND = """ + local HDR = string.dump(function() end):sub(1, 31) + -- 变长无符号数:7 位一组、大端在前,最后一组最高位置 1 + local function sz(n) + if n == 0 then return "\\128" end + local t = {} + while true do + t[#t + 1] = n % 128 + n = n // 128 + if n == 0 then break end + end + t[1] = t[1] + 128 + local r = {} + for i = #t, 1, -1 do r[#r + 1] = string.char(t[i]) end + return table.concat(r) + end + -- 拼一个「只有常量表、其它全空」的函数 + local function chunk(consts) + local s = "\\0" .. sz(0) .. sz(0) .. sz(0) -- nupvals, source, 行号范围 + .. "\\0\\0\\0" -- numparams, is_vararg, maxstacksize + .. sz(0) -- code + .. sz(#consts) + for i = 1, #consts do s = s .. consts[i] end + s = s .. sz(0) -- upvalues + .. sz(0) -- protos + .. sz(0) .. sz(0) .. sz(0) .. sz(0) -- 调试信息四段 + return HDR .. s + end + """; + + + /* ==================== 运行期 ==================== */ + + private static final String[] DUMP = { + """ + local f = function(a) return a * 2 end + local s = string.dump(f) + print(type(s), #s > 0) + print(load(s)(21)) + print(pcall(load, s, nil, "t")) + print(load(s, nil, "b") ~= nil) + """, + + """ + -- strip 剥掉行号与源名:错误不再带位置前缀 + local f = function() error("boom") end + print(pcall(f)) + local g = load(string.dump(f)) + print(pcall(g)) + local h = load(string.dump(f, true)) + print(pcall(h)) + """, + + """ + -- 内嵌函数、各类常量、长字符串都要能往返 + local f = function(a) + local function g(b) return b * 3 end + return g(a) .. "x" .. 1.5 .. tostring(true) .. tostring(nil) .. #string.rep("z", 100) + end + print(f(2)) + print(load(string.dump(f))(2)) + """, + + """ + -- 第 0 号 upvalue 会被 load 换成 _ENV:原 upvalue 变成全局表 + local up = 10 + local f = function(a) return a + up end + print(f(1)) + local g = load(string.dump(f)) + print(pcall(g, 1)) + """, + + """ + -- 主 chunk 也能 dump(它的 sizeupvalues 就是 1) + local m = load("return 7") + print(m()) + print(load(string.dump(m))()) + """, + + """ + -- 二进制 chunk 落盘再由 loadfile / dofile 读回来 + local p = "/tmp/fkdump_f1.luac" + local f = io.open(p, "wb") + f:write(string.dump(function() return 99 end)) + f:close() + print(loadfile(p)()) + print(dofile(p)) + print(pcall(loadfile, p, "t")) + os.remove(p) + """, + + """ + -- 同一个 proto 被多个闭包共享时 dump 出来的仍然只有一份 + local t = {} + for i = 1, 3 do t[i] = function() return i end end + local s = string.dump(t[1]) + print(#s) + print(load(s)()) + """, }; + + private static final String[] ERRORS = { + """ + print(pcall(string.dump)) + print(pcall(string.dump, print)) + print(pcall(string.dump, 42)) + print(pcall(string.dump, "s")) + print(pcall(string.dump, function() return 1 end, "x")) + print(#string.dump(function() return 1 end, false) > 0) + """, + + """ + -- 剥完之后源名也没了,于是 chunk 名是什么都不影响报错 + local f = function() error("bare") end + print(pcall(load(string.dump(f, true), "=myname"))) + print(pcall(load(string.dump(f), "=myname"))) + """, }; + + private static final String[] BAD = { + """ + -- 截断到各个位置,报的都是 truncated chunk + local s = string.dump(function() return 1 end) + print(load(s:sub(1, 10))) + print(load(s:sub(1, 20))) + print(load(s:sub(1, #s - 1))) + """, + + """ + -- 头部各字段逐个错:版本、格式、校验串 + print(load("\\27Lua\\1\\0")) + print(load("\\27Lua\\84\\1")) + print(load("\\27Lua\\84\\0\\25\\147\\r\\n\\26\\n\\4\\8\\8")) + print(load("\\27Lua\\84\\0\\25\\147\\r\\n\\26\\n\\4\\8\\8\\120\\86\\0\\0\\0\\0\\0\\0\\0")) + """, + + """ + -- 首字节是签名、但后面压根不是 Lua:走 undump 而不是词法 + print(load("\\27Lua")) + print(load("\\27Lua", "=myname")) + print(load("\\27", nil, "b")) + """, + + // 手工拼出来的 chunk:用来打那些「正常 dump 永远产不出来」的分支 + HAND + + """ + -- 变长长度编码溢出:10 个 \\127 还不够,第 11 个才顶到 limit + print(load(HDR .. string.rep("\\127", 10))) + print(load(HDR .. string.rep("\\127", 11))) + """, + + HAND + + """ + -- 常量类型字节写成一个不存在的类型:C 的 lua_assert 在默认构建下 + -- 是空操作,于是那个常量就停在 nil 上,chunk 照样加载成功 + print(type(load(chunk({ "\\99" })))) + -- 而类型合法、长度却是 0 的字符串常量要报错 + print(load(chunk({ "\\4" .. sz(0) }))) + """, + + HAND + + """ + -- 长字符串常量(> LUAI_MAXSHORTLEN,不驻留)走的是另一条分支 + local body = string.rep("z", 100) + print(type(load(chunk({ "\\4" .. sz(#body + 1) .. body })))) + """, }; + + /* ==================== 产物逐字节比 ==================== */ + + private static final String REF_BIN = "target/rt/dump_ref.bin"; + private static final String MINE_BIN = "target/rt/dump_mine.bin"; + + @Test + void dumpBytesMatchReference() throws Exception { + assertDumpBytes("simple", "function(a) return a * 2 end", false); + assertDumpBytes("simple-strip", "function(a) return a * 2 end", true); + assertDumpBytes("nested", "function(a) local function g(b) return b + a end return g(a) end", + false); + assertDumpBytes("consts", + "function() return 1, 1.5, \"short\", string.rep(\"z\", 100), true, nil end", false); + assertDumpBytes("vararg", "function(...) local a, b = ... return a, b, select('#', ...) end", + false); + assertDumpBytes("loop", "function(n) local s = 0 for i = 1, n do s = s + i end return s end", + false); + assertDumpBytes("upval", "function() local x = 1 return function() return x end end", false); + assertDumpBytes("goto", "function(n) for i = 1, n do if i == 3 then goto cont end ::cont:: end end", + false); + // 常量字符串超过 LUAI_MAXSHORTLEN:dump 与 undump 都走长字符串那条路 + assertDumpBytes("longstr", "function() return \"" + "z".repeat(100) + "\" end", false); + } + + /** + * 两边各自把 {@code string.dump(f)} 写到文件,再比这两个文件。 + *

+ * 两个脚本必须从同一个路径跑。dump 里存着源名,源名就是 chunk 名, + * 两边路径不同则产物必然不同,比出来的「差异」全是路径造成的噪声。 + * 所以先跑参考实现写 {@link #REF_BIN},再跑本实现写 {@link #MINE_BIN}, + * 中间复用同一个 {@link Diff#CASE_PATH}。 + */ + private static void assertDumpBytes(String name, String funcExpr, boolean strip) throws Exception { + String script = "local f = io.open(\"%s\", \"wb\")\n" + + "f:write(string.dump(%s, %s))\n" + + "f:close()\n"; + ReferenceLua.Result r = ReferenceLua.runBytes( + String.format(script, REF_BIN, funcExpr, strip).getBytes(StandardCharsets.UTF_8), + Diff.CASE_PATH).result; + assertEquals("", r.output, "参考实现跑 dump 用例 " + name + " 时就出错了"); + String mine = Diff.mine(String.format(script, MINE_BIN, funcExpr, strip)); + assertEquals("", mine, "本实现跑 dump 用例 " + name + " 时就出错了"); + assertArrayEquals(Files.readAllBytes(Paths.get(REF_BIN)), + Files.readAllBytes(Paths.get(MINE_BIN)), + "string.dump 的产物与参考实现不一致:" + name); + } + + private static final String GRID_BIN = "target/rt/dumpgrid.bin"; + + /** + * 字节对撞的全语法语料(约 360 条函数 × {@code strip} 真/假)。 + *

+ * 现有 {@link #dumpBytesMatchReference} 只比了 9 个手写函数,覆盖不到 + * 编译器里那些「只有规模上去了才走到的分支」。这里机械铺开: + *

    + *
  • 常量:整数(含 {@code 2^63-1}、{@code -2^63})、浮点 + * (含 {@code -0.0}、次正规 {@code 0x1p-1074})、字符串 + * (长度跨 {@code LUAI_MAXSHORTLEN} 的 39/40/41 与 70000);
  • + *
  • 运算符:18 个二元 + 4 个一元 + 拼接链(1..60 项);
  • + *
  • 控制流:{@code if}/{@code while}/{@code repeat}/数字 + * {@code for}/泛型 {@code for}/{@code break}/{@code goto};
  • + *
  • 闭包:1..60 个 upvalue、{@code } 与 {@code };
  • + *
  • 规模边界:{@code if} 体指令数到 140000({@code OP_JMP} + * 的 {@code sBx} 是 18 位有符号,超过 {@code 2^17} 就溢出)、 + * 嵌套 200/210 层(C 栈溢出)、upvalue 与参数的 255/256 上限、 + * 常量表 255/256/20000、{@code SETLIST} 的 50/51 批量;
  • + *
  • 行号:跨 {@code MAXIWTHABS} 的 300 行空行与 200 个局部变量。
  • + *
+ * 规模上限触发的语法错误本身也是差分目标({@code too many local + * variables (limit is 200)}、{@code function or expression needs too many + * registers}),所以脚本的 stdout 也一并比。 + *

+ * 产物写成「8 字节长度 + 内容」的记录流,便于失败时直接指出是第几条。 + */ + @Test + void dumpBytesGridMatchReference() throws Exception { + new File("target/rt").mkdirs(); + // 两边都从 CASE_PATH 跑:dump 里存着源名,源名就是 chunk 名 + String refOut = Diff.ref(GRID_SCRIPT); + byte[] ref = Files.readAllBytes(Paths.get(GRID_BIN)); + String mineOut = Diff.mine(GRID_SCRIPT); + byte[] got = Files.readAllBytes(Paths.get(GRID_BIN)); + assertEquals(refOut, mineOut, "dump 网格脚本自身的输出不一致"); + int at = firstDifferingRecord(ref, got); + if (at >= 0) { + org.junit.jupiter.api.Assertions.fail( + "string.dump 的产物与参考实现不一致:第 " + at + " 条记录(0 起," + + "每条函数两条:strip=false / true)"); + } + } + + /** 顺着「8 字节小端长度 + 内容」走记录流,返回第一条不同的下标;全同给 -1 */ + private static int firstDifferingRecord(byte[] a, byte[] b) { + int i = 0; + int n = 0; + while (i + 8 <= a.length && i + 8 <= b.length) { + long la = 0; + long lb = 0; + for (int k = 7; k >= 0; k--) { + la = (la << 8) | (a[i + k] & 0xFF); + lb = (lb << 8) | (b[i + k] & 0xFF); + } + i += 8; + if (la != lb || la < 0 || la > Integer.MAX_VALUE) { + return n; + } + int len = (int) la; + if (i + len > a.length || i + len > b.length) { + return n; + } + for (int k = 0; k < len; k++) { + if (a[i + k] != b[i + k]) { + return n; + } + } + i += len; + n++; + } + return (a.length == b.length) ? -1 : n; + } + + private static final String GRID_SCRIPT = """ + +-- string.dump 字节对撞:全语法语料 x(strip 真/假) +-- 每个条目都是一段「返回 Lua 函数」的源码;dump 出来的字节必须与参考实现 +-- 逐字节相同 —— 这是比「跑得对」更硬的断言(连带钉住 Parser/Codegen/Proto)。 +local FS = {} +local function add(e) FS[#FS + 1] = e end + +-- 常量 +for _, v in ipairs({ "0", "1", "-1", "127", "128", "255", "32767", "65535", + "2147483647", "2147483648", "-2147483648", "9007199254740993", + "-9007199254740993", "0x7fffffffffffffff", "-0x8000000000000000", + "0x10", "0X1p4" }) do + add("function() return " .. v .. " end") +end +for _, v in ipairs({ "0.0", "-0.0", "1.5", "-1.5", "1e300", "1e-300", ".5", "5.", + "1e15", "1e16", "3.14159265358979", "0x1p-1074", "0x1.fffffffffffffp+1023" }) do + add("function() return " .. v .. " end") +end +for _, n in ipairs({ 0, 1, 39, 40, 41, 42, 100, 255, 256 }) do + add("function() return '" .. string.rep("z", n) .. "' end") +end +add("function() return '' end") +add("function() return '\\\\0' end") +add("function() return '\\\\255\\\\254' end") +add("function() return nil end") +add("function() return true, false end") + +-- 运算符 +for _, op in ipairs({ "+", "-", "*", "/", "//", "%", "^", + "==", "~=", "<", "<=", ">", ">=", "&", "|", "~", "<<", ">>" }) do + add("function(a, b) return a " .. op .. " b end") +end +for _, u in ipairs({ "-", "not ", "#", "~" }) do add("function(a) return " .. u .. "a end") end +for _, n in ipairs({ 1, 2, 3, 10, 60 }) do + local t = {} + for i = 1, n do t[i] = "a" end + add("function(a) return " .. table.concat(t, " .. ") .. " end") +end +add("function(a) return a .. 1 .. 1.5 .. 'x' end") + +-- 表构造 +add("function() return {} end") +add("function() return {1, 2, 3} end") +add("function() return {x = 1, y = 2} end") +add("function() return {1, 2, x = 3, [10] = 4} end") +add("function() return {[1] = 1, [2] = 2, 3} end") +add("function(f, g) return {f(), g()} end") +add("function(...) return {...} end") +add("function(...) return {1, ...} end") +do + local t, u = {}, {} + for i = 1, 60 do t[i] = tostring(i) end + for i = 1, 30 do u[i] = "[" .. i .. "]=" .. i end + add("function() return {" .. table.concat(t, ",") .. "} end") + add("function() return {" .. table.concat(u, ",") .. "} end") +end + +-- 调用 +for _, e in ipairs({ "function(f) return f() end", "function(f, a) return f(a) end", + "function(f, a, b) return f(a, b) end", "function(t) return t:m() end", + "function(t, a) return t:m(a) end", + "function(f) local r = f() return r end", + "function(...) return select('#', ...) end", + "function(...) return select(2, ...) end", + "function(...) local a, b = ... return a, b end", + "function(f, ...) return f(...) end", "function(a) return (a) end", + "function(a, b) return a and b end", "function(a, b) return a or b end", + "function(a, b, c) return a and b or c end", + "function(a, b) return a and b or a or b end" }) do + add(e) +end + +-- 控制流 +for _, e in ipairs({ "function(a) if a then return 1 end end", + "function(a) if a then return 1 else return 2 end end", + "function(a, b) if a then return 1 elseif b then return 2 else return 3 end end", + "function(n) local s = 0 while s < n do s = s + 1 end return s end", + "function(n) local s = 0 repeat s = s + 1 until s >= n return s end", + "function(n) local s = 0 for i = 1, n do s = s + i end return s end", + "function(n) local s = 0 for i = 1, n, 2 do s = s + i end return s end", + "function(n) local s = 0 for i = n, 1, -1 do s = s + i end return s end", + "function(t) local s = 0 for k, v in pairs(t) do s = s + 1 end return s end", + "function(t) local s = 0 for i, v in ipairs(t) do s = s + v end return s end", + "function(n) for i = 1, n do if i == 3 then break end end return n end", + "function(n) for i = 1, n do if i == 3 then goto cont end ::cont:: end end", + "function(n) do goto done end ::done:: end", + "function(n) while true do if n then goto out end end ::out:: return n end", + "function(a, b, c) if a then if b then if c then return 1 end end end end" }) do + add(e) +end + +-- 闭包 / upvalue +add("function() local x = 1 return function() return x end end") +add("function() local x, y = 1, 2 return function() return x + y end end") +add("function() local t = {} for i = 1, 3 do t[i] = function() return i end end return t end") +add("function(a) return function(b) return function(c) return a + b + c end end end") +for _, n in ipairs({ 1, 2, 5, 20, 60 }) do + local ups, ret = {}, {} + for i = 1, n do ups[i] = "local u" .. i .. " = " .. i; ret[i] = "u" .. i end + add("function() " .. table.concat(ups, " ") .. " return function() return " + .. table.concat(ret, "+") .. " end end") +end +add("function() local x = 1 return function() return x end end") +add("function() local x = nil return function() return x end end") + +-- 规模:局部变量 / 常量 / 参数 +for _, n in ipairs({ 1, 20, 60, 200, 250, 260 }) do + local locs, ret = {}, {} + for i = 0, n - 1 do locs[i + 1] = "local v" .. i .. " = " .. i; ret[i + 1] = "v" .. i end + add("function() " .. table.concat(locs, " ") .. " return " .. table.concat(ret, "+") .. " end") +end +for _, n in ipairs({ 1, 100, 250, 260, 300 }) do + local t = {} + for i = 0, n - 1 do t[i + 1] = tostring(i) end + add("function() return " .. table.concat(t, ",") .. " end") +end +for _, n in ipairs({ 0, 1, 5, 20, 100, 200 }) do + local ps, ret = {}, {} + for i = 0, n - 1 do ps[i + 1] = "p" .. i; ret[i + 1] = "p" .. i end + add("function(" .. table.concat(ps, ",") .. ") return " + .. (n > 0 and table.concat(ret, "+") or "nil") .. " end") +end +add("function(...) return ... end") + +-- 行号:跨 MAXIWTHABS 的大间距 +add("function()\\n" .. string.rep("\\n", 300) .. "return 1\\nend") +do + local b = { "function()" } + for i = 0, 199 do b[#b + 1] = "local a" .. i .. " = " .. i end + b[#b + 1] = "return a199\\nend" + add(table.concat(b, "\\n")) +end + +-- 语法糖与边角 +for _, e in ipairs({ "function(a) return a.x.y end", + "function(a) a.x = 1 a[1] = 2 a['k'] = 3 return a end", + "function() local a; a = 1; return a end", + "function() local a, b, c = 1, 2, 3 return a, b, c end", + "function(a) local b = a return b end", + "function() return function() return function() return 1 end end end", + "function() do end do end return 1 end", + "function() while false do end return 1 end", + "function() for i = 1, 0 do end return 1 end", + "function() ::a:: ::b:: ::c:: return 1 end" }) do + add(e) +end + +-- 规模边界:跳转偏移(sBx 是 18 位有符号)、深度嵌套、upvalue/参数上限 +for _, n in ipairs({ 100, 1000, 60000, 70000, 140000 }) do + add("function(c) if c then " .. string.rep("a = 1 ", n) .. "end end") +end +for _, n in ipairs({ 100, 1000, 60000, 70000 }) do + add("function(c) local i = 0 while i < 1 do " .. string.rep("a = 1 ", n) .. "i = i + 1 end end") +end +for _, n in ipairs({ 1000, 40000 }) do + add("function() for i = 1, 1 do " .. string.rep("a = 1 ", n) .. "end end") +end +for _, n in ipairs({ 10, 100, 190, 200, 210 }) do + add("function() " .. string.rep("do ", n) .. string.rep("end ", n) .. "end") + add("function(a) " .. string.rep("if a then ", n) .. string.rep("end ", n) .. "end") +end +for _, n in ipairs({ 255, 256, 1000, 20000 }) do + local t = {} + for i = 1, n do t[i] = tostring(i) end + add("function() return " .. table.concat(t, ",") .. " end") +end +for _, n in ipairs({ 100, 254, 255, 256 }) do + local ups, ret = {}, {} + for i = 1, n do ups[i] = "local u" .. i .. " = " .. i; ret[i] = "u" .. i end + add("function() " .. table.concat(ups, " ") .. " return function() return " + .. table.concat(ret, "+") .. " end end") +end +for _, n in ipairs({ 100, 200, 201 }) do + local ps, ret = {}, {} + for i = 1, n do ps[i] = "p" .. i; ret[i] = "p" .. i end + add("function(" .. table.concat(ps, ",") .. ") return " .. table.concat(ret, "+") .. " end") +end +for _, n in ipairs({ 39, 40, 41, 1000, 70000 }) do + add("function() return '" .. string.rep("z", n) .. "' end") +end +for _, n in ipairs({ 100, 40000 }) do + add("function() do goto done end " .. string.rep("a = 1 ", n) .. "::done:: end") +end +for _, n in ipairs({ 1, 50, 51, 100, 2000 }) do + local t = {} + for i = 1, n do t[i] = tostring(i) end + add("function() return {" .. table.concat(t, ",") .. "} end") +end + +-- 每条 dump 成「长度前缀 + 内容」的记录,便于定位哪一条不一致 +local out = assert(io.open("target/rt/dumpgrid.bin", "wb")) +local n = 0 +for i = 1, #FS do + local f, err = load("return " .. FS[i]) + if not f then + print("LOADFAIL " .. i .. " " .. tostring(err)) + else + local ok, fn = pcall(f) + if not ok then + print("RUNFAIL " .. i .. " " .. tostring(fn)) + else + for _, strip in ipairs({ false, true }) do + local d = string.dump(fn, strip) + out:write(string.pack(" + * 起因是「消息处理函数的生效点」那个洞(约定 #87):它说明「错误怎么冒出去」 + * 这条线铺得还不够。这里把错误对象的类型与抛出上下文做笛卡尔积, + * 再单独铺一张处理函数递归深度的网格。 + *

+ * 片一({@code errgrid},413 行):16 种错误对象 × 五种上下文。 + *

    + *
  • {@code A}:{@code error(obj, level)},{@code level} 取 + * 0/1/2/3/99(0 与 99 都不加位置前缀,2/3 落在不同的帧上);
  • + *
  • {@code B}:{@code xpcall} 处理器的四种返回形态(拼 + * {@code tostring}、原样返回、自己抛、返回多值只取第一个);
  • + *
  • {@code C}:协程四路({@code resume} / {@code wrap} / + * yield 之后再抛 / {@code status});
  • + *
  • {@code D}:从 C 函数里抛 —— {@code __index} / {@code __add} / + * {@code table.sort} 的比较函数 / {@code gsub} 的回调 / {@code __pairs} / + * {@code print} 里的 {@code __tostring} / {@code __close};
  • + *
  • {@code E}:嵌套保护区({@code xpcall} 套 {@code pcall} 及其反向、 + * 两层 {@code xpcall}、{@code xpcall} 里 {@code resume});
  • + *
  • {@code F}:行钩子与调用钩子里抛(约定 #31:抛错能否被 {@code pcall} + * 接住取决于 {@code sethook} 在哪调)。
  • + *
+ * 错误对象里特意放了三个「{@code __tostring} 不正常」的表:返回非串、自己抛、 + * 返回数字。它们把「{@code tostring} 在处理函数里抛错」这条路径逼了出来。 + *

+ * 这一片咬出来的是本阶段最实的一个 bug:处理函数自己出错时,C 版是 + * 重新进一次 {@code luaG_errormsg}——递归地把新的错误对象交给 + * 同一个处理函数,而不是直接失败。本实现用「重入就判死」顶替, + * 于是把「第二次调用其实会成功」的情形一起吞成了 + * {@code "error in error handling"}: + *

+ * 参考:false  string:case.lua:73: '__tostring' must return a string
+ * 本实现:false  error in error handling
+ * 
+ *

+ * 片二({@code errdepth},105 行):处理函数连续出错 {@code K} 次后才成功, + * {@code K} 从 0 铺到 400。这一片把两个临界点钉死: + *

    + *
  • {@code K=196}:第 200 层 {@code luaE_checkcstack} 抛 + * {@code C stack overflow},这个消息又被交给处理函数,于是成功那次 + * 拿到的是 {@code done:C stack overflow};
  • + *
  • {@code K=215}:累到 220 层({@code MAXCCALLS / 10 * 11})转 + * {@code LUA_ERRERR},落成 {@code error in error handling}。
  • + *
+ * 两处都曾经差一格 —— {@code luaD_call} 的 {@code L->nCcalls -= inc} 被 + * longjmp 跳过了,而本实现用 {@code finally} 减回去了。另外片二还带 + * 「处理函数里 yield」({@code callnoyield})与「处理函数里再 {@code pcall}」 + * 两小组。 + *

+ * 写法上两条必须照做(约定 #42、#28): + * {@code show} 里先把 {@code 0x} 地址归一再量长度(C 的 {@code %p} 与 + * JVM 的哈希位数不同,先取 {@code #} 会把这份无关差别算进断言); + * {@code tostring} 一律包 {@code pcall}({@code __tostring} 返回非串时它自己会抛 + * {@code "'__tostring' must return a string"},不包就整个探针崩掉)。 + */ +class ErrorPropagationTest { + + @BeforeAll + static void prepare() { + new File("target/rt").mkdirs(); + } + + /** 片一:错误对象 x 传播上下文(413 行)。 */ + @Test + void errorObjectByContextGridMatchesReference() { + Diff.assertMatches(""" + +-- 错误对象 x 传播上下文 全网格 +-- 起因:msghandler 生效点那个洞(约定 #87)说明「错误怎么冒出去」这条线 +-- 铺得还不够。这里把错误对象的类型与抛出上下文做笛卡尔积。 +local HEX = "0123456789ABCDEF" +local function hb(b) return HEX:sub((b // 16) + 1, (b // 16) + 1) .. HEX:sub((b % 16) + 1, (b % 16) + 1) end +local function esc(s) + return (s:gsub(".", function(c) + local b = c:byte() + if b == 91 or b == 93 then return "<" .. hb(b) .. ">" end + if b < 32 or b > 126 then return "<" .. hb(b) .. ">" end + return c + end)) +end +local function show(v) + local t = type(v) + if t == "string" then + -- 约定 #42:地址先归一再量长度。C 的 %p 与 JVM 的哈希位数不同, + -- 先取 # 会把这份无关差别算进断言里。 + local s = v:gsub("0x%x+", "PTR") + return "#" .. #s .. "[" .. esc(s) .. "]" + elseif t == "number" then + if math.type(v) == "integer" then return "i" .. tostring(v) end + return "f" .. tostring(v) + elseif t == "nil" or t == "boolean" then return tostring(v) end + -- 约定 #42:地址一律归一成类型。 + -- tostring 要包起来:__tostring 返回非字符串时它自己会抛 + -- "'__tostring' must return a string",而那会把整个探针崩掉。 + local ok, s = pcall(tostring, v) + if not ok or type(s) ~= "string" then return t .. "()" end + return t .. "(" .. s:gsub("0x%x+", "PTR") .. ")" +end +local function P(tag, fn) + local r = table.pack(pcall(fn)) + local out = tag + for i = 1, r.n do out = out .. " | " .. show(r[i]) end + print(out .. " | n=" .. r.n) +end + +local OBJS = { + { "nil", "nil" }, + { "false", "false" }, + { "true", "true" }, + { "int", "42" }, + { "float", "1.5" }, + { "str", "'msg'" }, + { "strNUL", "'a\\\\0b'" }, + { "strHi", "'\\\\255\\\\254'" }, + { "empty", "''" }, + { "tbl", "{}" }, + { "tblTostr", "setmetatable({}, {__tostring = function() return 'TS' end})" }, + { "tblTostrBad", "setmetatable({}, {__tostring = function() return {} end})" }, + { "tblTostrErr", "setmetatable({}, {__tostring = function() error('in ts') end})" }, + { "tblTostrNonStr", "setmetatable({}, {__tostring = function() return 42 end})" }, + { "fn", "print" }, + { "co", "coroutine.create(function() end)" }, +} +local LEVELS = { 0, 1, 2, 3, 99 } + +print("=== A error(obj, level) 直接抛(外层 pcall 接)===") +for i = 1, #OBJS do + local name, expr = OBJS[i][1], OBJS[i][2] + for _, lv in ipairs(LEVELS) do + P("A " .. name .. " lvl" .. lv, function() + local o = load("return " .. expr)() + return error(o, lv) + end) + end +end + +print("=== B xpcall 的处理器返回什么 ===") +for i = 1, #OBJS do + local name, expr = OBJS[i][1], OBJS[i][2] + P("B " .. name, function() + local o = load("return " .. expr)() + return xpcall(function() error(o) end, + function(m) return type(m) .. ":" .. tostring(m) end) + end) + P("B2 " .. name, function() + local o = load("return " .. expr)() + return xpcall(function() error(o) end, function(m) return m end) + end) + P("B3 " .. name, function() + local o = load("return " .. expr)() + -- 处理器自己出错 + return xpcall(function() error(o) end, function(m) error("handler") end) + end) + P("B4 " .. name, function() + local o = load("return " .. expr)() + -- 处理器返回多个值:只取第一个 + return xpcall(function() error(o) end, function(m) return m, "second" end) + end) +end + +print("=== C 协程 ===") +for i = 1, #OBJS do + local name, expr = OBJS[i][1], OBJS[i][2] + P("C resume " .. name, function() + local o = load("return " .. expr)() + local co = coroutine.create(function() error(o) end) + return coroutine.resume(co) + end) + P("C wrap " .. name, function() + local o = load("return " .. expr)() + local w = coroutine.wrap(function() error(o) end) + return w() + end) + P("C yieldThen " .. name, function() + local o = load("return " .. expr)() + local co = coroutine.create(function() coroutine.yield(1) error(o) end) + local a = coroutine.resume(co) + return coroutine.resume(co) + end) + P("C status " .. name, function() + local o = load("return " .. expr)() + local co = coroutine.create(function() error(o) end) + local ok, e = coroutine.resume(co) + return coroutine.status(co), ok, e + end) +end + +print("=== D 从 C 函数里抛(元方法 / 回调 / 迭代器)===") +for i = 1, #OBJS do + local name, expr = OBJS[i][1], OBJS[i][2] + P("D index " .. name, function() + local o = load("return " .. expr)() + local t = setmetatable({}, {__index = function() error(o) end}) + return t.x + end) + P("D add " .. name, function() + local o = load("return " .. expr)() + local t = setmetatable({}, {__add = function() error(o) end}) + return t + 1 + end) + P("D sort " .. name, function() + local o = load("return " .. expr)() + return table.sort({ 3, 1, 2 }, function() error(o) end) + end) + P("D gsub " .. name, function() + local o = load("return " .. expr)() + return ("abc"):gsub("b", function() error(o) end) + end) + P("D pairs " .. name, function() + local o = load("return " .. expr)() + local t = setmetatable({}, {__pairs = function() error(o) end}) + for _ in pairs(t) do end + end) + P("D tostring " .. name, function() + local o = load("return " .. expr)() + return print(setmetatable({}, {__tostring = function() error(o) end})) + end) + P("D close " .. name, function() + local o = load("return " .. expr)() + local mt = {__close = function() error(o) end} + local f = load("local x = ... return 'ok'") + return f(setmetatable({}, mt)) + end) + P("D gc proxy: exit " .. name, function() + -- os.exit 会杀掉进程,这里只测它是否报错 + local o = load("return " .. expr)() + return pcall(function() return o end) + end) +end + +print("=== E 嵌套保护 ===") +for i = 1, #OBJS do + local name, expr = OBJS[i][1], OBJS[i][2] + P("E pcallInXpcall " .. name, function() + local o = load("return " .. expr)() + return xpcall(function() return pcall(function() error(o) end) end, + function(m) return "H:" .. tostring(m) end) + end) + P("E xpcallInPcall " .. name, function() + local o = load("return " .. expr)() + return pcall(function() + return xpcall(function() error(o) end, function(m) return "H:" .. tostring(m) end) + end) + end) + P("E xpcallInXpcall " .. name, function() + local o = load("return " .. expr)() + return xpcall(function() + return xpcall(function() error(o) end, function(m) return "I:" .. tostring(m) end) + end, function(m) return "O:" .. tostring(m) end) + end) + P("E coInXpcall " .. name, function() + local o = load("return " .. expr)() + return xpcall(function() + local co = coroutine.create(function() error(o) end) + return coroutine.resume(co) + end, function(m) return "H:" .. tostring(m) end) + end) +end + +print("=== F 钩子里抛 ===") +for i = 1, #OBJS do + local name, expr = OBJS[i][1], OBJS[i][2] + P("F hook " .. name, function() + local o = load("return " .. expr)() + debug.sethook(function() error(o) end, "l") + local r = table.pack(pcall(function() local x = 1 return x + 1 end)) + debug.sethook() + return table.unpack(r, 1, r.n) + end) + P("F hookC " .. name, function() + local o = load("return " .. expr)() + debug.sethook(function() error(o) end, "c") + local r = table.pack(pcall(function() return ("x"):upper() end)) + debug.sethook() + return table.unpack(r, 1, r.n) + end) +end + """); + } + + /** 片二:消息处理函数的递归深度(105 行,钉住 200 / 220 两个临界点)。 */ + @Test + void messageHandlerRecursionDepthMatchesReference() { + Diff.assertMatches(""" +-- 消息处理函数「自己出错」的递归深度网格 +-- 起因:C 版 luaG_errormsg 是在抛出点无保护地调处理函数,那时 L->errfunc +-- 还设着,于是处理函数里再 error 会递归进同一个处理函数——而不是直接失败。 +-- 层数靠 incXCcalls 累计(longjmp 跳过了 decXCcalls),第 200 层抛 +-- "C stack overflow",第 220 层才 LUA_ERRERR。这里把 K(连续出错几次后 +-- 才成功)铺成一张网格,把 200 / 220 两个临界点都钉住。 +local HEX = "0123456789ABCDEF" +local function hb(b) return HEX:sub((b // 16) + 1, (b // 16) + 1) .. HEX:sub((b % 16) + 1, (b % 16) + 1) end +local function esc(s) + return (s:gsub(".", function(c) + local b = c:byte() + if b == 91 or b == 93 then return "<" .. hb(b) .. ">" end + if b < 32 or b > 126 then return "<" .. hb(b) .. ">" end + return c + end)) +end +local function show(v) + local t = type(v) + if t == "string" then + local s = v:gsub("0x%x+", "PTR") + return "#" .. #s .. "[" .. esc(s) .. "]" + elseif t == "number" then + if math.type(v) == "integer" then return "i" .. tostring(v) end + return "f" .. tostring(v) + elseif t == "nil" or t == "boolean" then return tostring(v) end + local ok, s = pcall(tostring, v) + if not ok or type(s) ~= "string" then return t .. "()" end + return t .. "(" .. s:gsub("0x%x+", "PTR") .. ")" +end +local function P(tag, fn) + local r = table.pack(pcall(fn)) + local out = tag + for i = 1, r.n do out = out .. " | " .. show(r[i]) end + print(out .. " | n=" .. r.n) +end + +-- K = 处理函数连续出错几次之后才正常返回 +local KS = { 0, 1, 2, 3, 5, 10, 20, 50, 100, 150, 180, 190, 193, 194, 195, + 196, 197, 198, 199, 200, 201, 202, 205, 210, 215, 216, 217, 218, + 219, 220, 221, 225, 250, 400 } + +print("=== A 裸 xpcall(外层无 pcall)===") +for _, K in ipairs(KS) do + P("A K=" .. K, function() + local n = 0 + return xpcall(function() error("orig") end, function(m) + n = n + 1 + if n < K then error("h") end + return "done:" .. tostring(m) + end) + end) +end + +print("=== B 外层再套一层 pcall ===") +for _, K in ipairs(KS) do + P("B K=" .. K, function() + local n = 0 + return xpcall(function() error("orig") end, function(m) + n = n + 1 + if n < K then error("h") end + return "done:" .. tostring(m) + end) + end) +end + +print("=== C 处理函数只在错误对象非串时出错(第二次就成功)===") +local OBJS = { + { "str", "'msg'" }, + { "tbl", "{}" }, + { "tblTostr", "setmetatable({}, {__tostring = function() return 'TS' end})" }, + { "tblTostrBad", "setmetatable({}, {__tostring = function() return {} end})" }, + { "tblTostrErr", "setmetatable({}, {__tostring = function() error('in ts') end})" }, + { "tblTostrNonStr", "setmetatable({}, {__tostring = function() return 42 end})" }, + { "tblName", "setmetatable({}, {__name = 'N'})" }, + { "fn", "print" }, +} +for i = 1, #OBJS do + local name, expr = OBJS[i][1], OBJS[i][2] + P("C tostr " .. name, function() + local o = load("return " .. expr)() + return xpcall(function() error(o) end, + function(m) return type(m) .. ":" .. tostring(m) end) + end) + P("C luaLtolstring " .. name, function() + -- luaL_tolstring 的路径:__tostring 返回非串时它自己抛 + local o = load("return " .. expr)() + return xpcall(function() error(o) end, + function(m) return type(m) .. ":" .. ("%s"):format(m) end) + end) + P("C concat " .. name, function() + local o = load("return " .. expr)() + return xpcall(function() error(o) end, function(m) return "x" .. m end) + end) +end + +print("=== D 处理函数里 yield(callnoyield)===") +for _, K in ipairs({ 0, 1, 2, 3 }) do + P("D K=" .. K, function() + local co = coroutine.create(function() + local n = 0 + return xpcall(function() error("orig") end, function(m) + n = n + 1 + if n <= K then coroutine.yield("y" .. n) end + return "done:" .. tostring(m) + end) + end) + local r = { coroutine.resume(co) } + return table.unpack(r, 1, 3) + end) +end + +print("=== E 处理函数里再 pcall(嵌套保护区)===") +for _, K in ipairs({ 0, 1, 2, 3 }) do + P("E K=" .. K, function() + local n = 0 + return xpcall(function() error("orig") end, function(m) + n = n + 1 + if n < K then error("h") end + local ok, e = pcall(function() error("inner") end) + return "done:" .. tostring(m) .. "/" .. tostring(ok) .. "/" .. tostring(e) + end) + end) +end + """); + } + /** + * 片三:to-be-closed 变量出错的全网格(406 行)。 + *

+ * 起因是 JaCoCo 里 {@code LuaState#closeUpvalsOnUnwind} 有一条分支从没被 + * 走到 ——「{@code __close} 抛出的错误没有位置时补一个前缀」。既有的 + * {@code D close} 那一组清一色是 {@code error(o)}(默认 level 1,自带位置), + * 所以那条分支从没被扫过。 + *

+ * 两维铺开: + *

    + *
  • 维一 24 种「{@code __close} 怎么出错」:{@code error} 的 + * level 0/1/2/3/99/-1、运行时错、{@code error(非串)}、 + * {@code assert}、C 函数当 {@code __close} + * ({@code error} / {@code string.rep} / {@code tonumber} / + * {@code print})、带 {@code __call} 的表、以及 {@code __close} 里 + * yield;
  • + *
  • 维二 8 种上下文:函数体正常返回 / 函数体先抛错 / 两个 + * {@code } 变量 / {@code xpcall} + 处理函数 / 处理函数自己抛。
  • + *
+ * 再单铺四组:协程里({@code resume} 两趟)、main chunk 里 + * ({@code do} 块结束才关)、四种跳出作用域的方式 + * ({@code return} / {@code break} / {@code goto} / 尾调用), + * 以及 10 种「不可关」的值({@code non-closable})。 + *

+ * 这一片本身没有咬出分歧,但把上面那条分支和 + * {@link #memoryErrorSkipsHandlerMatchesReference}、{@code next} 那两处 + * 真分歧的触发语料一起固化了下来。 + *

+ * 写法上两个坑: + *

    + *
  • 换掉字符串的类型元表时必须带上 {@code __index = string}, + * 否则 {@code show} 里的 {@code v:gsub} 会抛 + * {@code attempt to index a string value},而那是在 {@code pcall} + * 外面抛的,整个探针崩掉(约定 #38);
  • + *
  • {@code print} 当 {@code __close} 会打出 {@code table: 0x...}, + * 由 {@link Diff} 的地址归一化兜住(约定 #42)。
  • + *
+ */ + @Test + void toBeClosedErrorGridMatchesReference() { + Diff.assertMatches(""" +-- to-be-closed 变量出错的全网格 +-- 起因:JaCoCo 里 LuaState 的 closeUpvalsOnUnwind 有一条分支从没被走到 —— +-- 「__close 抛出的错误没有位置时补一个前缀」。既有的 ErrorPropagationTest +-- 只有一组 `D close`,而且清一色是 error(o)(默认 level 1,自带位置), +-- 所以那条分支从没被扫过。 +-- +-- 这里铺两维: +-- 维一 24 种「__close 怎么出错」;维二 8 种「在什么上下文里出错」。 +-- 再加几组单点的:main chunk 里的 tbc、goto 跳出作用域、非函数 __close。 + +local function show(v) + local t = type(v) + if t == "string" then + return "#" .. #v .. "[" .. (v:gsub("[\\r\\n]", "?")) .. "]" + elseif t == "number" then + if math.type(v) == "integer" then return "i" .. tostring(v) end + return "f" .. tostring(v) + elseif t == "nil" or t == "boolean" then + return tostring(v) + end + local ok, s = pcall(tostring, v) + if not ok or type(s) ~= "string" then return t .. "()" end + return t .. "(" .. (s:gsub("0x%x+", "PTR")) .. ")" +end + +local function P(tag, fn) + local r = table.pack(pcall(fn)) + local out = tag + for i = 1, r.n do out = out .. " | " .. show(r[i]) end + print(out .. " | n=" .. r.n) +end + +-- ==================== 维一:__close 怎么出错 ==================== +-- 每一行是一个「close 方法」。_self 是被关的变量,_err 是传给它的错误对象。 +local SHAPES = { + { "nil", function(_self, _err) return nil end }, + { "retvals", function(_self, _err) return 1, 2, 3 end }, + { "err0", function(_self, _err) error("E0", 0) end }, + { "err1", function(_self, _err) error("E1", 1) end }, + { "err2", function(_self, _err) error("E2", 2) end }, + { "err3", function(_self, _err) error("E3", 3) end }, + { "err99", function(_self, _err) error("E99", 99) end }, + { "errneg", function(_self, _err) error("ENEG", -1) end }, + { "errnolevel", function(_self, _err) error("ENO") end }, + { "errtwo", function(_self, _err) error("ET", 2) end }, + { "nilindex", function(_self, _err) local t = nil return t.x end }, + { "callstr", function(_self, _err) return ("s")() end }, + { "arith", function(_self, _err) return nil + 1 end }, + { "obj", function(_self, _err) error(setmetatable({}, { __tostring = function() return "OBJ" end })) end }, + { "errarg", function(_self, _err) error("EARG:" .. show(_err)) end }, + { "errarg0", function(_self, _err) error("EARG0:" .. show(_err), 0) end }, + { "assert", function(_self, _err) return assert(false) end }, + { "assertmsg", function(_self, _err) return assert(false, "AMSG") end }, + { "cerr", error }, -- C 函数当 __close:error(obj, err) + { "crep", string.rep }, -- C 函数当 __close:参数不对 + { "ctonumber", tonumber }, -- C 函数当 __close:能跑通 + { "cprint", print }, -- C 函数当 __close:有副作用 + { "callable", setmetatable({}, { __call = function(_self2, _a, _b) error("ECALL", 0) end }) }, + { "yield", function(_self, _err) return coroutine.yield("YC") end }, +} + +-- ==================== 维二:什么上下文 ==================== +-- 每个上下文收一个 close 函数,返回一个「被 pcall 包起来的动作」。 +local CTXS = { + { "plain", function(cf) + return function() + local x = setmetatable({}, { __close = cf }) + return "body" + end + end }, + { "bodyerr", function(cf) + return function() + local x = setmetatable({}, { __close = cf }) + error("BODY") + end + end }, + { "bodynil", function(cf) + return function() + local x = setmetatable({}, { __close = cf }) + local t = nil + return t.x + end + end }, + { "two", function(cf) + return function() + local a = setmetatable({}, { __close = cf }) + local b = setmetatable({}, { __close = cf }) + return "body" + end + end }, + { "twoerr", function(cf) + return function() + local a = setmetatable({}, { __close = cf }) + local b = setmetatable({}, { __close = cf }) + error("BODY2") + end + end }, + { "xpcall", function(cf) + return function() + return xpcall(function() + local x = setmetatable({}, { __close = cf }) + return "body" + end, function(m) return "H:" .. show(m) end) + end + end }, + { "xpcallerr", function(cf) + return function() + return xpcall(function() + local x = setmetatable({}, { __close = cf }) + error("BODY3") + end, function(m) return "H:" .. show(m) end) + end + end }, + { "xpcallherr", function(cf) + return function() + return xpcall(function() + local x = setmetatable({}, { __close = cf }) + error("BODY4") + end, function(m) error("HANDLER") end) + end + end }, +} + +print("=== grid ===") +for i = 1, #CTXS do + local cname, mk = CTXS[i][1], CTXS[i][2] + for j = 1, #SHAPES do + local sname, cf = SHAPES[j][1], SHAPES[j][2] + P(cname .. "/" .. sname, mk(cf)) + end +end + +-- ==================== 协程里的 tbc ==================== +-- 单独一列:__close 在协程里出错时,错误是从 resume 那侧回来的。 +print("=== coroutine ===") +for j = 1, #SHAPES do + local sname, cf = SHAPES[j][1], SHAPES[j][2] + P("co/" .. sname, function() + local co = coroutine.create(function() + local x = setmetatable({}, { __close = cf }) + coroutine.yield("Y1") + return "body" + end) + local r1 = table.pack(coroutine.resume(co)) + local r2 = table.pack(coroutine.resume(co)) + local out = "" + for i = 1, r1.n do out = out .. " r1:" .. show(r1[i]) end + for i = 1, r2.n do out = out .. " r2:" .. show(r2[i]) end + return out .. " st=" .. coroutine.status(co) + end) + P("coerr/" .. sname, function() + local co = coroutine.create(function() + local x = setmetatable({}, { __close = cf }) + error("COBODY") + end) + local r1 = table.pack(coroutine.resume(co)) + local out = "" + for i = 1, r1.n do out = out .. " r1:" .. show(r1[i]) end + return out .. " st=" .. coroutine.status(co) + end) +end + +-- ==================== main chunk 里的 tbc ==================== +-- 函数体外面也能声明 :do 块结束时关。 +print("=== mainchunk ===") +for j = 1, #SHAPES do + local sname, cf = SHAPES[j][1], SHAPES[j][2] + P("mc/" .. sname, function() + local f = load("local x = ... return 'body'", "=c") + return f(setmetatable({}, { __close = cf })) + end) +end + +-- ==================== 跳出作用域的四种方式 ==================== +print("=== scape ===") +local function mkClose(cf) + return setmetatable({}, { __close = cf }) +end +for j = 1, #SHAPES do + local sname, cf = SHAPES[j][1], SHAPES[j][2] + P("return/" .. sname, function() + local function f() + local x = mkClose(cf) + if true then return "ret" end + return "no" + end + return f() + end) + P("break/" .. sname, function() + local function f() + for i = 1, 3 do + local x = mkClose(cf) + if i == 2 then break end + end + return "brk" + end + return f() + end) + P("goto/" .. sname, function() + local function f() + do + local x = mkClose(cf) + goto out + end + ::out:: + return "gto" + end + return f() + end) + P("tail/" .. sname, function() + local function g() return "tail" end + local function f() + local x = mkClose(cf) + return g() + end + return f() + end) +end + +-- ==================== 非函数 / 不可关的值 ==================== +print("=== nonclosable ===") +local NCS = { + { "nil", "nil" }, + { "false", "false" }, + { "true", "true" }, + { "int", "42" }, + { "float", "1.5" }, + { "str", "'abc'" }, + { "func", "print" }, + { "thread", "coroutine.create(function() end)" }, + { "notm", "setmetatable({}, {})" }, + { "emptymt", "setmetatable({}, {__close = nil})" }, +} +for i = 1, #NCS do + local name, expr = NCS[i][1], NCS[i][2] + P("nc/" .. name, function() + local f = load("local x = (" .. expr .. ") return 'body'", "=c") + return f() + end) + P("ncn/" .. name, function() + local function f() + local x = load("return " .. expr, "=c")() + return "body" + end + return f() + end) +end + +-- 字符串/数字的类型元表上挂 __close(约定 #80:用完必须还原) +print("=== typemt ===") +do + local old = debug.getmetatable("") + -- __index 必须带上:换掉字符串的类型元表之后,show 里的 v:gsub 会变成 + -- "attempt to index a string value",而那是在 pcall 外面抛的,整个探针就崩了 + debug.setmetatable("", { __index = string, __close = function() error("SCLOSE", 0) end }) + P("strmt", load("local x = 'zz' return 'body'", "=c")) + debug.setmetatable("", old) +end + +print("END") + """); + } + + /** + * 片四:{@code lua_error} 的内存错误特判(16 行,咬出两处真分歧)。 + *

+ * {@code lapi.c: lua_error} 里有唯一一处「按错误值的内容决定错误种类」 + * 的判断:错误对象是 {@code "not enough memory"} 这个驻留短串时走 + * {@code luaM_error}({@code LUA_ERRMEM},直跳),否则走 + * {@code luaG_errormsg}(先过 {@code L->errfunc})。 + * 两条路唯一的外部差别是过不过消息处理函数: + *

+     * xpcall(function() error("not enough memory", 0) end, H)
+     *   参考:false  not enough memory          (处理函数接不到)
+     *   本实现:false  H:not enough memory
+     * xpcall(function() return io.open("/dev/null"):read(-1) end, H)
+     *   参考:false  not enough memory
+     *   本实现:false  H:not enough memory
+     * 
+ * 而 {@code error("not enough memory")}(默认 level 1)会先拼上位置前缀, + * 拼出来的串不再是那个驻留串,于是退化成普通错误、处理函数照样接得到 + * ——「有没有位置」这个看似无关的差别把错误种类换了,所以十个用例里 + * {@code level} 取 0/1 都要有。 + *

+ * {@code f:read(-1)} 那条同源:C 的 {@code resizebox} 是 + * {@code lua_pushliteral + lua_error},同样被认成内存错误。 + */ + + @Test + void memoryErrorSkipsHandlerMatchesReference() { + Diff.assertMatches(""" +local function P(tag, fn) + local r = table.pack(pcall(fn)) + local out = tag + for i = 1, r.n do out = out .. " | " .. tostring(r[i]) end + print(out .. " | n=" .. r.n) +end +local H = function(m) return "H:" .. tostring(m) end +P("lit", function() return xpcall(function() error("not enough memory") end, H) end) +P("lit0", function() return xpcall(function() error("not enough memory", 0) end, H) end) +P("lit1", function() return xpcall(function() error("not enough memory", 1) end, H) end) +P("concat", function() return xpcall(function() error("not enough " .. "memory") end, H) end) +P("pcall", function() return pcall(error, "not enough memory") end) +P("read", function() return xpcall(function() return io.open("/dev/null"):read(-1) end, H) end) +P("close", function() return xpcall(function() + local x = setmetatable({}, {__close = function() error("not enough memory") end}) + return "b" end, H) end) +P("co", function() + local co = coroutine.create(function() error("not enough memory") end) + local r = table.pack(coroutine.resume(co)) + local o = "" + for i = 1, r.n do o = o .. " | " .. tostring(r[i]) end + return o end) +P("var", function() local s = "not enough memory" return xpcall(function() error(s) end, H) end) +-- level 0:拼出来的串就是那个驻留串("内容相等"与"同一个对象"等价) +P("var0", function() local s = "not enough memory" return xpcall(function() error(s, 0) end, H) end) +-- 拼接结果:短串驻留,拼出来的还是同一个串对象 +P("concat0", function() return xpcall(function() error("not enough " .. "memory", 0) end, H) end) +P("rep1", function() return xpcall(function() error(("not enough memory"):rep(1), 0) end, H) end) +P("sub", function() return xpcall(function() error(("Xnot enough memory"):sub(2), 0) end, H) end) +print("END") + """); + } + + /** + * 片五:{@code luaG_runerror} 在 C 帧里没有位置(74 行,咬出 + * {@code next} 那处真分歧)。 + *

+ * C 版给运行时错误加位置的是 {@code luaG_runerror},判据是 + * {@code isLua(ci)} —— 只看当前帧,与 {@code luaL_error} 的 + * {@code luaL_where(L, 1)}(回跳一层到调用者)完全是两回事。于是 + * {@code ltable.c: findindex} 里那句 + * {@code luaG_runerror(L, "invalid key to 'next'")} 永远没有位置:跑到 + * 那儿时 {@code ci} 是 {@code luaB_next} 这个 C 帧。 + *

+ * 本实现当时用的是 {@link LuaState#libError}({@code luaL_error} 那套), + * 于是凭空多出 {@code case.lua:N:}: + *

+     * 参考:  false  invalid key to 'next'
+     * 本实现:false  case.lua:12: invalid key to 'next'
+     * 
+ * 约定 #8 那句「库函数一律 {@code libError}」在这里不成立—— + * 这条消息的源头在 {@code ltable.c} 而不在库代码里,得走 + * {@link LuaState#runError}。 + *

+ * 这一片把 23 种运行时错误各放进三种上下文(直接在 Lua 帧里 / 在 + * {@code gsub} 的 Lua 回调里 / 由 C 函数自己触发),目的就是找出 + * 「同一个错误在不同帧里位置不一样」的那一族。除 {@code next} 之外 + * 全部一致({@code table.sort} 的 {@code attempt to compare} 也是 + * C 帧无位置,两边都对)。 + */ + @Test + void runtimeErrorPositionByFrameMatchesReference() { + Diff.assertMatches(""" +local function P(tag, fn) + local r = table.pack(pcall(fn)) + local out = tag + for i = 1, r.n do out = out .. " | " .. tostring(r[i]) end + print(out .. " | n=" .. r.n) +end +-- 一批「运行时错误」,分别在三种上下文里触发: +-- L = 直接在 Lua 帧里 +-- G = 在 gsub 的 Lua 回调里(ci 还是 Lua) +-- C = 由 C 函数自己做(ci 是 C 帧) +local E = { + { "nextbad", function() return next({}, 1) end }, + { "nextbad2", function() return next({1,2,3}, "k") end }, + { "sortcmp", function() local t = {1,"a"} table.sort(t) return t end }, + { "sortcmp2", function() local t = {{},{}} table.sort(t) return t end }, + { "callnil", function() return pcall(nil) end }, + { "callnum", function() return pcall(42) end }, + { "callnum2", function() return xpcall(42, print) end }, + { "nilidx", function() local t = {} t[nil] = 1 end }, + { "nanidx", function() local t = {} t[0/0] = 1 end }, + { "idiv0", function() return 1 // 0 end }, + { "mod0", function() return 1 % 0 end }, + { "arith", function() return {} + 1 end }, + { "concat", function() return {} .. "x" end }, + { "index", function() local t = nil return t.x end }, + { "callstr", function() return ("s")() end }, + { "intrep", function() return ("x"):rep(1.5) end }, + { "intrep2", function() return string.format("%d", 1.5) end }, + { "totoint", function() return ("x"):sub(1.5) end }, + { "yieldout", function() return coroutine.yield() end }, + { "yieldC", function() + local co = coroutine.create(function() return ("x"):gsub("x", function() return coroutine.yield(1) end) end) + local a = table.pack(coroutine.resume(co)) + local b = table.pack(coroutine.resume(co)) + return tostring(a[1]) .. " " .. tostring(a[2]) .. " / " .. tostring(b[1]) .. " " .. tostring(b[2]) + end }, + { "getmeta", function() return ("x"):gsub("x", setmetatable({}, {__index = function() error("IDX", 0) end})) end }, + { "le", function() return {} < {} end }, + { "ltn", function() return 1 < "a" end }, +} +for i = 1, #E do + local n, f = E[i][1], E[i][2] + P("L/" .. n, f) + P("G/" .. n, function() return ("x"):gsub("x", f) end) + P("C/" .. n, function() return pcall(f) end) +end +print("END") + """); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/FormatGridTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/FormatGridTest.java new file mode 100644 index 0000000..89b8def --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/FormatGridTest.java @@ -0,0 +1,433 @@ +package com.github.esrrhs.fakescript.lua; + +import org.junit.jupiter.api.Test; + +import org.junit.jupiter.api.BeforeAll; + +import java.io.File; + +/** + * {@code string.format} 的全网格差分 —— 阶段 12。 + *

+ * 为什么单开一类:{@code format} 是本实现里唯一一处「照 C99 抄一遍 printf」的 + * 代码,而 C 的每一条特殊规则都是踩出来的(约定 #15–#21)——浮点只走 + * {@code BigDecimal}、同一个转换只有一份实现、校验顺序因动词而异、整数动词 + * 一给精度 {@code 0} 标志就失效。规则之间是相乘的,手挑用例盖不住 + * 「动词 × 标志 × 宽度 × 精度 × 值」这个五维。 + *

+ * 十三段(6791 行): + *

    + *
  • {@code A}/{@code B}:18 个动词 × 39 个数值 / 10 个非数值;
  • + *
  • {@code C}/{@code D}/{@code E}/{@code F}:标志(20 种组合,含乱序 + * {@code "0#-+"})× 宽度 × 精度 × 动词 × 值——{@code 0} 标志那一条是坑 + * (约定 #19),{@code F} 段专铺「宽度+精度+标志」三件套;
  • + *
  • {@code G}/{@code M}:{@code %q} 的 5.4 新语义与往返 + * ({@code load} 回来还是不是原值);
  • + *
  • {@code H}:{@code %s}/{@code %c} 的字节安全(含 {@code \0}、 + * {@code 0xFF}、空串);
  • + *
  • {@code I}/{@code J}:畸形格式串与参数个数/类型;
  • + *
  • {@code K}:协程第二遍(约定 #7——{@code registry} 级的东西只在 + * 协程里现形);
  • + *
  • {@code L}:{@code %a} × {@code #} × 2 的整数次幂 / 次正规 / 非有限。
  • + *
+ *

+ * 这一片咬出三个 bug,都在 {@code %a} 上: + *

    + *
  1. {@code #} 标志下小数点必须留着:{@code ("%#a"):format(0.0)} 是 + * {@code 0x0.p+0}。原实现只在非零分支照这条写,零值分支漏了;
  2. + *
  3. {@code %q} 与 {@code %a} 各写了一份:同一个转换两份实现,次正规 + * 数上给出两种答案(约定 #16);
  4. + *
  5. {@code inf}/{@code nan} 不参与零填充: + * {@code ("%020.5a"):format(math.huge)} 是 17 个空格加 {@code inf},不是 + * 17 个 {@code 0}。glibc 的 {@code printf_fphex.c} 在 {@code special} + * 分支里直接 {@code pad(buf, ' ', width)},根本不看 {@code pad == '0'}。
  6. + *
+ *

+ * 两批用例刻意不打(两边判据一致,跳过的就是同一批): + *

    + *
  • 次正规数的 {@code %a}/{@code %A}/{@code %q}:C99 把次正规的写法 + * 留作 unspecified,glibc 保留机器编码({@code 0x0.…p-1022})、 + * Apple libc 先规格化({@code 0x1p-1074})——约定 #52;
  • + *
  • 带精度的 {@code %a} 里「舍弃的第一位十六进制数字恰为 8」:glibc + * 是就近舍入 + 平局取偶,Apple libc 在这一段根本不是就近舍入 + * ({@code ("%.4a"):format(0x1.500082p0)} 给 {@code 0x1.5000p+0}, + * {@code 0x82} 明明大于半 {@code 0x80} 却舍了下去)。
  • + *
+ * 这两批由 {@link StringLibTest#hexFloatPrecisionRoundingFollowsGlibc} 与 + * {@link StringLibTest#subnormalQuoteFollowsGlibc} 用普通断言钉住 glibc 的答案 + * ——CI 的一致性闸门跑在 ubuntu 上,glibc 才是裁判。 + *

+ * 写同类探针要记住的两个坑: + *

    + *
  • {@code string.find(s, "%.", 1, true)} 找的是两个字 {@code "%."} + * ——{@code plain=true} 时不做模式匹配。第一版这么写,于是 + * {@code precof} 恒返回 {@code nil},该跳过的平局用例一条没跳过;
  • + *
  • 判据函数不能抛错:{@code ("%a"):format('abcdef')} 会报 + * {@code bad argument},而 {@code skip} 在 {@code pcall} 外面跑, + * 一抛就整个脚本断在半路——两端输出还「一致」,是假的一致。
  • + *
+ */ +class FormatGridTest { + + @BeforeAll + static void prepare() { + new File("target/rt").mkdirs(); + } + + /** 动词 × 标志 × 宽度 × 精度 × 值。 */ + @Test + void stringFormatGridMatchesReference() { + Diff.assertMatches(""" +-- string.format 全网格:动词 × 标志 × 宽度 × 精度 × 值 +-- 起因:format 是本实现里少数「照 C99 抄一遍 printf」的地方,而 C 的每一条 +-- 特殊规则都是踩出来的(约定 #15-#21):格式化只走 BigDecimal、同一个转换 +-- 只有一份实现、校验顺序因动词而异、整数动词一指定精度 0 标志就失效…… +-- 手挑用例覆盖不到「标志 × 动词 × 精度」这个三维。 +local HEX = "0123456789ABCDEF" +local function hb(b) return HEX:sub((b // 16) + 1, (b // 16) + 1) .. HEX:sub((b % 16) + 1, (b % 16) + 1) end +local function esc(s) + return (s:gsub(".", function(c) + local b = c:byte() + if b == 91 or b == 93 then return "<" .. hb(b) .. ">" end + if b < 32 or b > 126 then return "<" .. hb(b) .. ">" end + return c + end)) +end +local function show(v) + local t = type(v) + if t == "string" then + local s = v:gsub("0x%x+", "PTR") + -- NaN 的符号是 libc 自己的(约定 #96):x86 默认 NaN 符号位是 1,glibc + -- 打 "-nan"(%+f 也打 "-nan"),Apple libc 打 "nan"。抹掉符号只是第一步 + -- —— glibc 填的是 4 个字符、Apple 填的是 3 个,同样 width=20,前者补 + -- 16 个空格、后者补 17 个;符号抹平之后总长度就差一格 + -- ("%20.5a":format(0/0) 参考给 19 列、我们给 20 列)。 + -- 所以带填充的 nan 连「填了几个空格」一起让出去,只比「填的是空格、 + -- 内容是 nan、填在哪一侧」。inf 不受影响:两边都是 3 个字符。 + -- 第一版写成 ^(%s*)(nan)(%s*)$ 的整串匹配,漏掉两种形状: + -- 结果后面还跟着别的字("%-10.2f|" 的哨兵 "|"、"(0/0) -> nan" 这种 + -- 前后带东西的)。改成「nan 前后那两段空格」分别压 —— 只要是贴着 nan + -- 的连续空格就压成一个 ,在哪一侧照样看得出来。 + s = s:gsub("(%s+)([%-%+]?[nN][aA][nN])", "%2") + s = s:gsub("([%-%+]?[nN][aA][nN])(%s+)", "%1") + s = s:gsub("[%-%+]nan", "nan"):gsub("[%-%+]NAN", "NAN") + return "#" .. #s .. "[" .. esc(s) .. "]" + elseif t == "number" then + if math.type(v) == "integer" then return "i" .. tostring(v) end + return "f" .. tostring(v) + elseif t == "nil" or t == "boolean" then return tostring(v) end + local ok, s = pcall(tostring, v) + if not ok or type(s) ~= "string" then return t .. "()" end + return t .. "(" .. s:gsub("0x%x+", "PTR") .. ")" +end +local function P(tag, fn) + local r = table.pack(pcall(fn)) + local out = tag + for i = 1, r.n do out = out .. " | " .. show(r[i]) end + print(out .. " | n=" .. r.n) +end + +-- ===== 平台相关的两批:差分表里直接不打(两边判据一致,跳过的就是同一批)===== +-- +-- ① 次正规数的 %a / %A / %q。C99 只规定「小数点左边恰好一位十六进制数字」, +-- 并且「这个数字只在数次正规时为 0,其它情况取值由实现定义」——次正规怎么写 +-- 属于 unspecified。glibc(CI 的 ubuntu)忠实于机器编码,5e-324 给 +-- 0x0.0000000000001p-1022;Apple libc / musl 先规格化,给 0x1p-1074。 +-- CI 跑 ubuntu,取 glibc,所以这批不拿 macOS 的参考实现来比。 +-- +-- ② 带精度的 %a 里「舍弃的第一位十六进制数字恰为 8」的那批。glibc 的判据 +-- (stdio-common/printf_fphex.c)是 round_away(negative, last_digit & 1, +-- next_digit >= 8, more_bits, mode),而 round_away 在 FE_TONEAREST 下就是 +-- half_bit && (last_digit_odd || more_bits)——就近舍入、平局取偶。 +-- Apple libc 在这一段根本不是就近舍入,错得可以量化: +-- ("%.4a"):format(0x1.500082p+0) -> 0x1.5000p+0 (0x82 > 0x80 却舍了) +-- ("%.4a"):format(0x1.500081p+0) -> 0x1.5001p+0 (反倒进了) +-- 它的判据成了「第二位舍弃数字的奇偶」。两条在 glibc 上都该进位。 +local MINNORMAL = 2 ^ -1022 +local function num(e) return load("return " .. e)() end +local function is_subnormal(x) + return math.type(x) == "float" and x ~= 0 and math.abs(x) < MINNORMAL +end +-- 不给精度时的十六进制展开(精确,不含 p 之后的部分) +local function hexfrac(x) + local s = ("%a"):format(x) + local d = s:find(".", 1, true) + if not d then return "" end + return (s:sub(d + 1):gsub("p.*$", "")) +end +local function skip(verb, x, prec) + if verb ~= "a" and verb ~= "A" and verb ~= "q" then return false end + -- 非数值的 %a 会报错,那是另一条用例(E 段有 'abcdef'),不该在这里炸掉 + if type(x) ~= "number" then return false end + if is_subnormal(x) then return true end + -- %q 不给精度(snprintf("%a")),不舍入;prec 为 nil 同理 + if verb == "q" or prec == nil then return false end + local f = hexfrac(x) + return #f > prec and f:sub(prec + 1, prec + 1) == "8" +end +-- 格式串里的精度;没有小数点就是不给精度,有小数点但没数字就是 0 +local function precof(spec) + local i = spec:find(".", 1, true) + if not i then return nil end + local d = spec:match("^%d+", i + 1) + return d and tonumber(d) or 0 +end +-- 打一条;平台相关的直接跳过 +local function PQ(tag, verb, x, prec, fn) + if skip(verb, x, prec) then return end + P(tag, fn) +end + +local NUMS = { + "0", "1", "-1", "42", "-42", "255", "-255", "1000000", + "math.maxinteger", "math.mininteger", "math.maxinteger + 0.0", + "0.0", "-0.0", "1.5", "-1.5", "0.5", "2.5", "3.5", + "1e300", "-1e300", "1e-300", "math.pi", "1 / 3", "2 ^ 53", "2 ^ 53 + 1.0", + "0.1", "1e15", "1e16", "1e-5", "123456.789", + "math.huge", "-math.huge", "0 / 0", "5e-324", "2 ^ -1074", + "1.7976931348623157e308", "0x7fffffff", "3.0", "-3.0", +} +local NONNUMS = { "nil", "true", "false", "'str'", "''", "'a\\\\0b'", "'\\\\255\\\\254'", + "{}", "print", "coroutine.create(function() end)" } + +local VERBS = { "d", "i", "u", "o", "x", "X", "e", "E", "f", "F", "g", "G", + "a", "A", "c", "s", "q", "%" } + +print("=== A 动词 × 数值(无标志无宽无精度)===") +for i = 1, #VERBS do + local v = VERBS[i] + for j = 1, #NUMS do + local e = NUMS[j] + local x = num(e) + PQ("A " .. v .. " " .. j .. " " .. e, v, x, nil, function() + return string.format("%" .. v, x) + end) + end +end + +print("=== B 动词 × 非数值 ===") +for i = 1, #VERBS do + local v = VERBS[i] + for j = 1, #NONNUMS do + local e = NONNUMS[j] + P("B " .. v .. " " .. j .. " " .. e, function() + return string.format("%" .. v, load("return " .. e)()) + end) + end +end + +print("=== C 标志 × 动词 × 值 ===") +-- C99 五个标志:- + 空格 0 #。取十六种组合(含空) +local FLAGS = { "", "-", "+", " ", "0", "#", "-+", "- ", "-0", "-#", "+ ", + "+0", "+#", " 0", " #", "0#", "-+0", "-+#", "-+ 0", "0#-+ " } +local CVALS = { "42", "-42", "1.5", "0.0", "math.maxinteger", "1 / 3", "255" } +for i = 1, #FLAGS do + local f = FLAGS[i] + for j = 1, #VERBS do + local v = VERBS[j] + for k = 1, #CVALS do + local x = num(CVALS[k]) + PQ("C '" .. f .. "' " .. v .. " " .. k, v, x, precof("%" .. f .. v), function() + return string.format("%" .. f .. v, x) + end) + end + end +end + +print("=== D 宽度 × 动词 × 值 ===") +local WIDTHS = { "", "0", "1", "3", "8", "20", "40" } +local DVALS = { "42", "-42", "1.5", "'ab'" } +for i = 1, #WIDTHS do + local w = WIDTHS[i] + for j = 1, #VERBS do + local v = VERBS[j] + for k = 1, #DVALS do + P("D '" .. w .. "' " .. v .. " " .. k, function() + return string.format("%" .. w .. v, load("return " .. DVALS[k])()) + end) + end + end +end + +print("=== E 精度 × 动词 × 值 ===") +-- 约定 #19:整数动词一旦指定精度,0 标志就失效 +local PRECS = { "", ".", ".0", ".1", ".3", ".5", ".14", ".17", ".20", ".40" } +local EVALS = { "42", "-42", "1.5", "0.0", "1 / 3", "255", "'abcdef'" } +for i = 1, #PRECS do + local p = PRECS[i] + for j = 1, #VERBS do + local v = VERBS[j] + for k = 1, #EVALS do + local x = num(EVALS[k]) + PQ("E '" .. p .. "' " .. v .. " " .. k, v, x, precof("%" .. p .. v), function() + return string.format("%" .. p .. v, x) + end) + end + end +end + +print("=== F 宽度 × 精度 × 标志 × 动词(0 标志那一条是坑)===") +local FWP = { "5.3", "05.3", "-05.3", "5.0", "05.0", "0.3", "8.2", "08.2", "-8.2", + "20.17", "020.17", "1.1", "0.0", "10.10", "3.40" } +local FVALS = { "42", "-42", "1.5", "1 / 3" } +for i = 1, #FWP do + local wp = FWP[i] + for j = 1, #VERBS do + local v = VERBS[j] + for k = 1, #FVALS do + local x = num(FVALS[k]) + PQ("F '" .. wp .. "' " .. v .. " " .. k, v, x, precof("%" .. wp .. v), function() + return string.format("%" .. wp .. v, x) + end) + end + end +end + +print("=== G %q(5.4 的新语义)===") +local QVALS = { "nil", "true", "false", "0", "-0.0", "1.5", "-1.5", "1e300", + "1e-300", "math.pi", "math.huge", "-math.huge", "0 / 0", + "0x10", "1e15", "1e16", "'str'", "''", "'a\\\\0b'", "'\\\\255'", + "'\\\\n\\\\r\\\\t\\\\\\\\\\\\\\"'", "math.maxinteger", "math.mininteger", + "1 / 3", "2 ^ 63", "5e-324" } +for i = 1, #QVALS do + local e = QVALS[i] + local x = num(e) + PQ("G " .. i .. " " .. e, "q", x, nil, function() + return string.format("%q", x) + end) + PQ("G r " .. i .. " " .. e, "q", x, nil, function() + -- 往返:%q 出来的东西应当能 load 回原值 + local s = string.format("%q", x) + local back = load("return " .. s) + if back == nil then return "NOLOAD " .. show(s) end + return tostring(s) .. " -> " .. tostring(back()) + end) +end + +print("=== H %s / %c 的字节安全 ===") +local SVALS = { "''", "'a'", "'a\\\\0b'", "'\\\\255\\\\254'", "'\\\\0'", "'\\\\n'", + "string.rep('x', 30)", "'\\\\1\\\\2\\\\3'" } +for i = 1, #SVALS do + local e = SVALS[i] + for _, spec in ipairs({ "%s", "%10s", "%-10s", "%.3s", "%10.3s", "%-10.3s", + "%.0s", "%1s", "%100s" }) do + P("H " .. i .. " " .. spec, function() + return string.format(spec, load("return " .. e)()) + end) + end + P("H tostring " .. i, function() + return tostring(load("return " .. e)()) + end) +end +for _, e in ipairs({ "65", "0", "255", "-1", "1.5", "65.0", "'A'", "math.maxinteger" }) do + for _, spec in ipairs({ "%c", "%5c", "%-5c", "%.2c" }) do + P("H c " .. spec .. " " .. e, function() + return string.format(spec, load("return " .. e)()) + end) + end +end + +print("=== I 畸形 / 边界格式串 ===") +local BAD = { "%", "%z", "%l", "%y", "%5", "%.", "%-%", "%%", "%%d", "%d%", + "%1$s", "%*d", "abc%d", "%1000d", "%-0d", "% d", "%+d", "%#o", + "%#x", "%#X", "%#.0o", "%#g", "%#.0g", "%#f", "%5.2s", "%.2", + "%999999999999d", "%-999999999999d", "%.999999999999f", + "%-", "%+", "% ", "%0", "%#", "%..1d", "%1.1.1d", "%--d", + "%-5", "%05%", "%s%s", "%d%d", "%\\\\0d", "%\\\\255" } +for i = 1, #BAD do + local f = BAD[i] + P("I " .. i .. " " .. esc(f), function() return string.format(f) end) + P("I a " .. i .. " " .. esc(f), function() return string.format(f, 42) end) + P("I b " .. i .. " " .. esc(f), function() return string.format(f, 42, "x", 1.5) end) +end + +print("=== J 参数个数与类型 ===") +P("J extra", function() return string.format("%d", 1, 2, 3) end) +P("J extra2", function() return string.format("%d %d", 1, 2, 3, 4) end) +P("J few", function() return string.format("%d %d", 1) end) +P("J none", function() return string.format("%d") end) +P("J empty", function() return string.format("") end) +P("J plain", function() return string.format("abc") end) +P("J nonstr", function() return string.format(42) end) +P("J nil0", function() return string.format(nil) end) +P("J nilarg", function() return string.format("a") end) +for _, a in ipairs({ "nil", "true", "42", "1.5", "{}", "print" }) do + P("J f " .. a, function() return string.format(load("return " .. a)()) end) + P("J f2 " .. a, function() return string.format("%d", load("return " .. a)()) end) + P("J f3 " .. a, function() return string.format("%s", load("return " .. a)()) end) +end +P("J noself", function() return string.format() end) + +print("=== K 协程第二遍:整片重跑 ===") +local function body() + local out = {} + for i = 1, #VERBS do + for j = 1, #NUMS do + local v = VERBS[i] + local x = num(NUMS[j]) + if not skip(v, x, nil) then + out[#out + 1] = show(select(2, pcall(string.format, "%" .. v, x))) + end + end + end + for i = 1, #FWP do + for j = 1, #VERBS do + local v = VERBS[j] + local spec = "%" .. FWP[i] .. v + if not skip(v, 42, precof(spec)) then + out[#out + 1] = show(select(2, pcall(string.format, spec, 42))) + end + end + end + return table.concat(out, ";") +end +P("K main", function() return body() end) +P("K co", function() + local c = coroutine.create(function() return body() end) + return select(2, coroutine.resume(c)) +end) +P("K wrap", function() + local w = coroutine.wrap(function() return body() end) + return w() +end) + +print("=== L %a × # × 2 的整数次幂 / 次正规 / 非有限 ===") +-- 咬出三个 bug 的地方:①# 标志下小数点必须留着(零与 2 的整数次幂上才看得出 +-- 来);②%q 与 %a 必须是同一个转换(之前两份实现,次正规数上给出两种答案); +-- ③inf/nan 不参与零填充(%020.5a 给 17 个空格加 inf,不是 17 个 0)。 +local AVALS = { "0.0", "-0.0", "1.0", "2.0", "4.0", "0.5", "5e-324", + "2 ^ -1074", "2 ^ -1022", "1 / 3", "math.pi", "1e300", + "math.huge", "-math.huge", "0 / 0", "255.0", "16.0" } +local ASPECS = { "%a", "%#a", "%.0a", "%#.0a", "%.3a", "%#.3a", "%A", "%#A", + "%#.0A", "%20.5a", "%-20.5a", "%#20.5a", "%020.5a", "%q" } +for i = 1, #AVALS do + local x = num(AVALS[i]) + for j = 1, #ASPECS do + local spec = ASPECS[j] + local verb = spec:sub(-1) + PQ("L " .. i .. " " .. j .. " " .. spec .. " " .. AVALS[i], verb, x, + precof(spec), function() + return string.format(spec, x) + end) + end +end + +print("=== M %q 往返:load 回来还是不是原值 ===") +local QV2 = { "0.0", "-0.0", "1.0", "-1.0", "5e-324", "-5e-324", "2 ^ -1022", + "1e308", "1e-308", "math.pi", "1 / 3", "math.maxinteger", + "math.mininteger", "2 ^ 53", "0.1", "1e15", "1e16", "math.huge", + "-math.huge", "0 / 0", "'a\\\\0b'", "'\\\\255'", "''" } +for i = 1, #QV2 do + local e = QV2[i] + local v = num(e) + PQ("M " .. i .. " " .. e, "q", v, nil, function() + local s = string.format("%q", v) + local back = load("return " .. s) + if back == nil then return "NOLOAD " .. show(s) end + local bv = back() + return show(s) .. " -> " .. show(bv) .. " same=" .. tostring(bv == v or (bv ~= bv and v ~= v)) + end) +end + """); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/FuzzTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/FuzzTest.java new file mode 100644 index 0000000..0406589 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/FuzzTest.java @@ -0,0 +1,1153 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * 差分 fuzz —— 四道闸门里的第三道,也是唯一能抓住「不崩、但答案错」的那道。 + *

+ * 手写用例的弱点很明显:它们是照着实现者的理解写的,理解错的地方压根不会被 + * 覆盖到。生成器不知道我们认为 Lua 该怎样,它只会往外吐组合, + * 于是「闭包读到的 upvalue 是哪个」「多重赋值的求值顺序」「返回值在第几层被截断」 + * 这类没有显式断言的语义会被大量随机组合反复踩到。 + *

+ * 两条硬约束,违反任何一条都会把测试变成麻烦制造机: + *

    + *
  • 必须终止。只生成有界的 {@code for}(上界是字面小整数)与 + * {@code ipairs}/pairs,不生成 {@code while}、 + * {@code repeat}、goto; + * 函数调用只允许指向之前已定义完的函数,于是调用图是 DAG, + * 不可能递归、也不可能死循环。
  • + *
  • 必须可复现。种子固定递增,不用 {@code Math.random}。 + * 失败时能靠「第几个种子」精确复现。
  • + *
+ * 出错的用例照样参与比对:两边报同样的错也是一致性的一部分, + * 比「一边报错一边不报」更有价值。 + */ +class FuzzTest { + + /** + * 种子数量。每个种子生成一整段程序,跑两边比对。 + *

+ * 默认值是「CI 跑得完」的量。本地要长跑就用 + * {@code ./mvnw test -Dtest=FuzzTest -Dfuzz.seeds=20000 -Dfuzz.coSeeds=5000} + * 把它调大——生成器是确定性的(种子递增,不用 {@code Math.random}), + * 所以多跑只会多覆盖,不会让已有用例变得不稳。 + */ + private static final int SEEDS = Integer.getInteger("fuzz.seeds", 1500); + + /** 协程模式的种子数 */ + private static final int CO_SEEDS = Integer.getInteger("fuzz.coSeeds", 600); + + /** 协程模式的种子偏移:与常规模式的种子错开,落盘文件名不会撞车 */ + private static final int CO_SEED_BASE = 100000; + + /** 预编译回环的种子偏移,同理 */ + private static final int CHUNK_SEED_BASE = 200000; + + /** + * 预编译回环的种子数。比常规模式少:每颗种子多一次 {@code string.dump}、 + * 一次 {@code load} 与一次 undump,两边加起来要跑的东西翻一倍。 + */ + private static final int CHUNK_SEEDS = Integer.getInteger("fuzz.chunkSeeds", 400); + + @BeforeAll + static void requireReference() { + assumeTrue(ReferenceLua.available(), + "no lua5.4 reference binary; set LUA54_BIN. found: " + ReferenceLua.describe()); + } + + @Test + void generatedProgramsMatchReference() { + for (int seed = 0; seed < SEEDS; seed++) { + String src = new Gen(new Random(seed), false).program(); + String want = Diff.ref(src); + assertCompiles(seed, src, want); + String got = Diff.mine(src); + if (!want.equals(got)) { + // 落到磁盘上:断言信息里的源码被转义过,直接抄出来跑不通 + String path = dump(seed, src, want, got); + assertEquals(want, got, + "seed=" + seed + ",程序已落到 " + path + "\n" + src); + } + } + } + + /** + * 生成的程序必须在参考实现上编得过。 + *

+ * 见过一次才知道这道护栏有多必要:生成器曾经把表构造写成年 + * {@code {'key0' = v}}(字符串键不能裸着当键),又在非变长函数里生成 + * {@code ...}——200 颗种子里 178 个编不过。而编不过的程序在两边是 + * 同一条编译错误,比对照样「一致」,于是这道闸门在几千颗种子上 + * 静默地什么也没测。 + *

+ * 判据是「参考实现的输出里有编译期错误的形状」({@code file:line: ... near + * ...})。运行期错误({@code attempt to ...}、{@code bad argument})不在此列—— + * 那些是期望之中的,两边报同样的错也是一致性的一部分。 + */ + private static void assertCompiles(int seed, String src, String want) { + java.util.regex.Matcher m = SYNTAX_ERROR.matcher(want); + if (m.find()) { + String path = dump(seed, src, want, "(生成器产出了编译不过的程序)"); + fail("生成的程序在参考实现上编译不过,这条种子等于没测。seed=" + seed + + ",程序已落到 " + path + "\n" + m.group() + "\n" + src); + } + } + + /** + * 编译期错误的形状:{@code 源名:行号: ... near 'tok'}。 + *

+ * 判据用「结尾是 {@code near '...'} / {@code near }」而不是「含 + * {@code expected}」:运行期错误里有大量 {@code expected} + * ({@code bad argument #1 to 'upper' (string expected, got nil)}), + * 只要含 {@code expected} 就判失败会误伤一片。而 llex / lparser 那批语法 + * 错误的措辞一律以 {@code near} 收尾——{@code unexpected symbol near}、 + * {@code '}' expected near}、{@code cannot use '...' outside a vararg + * function near}、{@code malformed number near}、{@code unfinished string + * near},全都逃不掉。 + */ + private static final java.util.regex.Pattern SYNTAX_ERROR = + java.util.regex.Pattern.compile("(?m)^\\S+:\\d+: .*\\bnear (?:'[^']*'|)\\s*$"); + + /** + * 同一套生成器,但主 chunk 跑在协程里,并随机撒 {@code coroutine.yield}。 + *

+ * 手工用例只能覆盖「我知道该怎么测」的地方;yield 会落在哪条指令的中间 + * 是生成器说了算的,所以诸如「元方法里 yield」「pcall 里 yield」 + * 「for 迭代器里 yield」这些组合,只有靠随机撒点才撞得出来——而 + * {@code luaV_finishOp} 的每个分支恰好对应其中一种。 + *

+ * 驱动循环必须有界:协程可能永远 yield 下去,两边一起挂住的话 + * 测试就再也跑不完。 + */ + @Test + void generatedCoroutineProgramsMatchReference() { + for (int seed = 0; seed < CO_SEEDS; seed++) { + String src = new Gen(new Random(seed + CO_SEED_BASE), true).program(); + String want = Diff.ref(src); + assertCompiles(seed + CO_SEED_BASE, src, want); + String got = Diff.mine(src); + if (!want.equals(got)) { + String path = dump(seed + CO_SEED_BASE, src, want, got); + assertEquals(want, got, + "seed=" + (seed + CO_SEED_BASE) + ",程序已落到 " + path + "\n" + src); + } + } + } + + /** + * 同一批程序,但先 {@code string.dump} 成二进制 chunk 再 {@code load} 回来跑。 + *

+ * 常规 fuzz 走的一直是文本路径:源码进词法器,编译成 {@code Proto} + * 就直接执行。{@code ldump.c} / {@code lundump.c} 那一对只在 + * {@code DumpTest} 那几十条手工用例里被碰过——阶段 11 挖出来的 + * 「{@code require} 加载不了预编译 chunk」正好就在这个盲区里。 + *

+ * 把随机程序也过一遍 dump / undump,等于让几千个随机的 {@code Proto} + * 去压序列化:常量表的顺序与变长编码、行号表、局部变量与 upvalue 的调试 + * 信息、{@code maxstacksize}、嵌套 proto 的父子关系——这些字段手写用例 + * 顾得过来的组合极其有限。 + *

+ * 程序体包进一个函数再 dump,是为了让它的 upvalue 只剩 {@code _ENV}: + * 顶层变量在函数里就是局部变量,不会变成 upvalue。 + */ + @Test + void generatedChunksSurviveDumpAndUndump() { + for (int seed = 0; seed < CHUNK_SEEDS; seed++) { + String body = new Gen(new Random(seed), false).program(); + /* + * 必须是变长函数:生成器会在主 chunk 里写 ..., + * 而主 chunk 本身就是变长的——包进普通函数会报 + * 「cannot use '...' outside a vararg function」。 + */ + String src = "local __main = function(...)\n" + + Gen.indent(body.stripTrailing()) + "\n" + + "end\n" + + "local __chunk = string.dump(__main)\n" + + "local __loaded = load(__chunk)\n" + /* + * 哨兵。缺了它,「string.dump 一直失败、load 一直返回 nil」 + * 会变成两边一致的输出——用例看着全绿,其实一条 dump/undump + * 都没跑。断言的是参考实现的输出里有这两个 true: + * 只要参考实现能 dump 能 load,本实现做不到就会对不上。 + */ + + "print('dumped', type(__chunk)=='string' and #__chunk>0," + + " 'loaded', __loaded ~= nil)\n" + + "if __loaded then __loaded() end\n"; + String want = Diff.ref(src); + assertTrue(want.contains("dumped\ttrue\tloaded\ttrue"), + "预编译回环没真的跑起来,用例等于没测:\n" + want + "\n" + src); + assertCompiles(seed + CHUNK_SEED_BASE, src, want); + String got = Diff.mine(src); + if (!want.equals(got)) { + String path = dump(seed + CHUNK_SEED_BASE, src, want, got); + assertEquals(want, got, + "seed=" + seed + "(预编译回环),程序已落到 " + path + "\n" + src); + } + } + } + + /** 把失败的用例落盘,附一份两边输出的对照,方便直接拿去跑参考实现 */ + private static String dump(int seed, String src, String want, String got) { + try { + java.nio.file.Path dir = java.nio.file.Paths.get("target/fuzz"); + java.nio.file.Files.createDirectories(dir); + java.nio.file.Files.write(dir.resolve("seed_" + seed + ".lua"), + src.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + java.nio.file.Files.write(dir.resolve("seed_" + seed + ".diff"), + ("--- reference ---\n" + want + "\n--- mine ---\n" + got + "\n") + .getBytes(java.nio.charset.StandardCharsets.UTF_8)); + return dir.resolve("seed_" + seed + ".lua").toString(); + } catch (Exception e) { + return "(落盘失败: " + e + ")"; + } + } + + /* ==================== 生成器 ==================== */ + + /** + * 递归下降的小生成器。 + *

+ * 刻意只覆盖阶段 4 关心的那几件事:局部变量与 upvalue、闭包、有界循环、 + * 变长参数、多重赋值与多重返回、表与元方法的浅层用法。不做字符串库、 + * 不做协程——那些还没有实现,生成了只会得到一堆「未实现」的假失败。 + */ + private static final class Gen { + + private final Random r; + private final List vars = new ArrayList<>(); + private final List funcs = new ArrayList<>(); + private int depth = 0; + private int tmpCounter = 0; + + /** 协程模式:撒 {@code coroutine.yield},并把主 chunk 塞进协程里跑 */ + private final boolean co; + + /** + * 正在生成的那个函数是不是变长的。 + *

+ * 只有变长函数里才写得进 {@code ...}。早先 {@code expression} 无条件 + * 生成 {@code (select('#', ...))},而函数只有 40% 的概率声明 {@code ...}, + * 于是大批程序编不过——报「cannot use '...' outside a vararg function」。 + * 主 chunk 是变长的,所以初值是 {@code true}。 + */ + private boolean vararg = true; + + Gen(Random r, boolean co) { + this.r = r; + this.co = co; + } + + private int pick(int n) { + return r.nextInt(n); + } + + private boolean chance(int pct) { + return r.nextInt(100) < pct; + } + + String program() { + StringBuilder sb = new StringBuilder(); + // 先定义几个函数(彼此只能调更早定义的,保证调用图无环) + int nfunc = 1 + pick(3); + for (int i = 0; i < nfunc; i++) { + sb.append(functionDef()).append('\n'); + } + // 再写主 chunk + StringBuilder main = new StringBuilder(); + int n = 3 + pick(6); + if (co) { + // 协程体是 function(),不是变长函数 + vararg = false; + } + for (int i = 0; i < n; i++) { + main.append(blockStatement(0)).append('\n'); + } + vararg = true; + if (co) { + sb.append("local co = coroutine.create(function()\n") + .append(indent(main.toString().stripTrailing())).append('\n'); + } else { + sb.append(main); + } + // 最后把还在作用域里的变量全打印出来,让「值不对」能被观察到 + sb.append("print("); + int printed = 0; + for (String v : vars) { + if (printed > 0) { + sb.append(", "); + } + sb.append(v); + printed++; + } + if (printed == 0) { + sb.append("'empty'"); + } + sb.append(")\n"); + if (co) { + sb.append("end)\n") + .append("local n = 0\n") + .append("while coroutine.status(co) ~= 'dead' and n < 30 do\n") + .append(" print(coroutine.resume(co, 1))\n") + .append(" n = n + 1\n") + .append("end\n"); + } + return sb.toString(); + } + + /** 函数定义:参数固定 + 变长参数,返回 0~3 个值 */ + private String functionDef() { + String name = "f" + funcs.size(); + int nparams = pick(3); + StringBuilder sb = new StringBuilder(); + sb.append("local function ").append(name).append('('); + List savedVars = new ArrayList<>(vars); + List params = new ArrayList<>(); + for (int i = 0; i < nparams; i++) { + if (i > 0) { + sb.append(", "); + } + String p = "p" + i; + sb.append(p); + params.add(p); + } + boolean va = chance(40); + if (va) { + if (nparams > 0) { + sb.append(", "); + } + sb.append("..."); + } + boolean savedVararg = vararg; + vararg = va; // 函数体里能不能写 ... 由这一次决定 + sb.append(")\n"); + vars.clear(); + vars.addAll(params); + // 一个被捕获的局部变量:闭包语义的核心 + String cap = "c" + funcs.size(); + sb.append(" local ").append(cap).append(" = ").append(atom()).append('\n'); + vars.add(cap); + int n = 2 + pick(4); + for (int i = 0; i < n; i++) { + sb.append(" ").append(blockStatement(0)).append('\n'); + } + // 闭包:捕获 cap,返回它 + if (chance(50)) { + sb.append(" local ").append("k").append(funcs.size()).append(" = function()") + .append(" return ").append(cap).append(" end\n"); + vars.add("k" + funcs.size()); + } + sb.append(" return "); + int nret = pick(4); + if (nret == 0) { + sb.append('\n'); + } else { + for (int i = 0; i < nret; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(expression(0)); + } + sb.append('\n'); + } + sb.append("end\n"); + vararg = savedVararg; + vars.clear(); + vars.addAll(savedVars); + vars.add(name); + funcs.add(name); + return sb.toString(); + } + + /** 语句(函数体内外都能用) */ + private String blockStatement(int d) { + if (depth > 3 || d > 2) { + return simpleStatement(); + } + depth++; + String s; + int kinds = co ? 7 : 6; + switch (pick(kinds)) { + case 6: // 协程模式专属:在任意深度 yield 一下 + s = "coroutine.yield(" + atom() + (chance(30) ? ", " + atom() : "") + ")"; + break; + case 0: // 有界数值 for + s = "for i" + d + " = " + (pick(3)) + ", " + (1 + pick(4)) + + (chance(30) ? ", 2" : "") + " do\n" + + indent(body(d + 1, 1 + pick(2))) + "\nend"; + break; + case 1: // 泛型 for + s = "for _, v" + d + " in ipairs({" + listLiteral() + "}) do\n" + + indent(body(d + 1, 1 + pick(2))) + "\nend"; + break; + case 2: // if / else + s = "if " + comparison() + " then\n" + + indent(body(d + 1, 1 + pick(2))) + + (chance(40) ? "\nelseif " + comparison() + " then\n" + + indent(body(d + 1, 1)) : "") + + (chance(40) ? "\nelse\n" + indent(body(d + 1, 1)) : "") + + "\nend"; + break; + case 3: // do 块(顺带制造一层作用域) + s = "do\n" + indent(body(d + 1, 1 + pick(2))) + "\nend"; + break; + case 4: // pcall 包一层调用 + s = "local ok" + d + ", err" + d + " = pcall(" + + (funcs.isEmpty() ? "function() return 1 end" : call(0)) + + ")\nprint(ok" + d + ")"; + break; + default: + s = simpleStatement(); + break; + } + depth--; + return s; + } + + private String body(int d, int n) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < n; i++) { + if (i > 0) { + sb.append('\n'); + } + sb.append(blockStatement(d)); + } + return sb.toString(); + } + + static String indent(String s) { + StringBuilder sb = new StringBuilder(); + for (String line : s.split("\n")) { + sb.append(" ").append(line).append('\n'); + } + return sb.toString().stripTrailing(); + } + + private String simpleStatement() { + switch (pick(11)) { + case 10: // 语句级的标准库用法(gmatch / next / 非字符串错误值 / pack) + return stdLibOps(); + case 9: // table 库的一串操作(阶段 10 的缺陷就在这块) + return tableOps(); + case 8: // 带元方法的表 + return metaTable(); + case 0: { // 局部声明(可能多重) + int n = 1 + pick(3); + StringBuilder sb = new StringBuilder("local "); + List names = new ArrayList<>(); + for (int i = 0; i < n; i++) { + if (i > 0) { + sb.append(", "); + } + String v = "v" + (tmpCounter++); + sb.append(v); + names.add(v); + } + sb.append(" = "); + for (int i = 0; i < n; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(expression(0)); + } + vars.addAll(names); + return sb.toString(); + } + case 1: { // 多重赋值(交换是重点) + if (vars.isEmpty()) { + return "local z" + (tmpCounter++) + " = 1"; + } + int n = Math.min(1 + pick(3), vars.size()); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < n; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(vars.get(pick(vars.size()))); + } + sb.append(" = "); + for (int i = 0; i < n; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(expression(0)); + } + return sb.toString(); + } + case 2: { // 表写入 + String t = "t" + (tmpCounter++); + vars.add(t); + return "local " + t + " = " + tableLiteral(0) + "\n" + + t + "[" + keyExpr().replace("'", "\"") + "] = " + expression(1); + } + case 3: { // 闭包捕获当前变量 + if (vars.isEmpty()) { + return "local z" + (tmpCounter++) + " = 1"; + } + String k = "k" + (tmpCounter++); + String cap = vars.get(pick(vars.size())); + vars.add(k); + return "local " + k + " = function() return " + cap + " end"; + } + case 4: { // 调用一个之前定义的函数 + if (funcs.isEmpty()) { + return "local z" + (tmpCounter++) + " = 1"; + } + String lhs = "r" + (tmpCounter++); + vars.add(lhs); + return "local " + lhs + " = " + call(0); + } + case 6: { // 通过闭包写 upvalue:开放 upvalue 的写路径 + if (vars.size() < 2) { + return "local z" + (tmpCounter++) + " = 1"; + } + String target = vars.get(pick(vars.size())); + String setter = "s" + (tmpCounter++); + vars.add(setter); + return "local " + setter + " = function(v) " + target + " = v end\n" + + setter + "(" + expression(1) + ")"; + } + case 7: { // 方法定义与调用:self 与 upvalue 混在一起 + String t = "o" + (tmpCounter++); + vars.add(t); + return "local " + t + " = {n = " + (pick(5)) + "}\n" + + "function " + t + ":m(x) return self.n + x end\n" + + "print(" + t + ":m(" + expression(1) + "))"; + } + default: + return "print(" + expression(0) + ")"; + } + } + + private String call(int d) { + String f = funcs.get(pick(funcs.size())); + int n = pick(3); + StringBuilder sb = new StringBuilder(f).append('('); + for (int i = 0; i < n; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(d > 1 ? atom() : expression(d + 1)); + } + return sb.append(')').toString(); + } + + private String expression(int d) { + if (d > 2) { + return atom(); + } + switch (pick(9)) { + case 0: + return atom(); + case 8: // 标准库调用 + return libCall(d); + case 1: { // 二元算术 + if (chance(15)) { + return atom(); // 偶尔退化,免得表达式过长 + } + String[] ops = { "+", "-", "*", "/", "//", "%", "^" }; + return "(" + expression(d + 1) + " " + ops[pick(ops.length)] + + " " + expression(d + 1) + ")"; + } + case 2: { // 拼接 + return "(" + atom() + " .. " + atom() + ")"; + } + case 3: + return tableLiteral(d + 1); + case 4: + return funcs.isEmpty() ? atom() : call(d + 1); + case 5: { // 变长参数个数(只有变长函数里能写) + return vararg ? "(select('#', ...))" : atom(); + } + case 6: { // 取长度 + return "(#" + atom() + ")"; + } + default: + return comparison(); + } + } + + private String comparison() { + String[] ops = { "<", "<=", ">", ">=", "==", "~=" }; + return "(" + atom() + " " + ops[pick(ops.length)] + " " + atom() + ")"; + } + + /* ==================== 标准库 ==================== */ + + /* + * 生成器原本只碰语言本身(闭包 / upvalue / 变长参数 / 多重赋值), + * 而阶段 10 从上游用例里挖出来的缺陷——tunpack 的溢出判断、tmove 的 + * 越界判据、tonumber 的 \0、luaG_tointerror 的 varinfo——全在库 + * 里。语言本身已经很干净了,继续只生成语言构造等于在同一块地里反复锄。 + * 所以这里让表达式也能长出库调用,参数照样是随机的:类型不对、下标越界、 + * 进制越界这些分支会被大量组合踩到。 + */ + + /** 不放在字符串里的格式串,避免转义把生成结果弄成语法错误 */ + private static final String[] FORMATS = { + "%d", "%s", "%x", "%o", "%c", "%5d", "%-5d", "%05d", "%+d", + "%.3f", "%e", "%g", "%q", "%%", "%d%%", + }; + + private static final String[] PATTERNS = { + "%a+", "%d+", "%s*", "[abc]", "[^abc]", "(%a)", "(%d)(%d)", + "^a", "a$", ".", "%.", "%w*", "[a-c]+", + // 位置捕获、平衡匹配、边界匹配:三类以前一个都没生成过 + "()%a+", "%bxy", "%f[%a]%a+", "((%a)(%d))", + }; + + private static final String[] REPLS = { + "x", "%0", "%1", "%1%1", "", "-", + }; + + private static final String[] MATH1 = { + "floor", "ceil", "abs", "type", "tointeger", "sqrt", + }; + + /** + * 只给「必须是字符串」的位置用。 + *

+ * 一律写成 Lua 转义({@code \xNN})而不是真的非 ASCII 字符:源文件落盘时 + * 经过一层编码,字节就不受控了,而这里要的正是具体的字节 + * ——多字节 UTF-8、内嵌 {@code \0}、以及非 UTF-8 的 {@code \xff}。 + */ + private static final String[] SUBJECTS = { + "'abc'", "'abc123'", "' x y '", "''", "'a,b,c'", + "'\\xc3\\xa4\\xc3\\xb6'", "'\\xe4\\xb8\\xad\\xe6\\x96\\x87'", + "'a\\x00b'", "'\\xff\\xfe'", "'A\\xc3\\xa9B'", "'\\x7f'", + }; + + /** {@code string.pack} 的格式串与配套参数:两者的元数必须对齐 */ + private static final String[][] PACKCASES = { + { "i4", "7" }, { "I4", "7" }, { "i2", "-3" }, { "b", "1" }, { "B", "255" }, + { "h", "300" }, { "l", "123456" }, { "j", "-9" }, { "n", "1.5" }, { "d", "0.1" }, + { "f", "2.5" }, { "s1", "'ab'" }, { "s2", "'abc'" }, { "z", "'hi'" }, + { "c3", "'xyz'" }, { "i4", "-1" }, { "=i4", "1" }, + { "!4i4", "5" }, { "Xi4", "6" }, { "i1i1", "1, 2" }, { "i4c2", "3, 'ab'" }, + { " + * 为什么值得单独开一格:阶段 10 从上游用例里挖出来的两个真缺陷—— + * {@code tunpack} 的溢出判断、{@code tmove} 的越界判据——都在 + * {@code ltablib.c},而生成器此前一个 {@code table.*} 都没生成过。 + *

+ * {@code sort} 与 {@code unpack} 一律包在 {@code pcall} 里:比较函数 + * 不自洽会抛 {@code invalid order function for sorting},区间越界会抛 + * {@code too many results}——这些都是期望之中的输出,包住才不会 + * 让整段程序在第一个错误处戛然而止,后面的操作就再也跑不到了。 + */ + private String tableOps() { + String t = "tb" + (tmpCounter++); + vars.add(t); + StringBuilder sb = new StringBuilder("local ").append(t).append(" = ") + .append(tableLiteral(1)).append('\n'); + switch (pick(8)) { + case 7: + /* + * 跨表 move:这才是 tmove 那个「第三条件」生效的地方。 + * 同表 move 走的是 t > e || t <= f 两条,第三条件恒假; + * 跨表时它与前两条无关,一律正向。曾经漏掉它,症状是 + * 内容完全一致、只有 #t 不一样(插入次序反了 → 布局变了)。 + * 所以这里 f/t 取到负数(会把洞开在数组部分之外), + * 并且把目标表按 -2..8 逐格 dump 出来 —— 只比 #t 才看得见。 + */ + sb.append("local tb2").append(t).append(" = ").append(tableLiteral(1)) + .append('\n'); + sb.append("table.move(").append(t).append(", ").append(pick(5) - 2) + .append(", ").append(1 + pick(5)).append(", ").append(pick(5) - 2) + .append(", tb2").append(t).append(")\n"); + sb.append("local d").append(t).append(" = {}\n"); + for (int i = -2; i <= 8; i++) { + sb.append("d").append(t).append("[").append(i + 3).append("] = tostring(") + .append("rawget(tb2").append(t).append(", ").append(i).append("))\n"); + } + sb.append("print(#tb2").append(t).append(", table.concat(d").append(t) + .append(", ','))\n"); + sb.append("print(#").append(t).append(")"); + return sb.toString(); + case 0: + sb.append("table.insert(").append(t).append(", ").append(expression(1)).append(')'); + break; + case 1: + sb.append("table.insert(").append(t).append(", ").append(1 + pick(4)) + .append(", ").append(expression(1)).append(')'); + break; + case 2: + sb.append("local rm").append(t).append(" = table.remove(").append(t) + .append(chance(50) ? ", " + (1 + pick(4)) : "").append(')'); + vars.add("rm" + t); + break; + case 3: + sb.append("table.move(").append(t).append(", ").append(pick(3)) + .append(", ").append(1 + pick(5)).append(", ").append(1 + pick(4)) + .append(')'); + break; + case 4: + sb.append("print(pcall(table.sort, ").append(t); + if (chance(40)) { + // 一半给个不自洽的比较函数,去撞 invalid order function + sb.append(", ").append(chance(50) + ? "function(a, b) return tostring(a) < tostring(b) end" + : "function(a, b) return true end"); + } + sb.append("))"); + break; + case 5: + sb.append("print(pcall(table.unpack, ").append(t).append(", ").append(pick(3)) + .append(", ").append(1 + pick(6)).append("))"); + break; + default: + sb.append("print(pcall(table.concat, ").append(t).append(", ',', ") + .append(pick(3)).append(", ").append(1 + pick(5)).append("))"); + break; + } + sb.append("\nprint(#").append(t).append(")"); + return sb.toString(); + } + + /** + * 标准库里那些「不是一行表达式」的用法。 + *

+ * 为什么单独开一格:{@link #libCall} 只能生成表达式,而 {@code gmatch} + * 的迭代器、{@code next} 的遍历、{@code error} 的非字符串错误值都是 + * 语句级的 —— 手写用例最稀薄的地方,生成器此前一个都没碰过。 + *

+ * 两条硬约束,违反任何一条都会制造假的不一致: + *

    + *
  1. {@code print} 一张表会打出地址,两边必然不同。所以返回表 + * 或函数的值都不直接打印:{@code gmatch} 取迭代次数与拼接结果, + * {@code error} 的错误对象统一挂 {@code __tostring}。
  2. + *
  3. {@code next} / {@code pairs} 的顺序由实现决定,只取条数, + * 不取键的先后。
  4. + *
+ */ + private String stdLibOps() { + String tmp = "q" + (tmpCounter++); + StringBuilder sb = new StringBuilder(); + switch (pick(12)) { + case 11: // xpcall:消息处理函数必须是函数,且校验发生在调用之前 + /* + * 曾经只判「参数够不够两个」,于是 xpcall(f, 5) 照常调 f, + * f 抛错后拿 5 当处理函数。这里把三种形态都铺到: + * 非函数的处理函数、显式 nil、以及干脆一个参数都不给。 + */ + sb.append("print(pcall(xpcall, function() error('E') end, ") + .append(chance(50) ? "5" : "nil").append("))\n"); + sb.append("print(pcall(xpcall))\n"); + sb.append("print(xpcall(function() return ").append(nonVarargExpression(1)) + .append(" end, function(m) return 'H:' .. tostring(m) end))\n"); + sb.append("print(pcall(xpcall, function() error({}) end," + + " function(m) return type(m) end))\n"); + sb.append("print(pcall(xpcall, function() error(setmetatable({}," + + " {__tostring = function() return 'TS' end})) end," + + " function(m) return m end))"); + break; + case 7: // math.random 的序列:必须先播种 + /* + * 不播种的 math.random 每次进程都不一样,所以只能整段出现: + * randomseed 固定种子 → 取一串 → 打印。序列由 xoshiro256** 定死, + * 与实现无关,是个很硬的差分点。 + */ + sb.append("math.randomseed(").append(pick(1000)).append(")\n"); + sb.append("local ").append(tmp).append(" = {}\n"); + for (int i = 0; i < 4; i++) { + String e; + switch (pick(4)) { + case 0: + e = "math.random()"; + break; + case 1: + e = "math.random(100)"; + break; + case 2: + e = "math.random(-5, 5)"; + break; + default: + e = "math.random(" + pick(3) + ", " + (pick(3) + 4) + ")"; + break; + } + sb.append(" ").append(tmp).append("[").append(i + 1).append("] = ") + .append(e).append('\n'); + } + sb.append("print(table.concat(").append(tmp).append(", ','))\n"); + sb.append("print(pcall(math.random, ").append(pick(4) + 3).append(", ") + .append(pick(3)).append("))"); + break; + case 8: // os.time / os.date:一律用 '!' 强制 UTC,否则看机器时区 + sb.append("print(os.time({year = 2020, month = 1, day = 1, hour = 0," + + " min = 0, sec = 0}))\n"); + sb.append("print(os.date('!%Y-%m-%d %H:%M:%S', ") + .append(pick(2000000000)).append("))\n"); + sb.append("local d").append(tmp).append(" = os.date('!*t', ") + .append(pick(2000000000)).append(")\n"); + sb.append("print(d").append(tmp).append(".year, d").append(tmp) + .append(".month, d").append(tmp).append(".day, d").append(tmp) + .append(".hour, d").append(tmp).append(".wday, d").append(tmp) + .append(".yday, d").append(tmp).append(".isdst)\n"); + sb.append("print(pcall(os.date, '!%Y', 'x'), os.difftime(100, 50))"); + break; + case 9: // coroutine:wrap / status / close / isyieldable + sb.append("local co").append(tmp).append(" = coroutine.create(function(a)\n"); + sb.append(" local b = coroutine.yield(a + 1)\n"); + sb.append(" coroutine.yield(b * 2)\n"); + sb.append(" return ").append(nonVarargExpression(1)).append('\n'); + sb.append("end)\n"); + sb.append("print(coroutine.status(co").append(tmp).append(")," + + " coroutine.isyieldable())\n"); + sb.append("print(coroutine.resume(co").append(tmp).append(", 1))\n"); + sb.append("print(coroutine.status(co").append(tmp).append("))\n"); + sb.append("print(coroutine.resume(co").append(tmp).append(", 5))\n"); + sb.append("print(coroutine.resume(co").append(tmp).append("))\n"); + sb.append("print(coroutine.status(co").append(tmp).append("))\n"); + sb.append("local w").append(tmp) + .append(" = coroutine.wrap(function() for i = 1, 3 do" + + " coroutine.yield(i) end end)\n"); + sb.append("print(w").append(tmp).append("(), w").append(tmp) + .append("(), w").append(tmp).append("())\n"); + sb.append("print(pcall(w").append(tmp).append("))\n"); + sb.append("local c").append(tmp) + .append(" = coroutine.create(function() coroutine.yield() end)\n"); + sb.append("coroutine.resume(c").append(tmp).append(")\n"); + sb.append("print(coroutine.close(c").append(tmp).append("))"); + break; + case 10: // debug:只打印确定量,绝不打函数/源名(地址与路径不可比) + sb.append("local function df").append(tmp).append("(a, b) local zz = 1" + + " return zz end\n"); + sb.append("local i").append(tmp).append(" = debug.getinfo(df") + .append(tmp).append(")\n"); + sb.append("print(i").append(tmp).append(".what, i").append(tmp) + .append(".nups, i").append(tmp).append(".nparams, i").append(tmp) + .append(".isvararg, i").append(tmp).append(".linedefined, i") + .append(tmp).append(".currentline, type(i").append(tmp) + .append(".func))\n"); + sb.append("print(debug.getinfo(1).what, debug.getinfo(1).currentline," + + " debug.getinfo(print).what)\n"); + sb.append("local nm").append(tmp).append(", vl").append(tmp) + .append(" = debug.getlocal(1, 1)\n"); + sb.append("print(type(nm").append(tmp).append("), type(vl") + .append(tmp).append("))\n"); + sb.append("local h").append(tmp).append(" = 0\n"); + sb.append("debug.sethook(function() h").append(tmp).append(" = h") + .append(tmp).append(" + 1 end, 'l')\n"); + sb.append("for s = 1, 6 do local _ = s end\n"); + sb.append("debug.sethook()\n"); + sb.append("print(h").append(tmp).append(")\n"); + sb.append("print(#debug.traceback() > 0," + + " #debug.traceback('m') > 0)\n"); + sb.append("print(debug.getmetatable(1)," + + " type(debug.setmetatable({}, {})))"); + break; + case 0: // gmatch:迭代次数 + 拼起来的结果 + // 字符串字面量必须先括起来才能接 ':' —— 实测 'abc':gmatch(...) + // 在 for-in 的表达式表里是 'do' expected near ':' + sb.append("local ").append(tmp).append(" = {}\n"); + sb.append("for w in (").append(strAtom()).append("):gmatch('") + .append(PATTERNS[pick(PATTERNS.length)]).append("') do ") + .append(tmp).append("[#").append(tmp).append(" + 1] = w end\n"); + sb.append("print(#").append(tmp).append(", table.concat(").append(tmp) + .append(", '|'))"); + break; + case 1: // utf8.codes:把码点挨个拼出来 + sb.append("local ").append(tmp).append(" = {}\n"); + sb.append("for _, c in utf8.codes(").append(strAtom()).append(") do ") + .append(tmp).append("[#").append(tmp).append(" + 1] = c end\n"); + sb.append("print(#").append(tmp).append(", table.concat(").append(tmp) + .append(", ','))"); + break; + case 2: // next / pairs:只取条数与「有没有下一个」 + sb.append("local ").append(tmp).append(" = ").append(tableLiteral(0)).append('\n'); + sb.append("local n").append(tmp).append(" = 0\n"); + sb.append("local k").append(tmp).append(" = next(").append(tmp).append(")\n"); + sb.append("while k").append(tmp).append(" ~= nil do n").append(tmp) + .append(" = n").append(tmp).append(" + 1 k").append(tmp) + .append(" = next(").append(tmp).append(", k").append(tmp) + .append(") end\n"); + sb.append("print(n").append(tmp).append(", next(").append(tmp) + .append(") ~= nil, rawlen(").append(tmp).append("))"); + break; + case 3: // 非字符串错误值:__tostring / nil / 带 level 的 error / xpcall + sb.append("local e").append(tmp).append(" = setmetatable({}, ") + .append("{__tostring = function() return 'EO' end})\n"); + sb.append("print(pcall(error, e").append(tmp).append("))\n"); + sb.append("print(pcall(function() error(nil) end))\n"); + sb.append("print(pcall(function() error('m', 2) end))\n"); + sb.append("print(xpcall(function() error('x') end,") + .append(" function(m) return 'H:' .. tostring(m) end))"); + break; + case 4: { // pack / packsize / unpack 往返 + String[] c = PACKCASES[pick(PACKCASES.length)]; + sb.append("print(pcall(function()\n"); + sb.append(" local p").append(tmp).append(" = string.pack('").append(c[0]) + .append("', ").append(c[1]).append(")\n"); + sb.append(" print(#p").append(tmp).append(", string.packsize('").append(c[0]) + .append("'))\n"); + sb.append(" print(string.unpack('").append(c[0]).append("', p") + .append(tmp).append("))\n"); + sb.append("end))"); + break; + } + case 5: // table.pack / select('#'):n 是精确的,#p 是有边界语义的,只打前者 + sb.append("local p").append(tmp).append(" = table.pack(").append(listLiteral()) + .append(")\n"); + sb.append("print(p").append(tmp).append(".n, select('#', table.unpack(p") + .append(tmp).append(")))"); + break; + case 6: // gsub 的函数替换:返回 nil 表示保留原匹配 + sb.append("local s").append(tmp).append(" = ").append(strAtom()).append('\n'); + sb.append("local r, n = string.gsub(s").append(tmp).append(", '") + .append(PATTERNS[pick(PATTERNS.length)]) + .append("', function(c) return ") + .append(chance(50) ? "c .. '!'" : "nil").append(" end)\n"); + sb.append("print(r, n)"); + break; + default: // string.find 的 plain 与 init 两个参数 + sb.append("print(pcall(string.find, ").append(strAtom()).append(", '") + .append(PATTERNS[pick(PATTERNS.length)]).append("', ") + .append(pick(4)).append(chance(50) ? ", true" : "") + .append("))"); + break; + } + return sb.toString(); + } + + /** 挂一两个元方法的新表:元方法派发与 luaV_finishOp 的组合全靠这里踩 */ + private String metaTable() { + String t = "m" + (tmpCounter++); + StringBuilder mm = new StringBuilder(); + int n = 1 + pick(2); + for (int i = 0; i < n; i++) { + if (i > 0) { + mm.append(", "); + } + switch (pick(8)) { + case 0: + mm.append("__add = function(a, b) return 7 end"); + break; + case 1: + mm.append("__concat = function(a, b) return 'C' end"); + break; + case 2: + mm.append("__len = function() return 3 end"); + break; + case 3: + mm.append("__index = function(t, k) return tostring(k) .. '!' end"); + break; + case 4: + mm.append("__call = function(s, x) return x end"); + break; + case 5: + mm.append("__lt = function(a, b) return true end"); + break; + case 6: + mm.append("__unm = function(a) return -1 end"); + break; + default: + mm.append("__tostring = function() return 'T' end"); + break; + } + } + vars.add(t); + return "local " + t + " = setmetatable({}, {" + mm + "})"; + } + + private String keyExpr() { + switch (pick(3)) { + case 0: + return String.valueOf(pick(4) + 1); + case 1: + return "'" + ("key" + pick(3)) + "'"; + default: + return String.valueOf(pick(3) + 1); + } + } + + private String tableLiteral(int d) { + int n = pick(4); + StringBuilder sb = new StringBuilder("{"); + for (int i = 0; i < n; i++) { + if (i > 0) { + sb.append(", "); + } + if (chance(50)) { + /* + * 一律写成 [k] = v。 + * + * 早先只对数字键加方括号,字符串键直接写成 'key0' = v—— + * 那是语法错误(只有名字才能裸着当键)。这个一处 + * 就让近半数的生成程序编不过,而编不过的程序在两边都是 + * 同一条编译错误,比对「一致」却什么也没测到。 + */ + sb.append('[').append(keyExpr()).append("] = "); + } + sb.append(d > 1 ? atom() : expression(d + 1)); + } + return sb.append('}').toString(); + } + + private String listLiteral() { + int n = 1 + pick(4); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < n; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(atom()); + } + return sb.toString(); + } + + /** + /** + * 在非变长函数体里生成表达式。 + *

+ * {@link #expression} 会写 {@code (select('#', ...))},而协程体造的是 + * {@code function(a)} —— 不是变长函数,写出来就是 + * {@code cannot use '...' outside a vararg function}。 + */ + private String nonVarargExpression(int d) { + boolean saved = vararg; + vararg = false; + try { + return expression(d); + } finally { + vararg = saved; + } + } + + /** + * 一个大概是字符串的原子。 + *

+ * 七成给 {@link #SUBJECTS}(具体字节受控),三成给 {@link #atom()} + * ——数字会被 {@code luaL_checklstring} 转成字符串,nil / 布尔会报错, + * 这两条路都值得走一走。 + */ + private String strAtom() { + if (chance(70)) { + return SUBJECTS[pick(SUBJECTS.length)]; + } + /* + * 需要字符串的地方不喂 {@code (0/0)}:NaN 的符号是 libc 定的 + * (约定 #96),glibc 给 {@code -nan}、Apple libc 给 {@code nan}, + * 于是「这个字符串有几个字符」两边就差一个 —— {@code gsub} 的第二 + * 个返回值(匹配次数)因此是 4 对 3,而数字是补不回来的(归一化 + * 只够得着字面,够不着长度,约定 #104)。 + */ + for (int i = 0; i < 8; i++) { + String a = atom(); + if (!a.equals("(0/0)")) { + return a; + } + } + return "'s0'"; + } + + private String atom() { + switch (pick(12)) { + case 9: // 无穷:tonumber / string.format('%d') / 位运算都会在这里翻车 + return pick(2) == 0 ? "(1/0)" : "(-1/0)"; + case 10: { // 数字形状的字符串:tonumber 与算术的隐式转换 + String[] ss = { "'3.5'", "'-7'", "'0x10'", "' 12 '", "'1e2'", "'abc'" }; + return ss[pick(ss.length)]; + } + case 11: // 整数上下界:溢出那条路 + return pick(2) == 0 ? "math.maxinteger" : "math.mininteger"; + case 0: + return String.valueOf(pick(21) - 10); // 小整数,含负数 + case 1: + return String.valueOf(pick(21) - 10) + ".5"; // 浮点 + case 2: + return String.valueOf(pick(5)) + ".0"; // 整值浮点 + case 3: + return "'" + ("s" + pick(5)) + "'"; + case 4: + return pick(2) == 0 ? "true" : "false"; + case 5: + return "nil"; + case 6: + return "(0/0)"; + case 7: + return vars.isEmpty() ? "0" : vars.get(pick(vars.size())); + default: + return funcs.isEmpty() ? "0" : funcs.get(pick(funcs.size())); + } + } + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/GcTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/GcTest.java new file mode 100644 index 0000000..3198a54 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/GcTest.java @@ -0,0 +1,172 @@ +package com.github.esrrhs.fakescript.lua; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +/** + * {@code __gc} 的差分用例 —— 阶段 6 的第三块。 + *

+ * 与弱表那批一样,这批依赖 JVM 真的把对象收掉,而且多依赖一样: + * {@code finalize} 必须真的被调到。所以这里只写能稳定复现的形态, + * 不写「正好卡在某一轮收集上」的用例——那是写给自己找麻烦。 + *

+ * 三个被实测钉住的事实,是这批用例的全部依据: + *

    + *
  • {@code __gc} 只跑一次;
  • + *
  • {@code __gc} 里的错误静默吞掉,既不打印也不传播;
  • + *
  • 设元表之后才往元表里塞 {@code __gc} 不生效。
  • + *
+ */ +class GcTest { + + @TestFactory + List gcScriptsMatchReference() { + List tests = new ArrayList<>(); + for (int i = 0; i < SCRIPTS.length; i++) { + String src = SCRIPTS[i]; + int n = i; + tests.add(DynamicTest.dynamicTest("gc[" + n + "]", + () -> Diff.assertMatches(src))); + } + return tests; + } + + /** + * 不依赖收集时机的那一半。 + *

+ * 这一半无论 JVM 收不收都必须成立——它们红了是逻辑写错了,不是环境的问题。 + */ + @TestFactory + List gcRulesWithoutCollection() { + List tests = new ArrayList<>(); + for (int i = 0; i < NO_GC.length; i++) { + String src = NO_GC[i]; + int n = i; + tests.add(DynamicTest.dynamicTest("gcNoGc[" + n + "]", + () -> Diff.assertMatches(src))); + } + return tests; + } + + private static final String[] NO_GC = { + + """ + -- 设元表当下没有 __gc → 之后补上也不生效(实测参考实现如此) + local mt = {} + local t = setmetatable({}, mt) + mt.__gc = function() print("SHOULD NOT RUN") end + collectgarbage() + print("done") + """, + + """ + -- __gc 不是函数:静默忽略 + local t = setmetatable({}, {__gc = 5}) + collectgarbage() + print("ok", type(t)) + """, + + """ + -- 还活着的对象不会被终结 + local t = setmetatable({}, {__gc = function() print("SHOULD NOT RUN") end}) + collectgarbage() + print("alive", type(t)) + """, + + """ + -- __gc 是 nil:什么也不做 + local t = setmetatable({}, {__gc = nil}) + collectgarbage() + print("ok") + """, + + """ + -- 反复 collectgarbage,__gc 只跑一次 + local n = 0 + do + local t = setmetatable({}, {__gc = function() n = n + 1 end}) + end + collectgarbage() + collectgarbage() + collectgarbage() + print("times", n) + """, + }; + + private static final String[] SCRIPTS = { + + """ + -- 最基本的:对象不可达后 __gc 跑一次 + local n = 0 + do + local t = setmetatable({}, {__gc = function(o) n = n + 1 end}) + end + collectgarbage() + print("called", n) + """, + + """ + -- 参数就是那个对象 + do + local t = setmetatable({}, {__gc = function(o) print("arg", type(o)) end}) + end + collectgarbage() + """, + + """ + -- __gc 里能看见全局 + do + local t = setmetatable({}, {__gc = function(o) print("sees", type(_G)) end}) + end + collectgarbage() + """, + + """ + -- __gc 里报错:静默吞掉,后面的代码照常跑 + do + local t = setmetatable({}, {__gc = function(o) error("boom in gc") end}) + end + collectgarbage() + print("after") + """, + + """ + -- 一批对象的 __gc 都会跑到。 + -- 只数个数、不比对顺序:Lua 没规定顺序,参考实现给出的是 3,2,1, + -- 而本实现的顺序取决于 JVM 终结线程把哨兵入队的先后,本身就不该被测。 + local n = 0 + for i = 1, 3 do + local t = setmetatable({}, {__gc = function(o) n = n + 1 end}) + end + collectgarbage() + print("count", n) + """, + + """ + -- 没有 collectgarbage:退出时照样跑 + do + local t = setmetatable({}, {__gc = function(o) print("at exit") end}) + end + print("before exit") + """, + + """ + -- 弱表与 __gc 同时用:__gc 照常会跑。 + -- 这里不断言弱表里那一项还在不在:参考实现里带终结器的对象在 + -- 终结器跑掉之前会被 mark 成活的,于是那一轮弱条目还在(实测 false); + -- 本实现靠 JVM 的 WeakReference 判死活,做不到「死过一次又活过来」, + -- 弱条目会在同一轮就被摘掉。这是已知的一处不一致,README 里记着。 + local n = 0 + local wt = setmetatable({}, {__mode = "k"}) + do + local t = setmetatable({}, {__gc = function(o) n = n + 1 end}) + wt[t] = "v" + end + collectgarbage() + print("gc ran", n) + """, + }; +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/IoLibTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/IoLibTest.java new file mode 100644 index 0000000..512f19a --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/IoLibTest.java @@ -0,0 +1,1700 @@ +package com.github.esrrhs.fakescript.lua; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; + +/** + * io 库的差分用例 —— 阶段 7。 + *

+ * 选例的原则是「只比对平台无关的那部分」: + *

    + *
  • 不比对地址。{@code print(io.stdout)} 给的是 + * {@code file (0x...)},两边不可能相同,差分比对会把它抹成 + * {@code 0xADDR}——所以这种用例只验证「形状」,验证不了数值。
  • + *
  • 不比对 {@code io.popen} 拿 errno 的路径:Java 起进程失败的 + * errno 与 C 的 {@code popen} 不同。
  • + *
  • 临时文件用固定路径:两边跑在同一台机器上,同一个路径对两边 + * 都可见;每个用例开头先 {@code os.remove} 掉,避免相互干扰。
  • + *
+ *

+ * 真正盯住的是这几处容易翻车的地方: + *

    + *
  • {@code io.write} 的返回值是句柄(不是 true), + * {@code io.read} 同理;
  • + *
  • {@code luaL_fileresult} 的三元组:{@code io.open} 失败时给 + * {@code nil, "<路径>: No such file or directory", 2};
  • + *
  • {@code f:close()} 对标准文件给 + * {@code nil, "cannot close standard file"} 两个值,不是报错;
  • + *
  • 已关闭的句柄再用报 {@code attempt to use a closed file};
  • + *
  • {@code io.lines(文件)} 返回四个值(第四个是待关闭变量), + * 迭代结束会把文件关掉,之后 {@code io.type} 应是 {@code closed file};
  • + *
  • {@code f:read("n")} 的「读一个合法数字前缀」行为: + * {@code "3x"} 读成 {@code 3} 且把 {@code x} 留在流里。
  • + *
+ */ +class IoLibTest { + + @TestFactory + List ioScriptsMatchReference() { + List tests = new ArrayList<>(); + add(tests, "type", TYPES); + add(tests, "open", OPEN); + add(tests, "read", READ); + add(tests, "write", WRITE); + add(tests, "lines", LINES); + add(tests, "seek", SEEK); + add(tests, "err", ERRORS); + add(tests, "zero", ZERO); + add(tests, "proc", PROC); + add(tests, "mode", MODE); + add(tests, "gaps", GAPS); + add(tests, "errpath", ERRPATH); + return tests; + } + + private static void add(List tests, String tag, String[] srcs) { + for (int i = 0; i < srcs.length; i++) { + String src = srcs[i]; + int n = i; + tests.add(DynamicTest.dynamicTest(tag + "[" + n + "]", () -> Diff.assertMatches(src))); + } + } + + /* ==================== 类型与句柄 ==================== */ + + private static final String[] TYPES = { + """ + print(type(io.stdout), type(io.stdin), type(io.stderr)) + print(io.type(io.stdout), io.type(io.stdin), io.type(io.stderr)) + print(io.type(42), io.type("x"), io.type({}), io.type(nil), io.type(print)) + local mt = getmetatable(io.stdout) + print(mt ~= nil, type(mt.__index), type(mt.__gc), type(mt.__close), type(mt.__tostring)) + print(mt == getmetatable(io.stdin)) + """, + + """ + -- io.write / io.read 返回的是句柄本身,不是 true + local r = io.write("") + print(io.type(r)) + print(io.stdout == r) + """, + + """ + -- 标准文件关不掉:给两个值,不报错 + print(io.stdout:close()) + print(io.stdin:close()) + print(io.type(io.stdout)) + print(io.stdout:close()) + """, + + """ + -- 方法表里没有的东西索引出来是 nil;userdata 不能取长度 + print(io.stdout.nosuchmethod) + print(io.stdout.read == io.stdout.read) + print(pcall(function() return #io.stdout end)) + print(pcall(function() io.stdout.x = 1 end)) + """, }; + + /* ==================== 打开 ==================== */ + + private static final String[] OPEN = { + """ + local p = "/tmp/fkio_open1.txt" + os.remove(p) + local f = io.open(p, "w") + print(io.type(f)) + f:write("hello\\n") + f:close() + print(io.type(f)) + local g = io.open(p, "r") + print(g:read("l")) + g:close() + os.remove(p) + """, + + """ + -- 打不开:三元组 nil, "<路径>: No such file or directory", 2 + print(io.open("/nonexistent_dir_xyz/zzz.txt")) + print(io.open("/nonexistent_dir_xyz/zzz.txt", "w")) + local ok, e = io.open("/etc/hostname", "wb") + print(ok ~= nil) + """, + + """ + -- 模式校验:只认 [rwa]%+?[b]* + print(pcall(io.open, "/tmp/fkio_open3.txt", "bogus")) + print(pcall(io.open, "/tmp/fkio_open3.txt", "")) + print(pcall(io.open, "/tmp/fkio_open3.txt", "rw")) + print(pcall(io.open, "/tmp/fkio_open3.txt", "rb+")) + print(pcall(io.open)) + """, + + """ + -- tmpfile 能读能写 + local f = io.tmpfile() + print(io.type(f)) + f:write("abc") + print(f:seek("set", 0)) + print(f:read("a")) + print(f:close()) + """, + + """ + -- io.input / io.output 换默认流 + local p = "/tmp/fkio_open5.txt" + os.remove(p) + local o = io.output(p) + print(io.type(o)) + o:write("line1\\nline2\\n") + io.output(io.stdout) + print("to stdout") + local i = io.input(p) + print(io.type(i), i:read("l")) + io.input(io.stdin) + os.remove(p) + """, + + """ + -- popen + local f = io.popen("echo hi") + print(io.type(f)) + print(f:read("l")) + print(f:close()) + print(io.popen("echo hi", "x")) + """, }; + + /* ==================== 读 ==================== */ + + private static final String[] READ = { + """ + local p = "/tmp/fkio_read1.txt" + os.remove(p) + local f = io.open(p, "w") + f:write("a\\nb\\nc\\n") + f:close() + f = io.open(p, "r") + print(f:read("l"), f:read("l"), f:read("l")) + print(f:read("l")) + print(f:read("a")) + f:close() + os.remove(p) + """, + + """ + local p = "/tmp/fkio_read2.txt" + os.remove(p) + local f = io.open(p, "w") + f:write("a\\nb\\nc\\n") + f:close() + f = io.open(p, "r") + -- 'L' 保留换行,'a' 读到尾 + print(f:read("L")) + print(f:read("a")) + f:close() + print(f:read("a")) + os.remove(p) + """, + + """ + local p = "/tmp/fkio_read3.txt" + os.remove(p) + local f = io.open(p, "w") + f:write("3.5 -7 0x10 1e3 3x - .5") + f:close() + f = io.open(p, "r") + for i = 1, 8 do print(f:read("n")) end + print(f:read("n")) + print(f:read("a")) + f:close() + os.remove(p) + """, + + """ + local p = "/tmp/fkio_read4.txt" + os.remove(p) + local f = io.open(p, "w") + f:write("0123456789") + f:close() + f = io.open(p, "r") + print(f:read(0)) -- 空串表示还没到 EOF + print(f:read(4)) + print(f:read(100)) + print(f:read(0)) -- nil 表示到 EOF 了 + print(f:read(1)) + f:close() + os.remove(p) + """, + + """ + -- 无参 io.read 从 stdin 读;stdin 是 /dev/null,直接 EOF + print(io.read()) + print(io.read("a")) + print(io.read("l")) + print(io.read("n")) + print(io.read(0)) + print(io.read(3)) + """, + + """ + -- 多个读格式一次返回多个结果 + local p = "/tmp/fkio_read6.txt" + os.remove(p) + local f = io.open(p, "w") + f:write("12 abc\\n34\\n") + f:close() + f = io.open(p, "r") + print(f:read("n", "l", "n")) + f:close() + os.remove(p) + """, + + """ + -- 读格式带 '*' 前缀(兼容写法) + local p = "/tmp/fkio_read7.txt" + os.remove(p) + local f = io.open(p, "w") + f:write("hi\\n") + f:close() + f = io.open(p, "r") + print(f:read("*l")) + f:close() + os.remove(p) + """, }; + + /* ==================== 写 ==================== */ + + private static final String[] WRITE = { + """ + -- 数字按 %.14g 写;整数不带 .0 + io.write("a", 1, "b", 1.5, " ", -0.0, " ", 1e300, "\\n") + io.write(7, "\\n") + print(io.write("x", "\\n")) + """, + + """ + local p = "/tmp/fkio_write2.txt" + os.remove(p) + local f = io.open(p, "w") + print(f:write("one\\n")) + print(f:write("two\\n")) + f:close() + local g = io.open(p, "r") + print(g:read("a")) + g:close() + os.remove(p) + """, + + """ + -- 'a' 是追加:重写不会截断 + local p = "/tmp/fkio_write3.txt" + os.remove(p) + local f = io.open(p, "w") + f:write("base\\n") + f:close() + f = io.open(p, "a") + f:write("more\\n") + f:close() + f = io.open(p, "r") + print(f:read("a")) + f:close() + -- 'w' 会截断 + f = io.open(p, "w") + f:write("new\\n") + f:close() + f = io.open(p, "r") + print(f:read("a")) + f:close() + os.remove(p) + """, + + """ + -- 写字节:Lua 字符串可以含 \\0 + local p = "/tmp/fkio_write4.txt" + os.remove(p) + local f = io.open(p, "wb") + f:write("a\\0b") + f:close() + f = io.open(p, "rb") + local s = f:read("a") + print(#s, s:byte(1), s:byte(2), s:byte(3)) + f:close() + os.remove(p) + """, }; + + /* ==================== lines ==================== */ + + private static final String[] LINES = { + """ + local p = "/tmp/fkio_lines1.txt" + os.remove(p) + local f = io.open(p, "w") + f:write("x\\ny\\nz\\n") + f:close() + f = io.open(p, "r") + local r = {} + for l in f:lines() do r[#r+1] = l end + print(table.concat(r, ",")) + print(io.type(f)) + f:close() + os.remove(p) + """, + + """ + -- io.lines(文件) 返回四个值,迭代结束自动关文件 + local p = "/tmp/fkio_lines2.txt" + os.remove(p) + local f = io.open(p, "w") + f:write("1\\n2\\n3\\n") + f:close() + local g = io.open(p, "r") + local r = {} + for l in g:lines("l") do r[#r+1] = l end + print(table.concat(r, ",")) + g:close() + local it, s, c, cl = io.lines(p) + print(type(it), s, c, io.type(cl)) + f = io.open(p, "r") + local h = f + for l in io.lines(p) do r[#r+1] = l end + print(table.concat(r, ",")) + print(io.type(h)) + os.remove(p) + """, + + """ + -- lines 的读格式会传给每次迭代 + local p = "/tmp/fkio_lines3.txt" + os.remove(p) + local f = io.open(p, "w") + f:write("10 20\\n30 40\\n") + f:close() + local r = {} + for a, b in io.lines(p, "n", "n") do r[#r+1] = a .. "/" .. b end + print(table.concat(r, " ")) + os.remove(p) + """, + + """ + -- 文件不存在时 io.lines 直接报错(opencheck),不是返回 nil + print(pcall(io.lines, "/nonexistent_dir_xyz/nope.txt")) + """, }; + + /* ==================== seek / setvbuf ==================== */ + + private static final String[] SEEK = { + """ + local p = "/tmp/fkio_seek1.txt" + os.remove(p) + local f = io.open(p, "w") + f:write("0123456789") + f:close() + f = io.open(p, "r") + print(f:seek("set", 3)) + print(f:read(2)) + print(f:seek("cur", 0)) + print(f:seek("cur", -2)) + print(f:seek("end")) + print(f:seek("end", -3)) + print(f:read(3)) + print(f:seek("set")) + f:close() + os.remove(p) + """, + + """ + -- 管道不能定位。只测 stdout:参考实现的 stdin 被定向到 /dev/null, + -- 而 /dev/null 是可定位的字符设备(lseek 恒返回 0), + -- 本实现的 stdin 是内存流,必然 ESPIPE——那是流的来源不同, + -- 不是 io 库的行为差异,所以这一条不比。 + print(io.stdout:seek()) + print(io.stdout:seek("set", 0)) + print(io.stdout:seek("end")) + """, + + """ + local p = "/tmp/fkio_seek3.txt" + os.remove(p) + local f = io.open(p, "w") + f:write("hello") + f:close() + f = io.open(p, "r+") + print(f:read(2)) + print(f:seek("set", 0)) + print(f:write("HE")) + print(f:seek("set", 0)) + print(f:read("a")) + f:close() + os.remove(p) + """, + + """ + -- setvbuf 只认模式名,总是成功 + local p = "/tmp/fkio_seek4.txt" + os.remove(p) + local f = io.open(p, "w") + print(f:setvbuf("no")) + print(f:setvbuf("full", 4096)) + print(f:setvbuf("line")) + print(pcall(f.setvbuf, f, "bogus")) + print(pcall(f.setvbuf, f)) + f:close() + os.remove(p) + """, + + """ + print(io.flush()) + local p = "/tmp/fkio_seek5.txt" + os.remove(p) + local f = io.open(p, "w") + print(f:flush()) + f:close() + os.remove(p) + """, }; + + /* ==================== 报错 ==================== */ + + private static final String[] ERRORS = { + """ + -- 已关闭的句柄 + local p = "/tmp/fkio_err1.txt" + os.remove(p) + local f = io.open(p, "w") + f:write("x") + f:close() + print(pcall(f.write, f, "y")) + print(pcall(f.read, f, "l")) + print(pcall(f.lines, f)) + print(pcall(f.seek, f)) + print(pcall(f.close, f)) + os.remove(p) + """, + + """ + -- 参数类型 + print(pcall(io.type)) + print(pcall(io.open, 42)) + print(pcall(io.open, "x", 42)) + print(pcall(io.write, {})) -- 表不能 tostring 成字符串 + print(pcall(io.write, true)) + """, + + """ + -- 读格式不认识 + local p = "/tmp/fkio_err3.txt" + os.remove(p) + local f = io.open(p, "w") + f:write("hi\\n") + f:close() + f = io.open(p, "r") + print(pcall(f.read, f, "z")) + print(pcall(f.read, f, "")) + print(pcall(f.read, f, "*z")) + print(f:read("l")) + f:close() + os.remove(p) + """, + + """ + -- 默认流被关掉之后再用 + local p = "/tmp/fkio_err4.txt" + os.remove(p) + local i = io.input(p) + print(io.type(i)) + i:close() + print(pcall(io.read)) + print(pcall(io.lines)) + os.remove(p) + """, + + """ + -- 错误里的函数名:方法调用时参数号要减一 + local p = "/tmp/fkio_err5.txt" + os.remove(p) + local f = io.open(p, "w") + print(pcall(f.read, f, 42)) + print(pcall(f.seek, f, "bogus")) + print(pcall(io.open, nil)) + f:close() + os.remove(p) + """, }; + + /** + * 零长度 I/O。 + *

+ * 咬出来的是 {@code f:write("")}:写 0 个字节在 C 里一定成功 —— + * {@code fwrite(s, 1, 0, f)} 返回 0、正好等于 {@code l},于是 {@code g_write} + * 走成功分支,把文件自己返回来。我们的 {@code LuaFile.write} 是先判 + * {@code !writable} 再看长度,于是往只读文件写空串也报了 + * {@code Bad file descriptor}。 + *

+ * 能把它逼出来的只有这一个组合:往可写文件写空串两边都对,往只读文件写 + * 非空串两边都报 EBADF。这也说明为什么「只看返回值对不对」不够 —— + * 得把「可写 × 只读 × 空 × 非空」四个格子全铺开。 + *

+ * 读那一侧 {@code f:read(0)} 走的是另一条路:{@code g_read} 对数字 0 + * 调的不是 {@code read_chars} 而是 {@code test_eof}(先 {@code getc} 再 + * {@code ungetc}),所以在只写文件上照样失败、给三元组。我们的 {@code read} + * 本来就是先 {@code done >= len} 再判 {@code readable},因此没踩到 —— + * 但这两条路不对称,写那边补的这一刀别照着挪到读这边来。 + */ + private static final String[] ZERO = { + """ + -- 往只读文件写空串是成功的(fwrite 的 l == 0) + local p = "/tmp/fkio_zero.txt" + os.remove(p) + local w = io.open(p, "w") + w:write("hello") + w:close() + local ro = io.open(p) + print("W1", ro:write("") == ro) + print("W2", select("#", ro:write(""))) + print("W3", ro:write() == ro) + print("W4", select("#", ro:write("a"))) + print("W5", ro:write("a") == ro) + print("W6", pcall(function() return ro:write("a") end)) + ro:close() + os.remove(p) + """, + + """ + -- 标准流:stdin 是只读的,写空串照样成功 + print("S1", io.stdin:write("") == io.stdin) + print("S2", io.stdout:write("") == io.stdout) + print("S3", io.stderr:write("") == io.stderr) + print("S4", io.write("") == io.stdout) + print("S5", io.write() == io.stdout) + print("S6", select("#", io.write(""))) + print("S7", select("#", io.write("x", ""))) + """, + + """ + -- 读 0 字节:g_read 对 0 走 test_eof(getc + ungetc),不是 fread + local p = "/tmp/fkio_zero2.txt" + os.remove(p) + local w = io.open(p, "w") + w:write("abc") + w:close() + local ro = io.open(p) + print("R1", ro:read(0) == "") + print("R2", select("#", ro:read(0))) + local wo = io.open(p, "a") + print("R3", select("#", wo:read(0))) + print("R4", wo:read(0)) + print("R5", pcall(function() return wo:read("l") end)) + wo:close() + ro:close() + os.remove(p) + """, + + """ + -- 多参数里混空串,以及关闭之后 + local p = "/tmp/fkio_zero3.txt" + os.remove(p) + local w = io.open(p, "w") + print("M1", w:write("", "", "") == w) + print("M2", w:write("a", "", "b") == w) + print("M3", w:write(1, "", 2.5) == w) + print("M4", w:close()) + print("M5", io.open(p):read("a")) + print("M6", pcall(function() return w:write("") end)) + print("M7", pcall(function() return w:write() end)) + os.remove(p) + """, }; + + + /** + * {@code io.popen} / {@code io.tmpfile} / {@code setvbuf}。 + *

+ * 这一组之前只有 1~2 个用例,是 io 库里最薄的一块。铺开之后咬出一处真 bug: + * {@code io.popen("exit 3"):close()} 参考实现给 {@code nil, "exit", 3},我们给的是 + * {@code false, "exit", 3} —— {@code luaL_execresult} 失败时压的是 + * {@code luaL_pushfail},而 5.4 里它就是 nil + * ({@code lauxlib.h: #define luaL_pushfail(L) lua_pushnil(L)})。 + * 只有 {@code io.popen} 的收尾走 {@code LuaAux.execResult}, + * {@code os.execute} 有自己一份、本来就是对的,所以这个 bug 在 os 那边看不出来。 + *

+ * 三样东西不比,每一条都是实测出来的,不是托辞: + *

    + *
  • 子进程的输出/错误必须在命令里重定向掉。继承来的 stdout / stderr + * 不进本实现的捕获缓冲(Java 的子进程继承的是 JVM 真那个 stderr), + * 两边对不齐。所以这里写的是 {@code cat > /dev/null} 与 + * {@code 2>/dev/null},而不是 {@code cat} 与裸命令。
  • + *
  • 不排空管道就 {@code close}:参考实现拿到的是 + * {@code nil, "signal", 13}({@code echo} 被 SIGPIPE 杀掉), + * 我们给 {@code true, "exit", 0}。这是竞态——取决于子进程有没有 + * 先写完,逐次运行结果不稳定,不能进比对。
  • + *
  • {@code setvbuf} 的负的 / 极大的 size。 C 把它转成 + * {@code size_t} 交给 libc 的 {@code setvbuf},能不能分配出来是 libc + * 自己定的:同一台机器上 {@code 2^31} 失败而 {@code 2^40} 成功, + * {@code "no"} 配 {@code -1} 成功而 {@code "line"} 配 {@code -1} 失败。 + * Java 侧没法合理建模。
  • + *
+ */ + private static final String[] PROC = { + """ + -- 这一片刻意把子进程的输出/错误都在命令里重定向掉:它只关心「读到了 + -- 什么、退出码是多少」。继承来的 stdout/stderr 怎么落另有 errpath 段 + -- (那边是能对的 —— 本实现把它们转到了 L.out / L.err,正是捕获缓冲)。 + local out = {} + local function c(tag, f, ...) + local r = table.pack(pcall(f, ...)) + local s = tag + for i = 1, r.n do s = s .. "\t" .. tostring(r[i]) end + out[#out + 1] = s + end + local function say(s) out[#out + 1] = s end + local p = io.popen("echo hi") + say("p1 " .. io.type(p) .. " " .. tostring(p:read("l")) + .. " " .. tostring(p:read("a"))) + c("p2", p.close, p) + p = io.popen("printf abc") + say("p3 " .. tostring(p:read("a"))) + c("p4", p.close, p) + local w = io.popen("cat > /dev/null", "w") + say("w1 " .. tostring(w:write("abc") == w)) + c("w2", w.close, w) + -- 非零退出码是 nil 不是 false(luaL_pushfail 在 5.4 里就是 lua_pushnil) + for _, code in ipairs({ 0, 3, 7 }) do + local f = io.popen("exit " .. code) + c("e" .. code, f.close, f) + end + local ef = io.popen("false") + c("ef", ef.close, ef) + local nx = io.popen("no_such_cmd_xyz_12345 2>/dev/null") + say("n1 " .. tostring(nx ~= nil)) + if nx then + say("n2 [" .. tostring(nx:read("a")) .. "]") + c("n3", nx.close, nx) + end + print(table.concat(out, "\n")) + """, + + + """ + -- popen 的模式检查,以及管道上能做什么 + local out = {} + local function c(tag, f, ...) + local r = table.pack(pcall(f, ...)) + local s = tag + for i = 1, r.n do s = s .. "\t" .. tostring(r[i]) end + out[#out + 1] = s + end + local function say(s) out[#out + 1] = s end + c("m1", io.popen, "echo hi", "r") + c("m3", io.popen, "echo hi", "rw") + c("m4", io.popen, "echo hi", "") + c("m5", io.popen, "echo hi", "x") + c("m6", io.popen) + c("m7", io.popen, nil) + c("m8", io.popen, "echo hi", "r", 99) + local m2ok, m2 = pcall(io.popen, "cat > /dev/null", "w") + say("m2 " .. tostring(m2ok) .. " " .. tostring(io.type(m2))) + if m2ok then c("m2c", m2.close, m2) end + local s1 = io.popen("echo hi") + c("s1", s1.seek, s1, "set", 0) + c("s2", s1.setvbuf, s1, "no") + say("s3 " .. tostring(type(s1:lines()))) + say("s4 " .. tostring(s1:read("a"))) + c("s5", s1.close, s1) + c("s6", s1.close, s1) + say("s7 " .. tostring(io.type(s1))) + print(table.concat(out, "\n")) + """, + + + """ + local out = {} + local function c(tag, f, ...) + local r = table.pack(pcall(f, ...)) + local s = tag + for i = 1, r.n do s = s .. "\t" .. tostring(r[i]) end + out[#out + 1] = s + end + local function say(s) out[#out + 1] = s end + local t = io.tmpfile() + say("t1 " .. tostring(t ~= nil) .. " " .. tostring(io.type(t))) + say("t2 " .. tostring(t:write("hello") == t)) + say("t3 " .. tostring(t:seek("set", 0))) + say("t4 " .. tostring(t:read("a"))) + c("t5", t.close, t) + say("t6 " .. tostring(io.type(t))) + local t2 = io.tmpfile() + c("t7", t2.close, t2) + local t3 = io.tmpfile(1, 2) + say("t8 " .. tostring(t3 ~= nil)) + if t3 then t3:close() end + print(table.concat(out, "\n")) + """, + + + """ + local out = {} + local function c(tag, f, ...) + local r = table.pack(pcall(f, ...)) + local s = tag + for i = 1, r.n do s = s .. "\t" .. tostring(r[i]) end + out[#out + 1] = s + end + local function say(s) out[#out + 1] = s end + local p = "/tmp/fkio_vbuf.txt" + os.remove(p) + for _, mode in ipairs({ "no", "full", "line" }) do + local f = io.open(p, "w") + c("v_" .. mode, f.setvbuf, f, mode) + f:write("xyz") + c("vc_" .. mode, f.close, f) + local g = io.open(p) + say("vr_" .. mode .. " " .. tostring(g:read("a"))) + g:close() + end + local f2 = io.open(p, "w") + c("v_sz", f2.setvbuf, f2, "full", 4096) + c("v_sz0", f2.setvbuf, f2, "full", 0) + -- 负的 / 极大的 size 不比:C 转成 (size_t) 后交给 libc 的 setvbuf, + -- 能不能分配出来是 libc 自己定的(同一台机器上 2^31 失败、2^40 成功, + -- "no" 配 -1 成功、"line" 配 -1 失败),Java 侧没法合理建模 + c("v_bad", f2.setvbuf, f2, "nope") + c("v_none", f2.setvbuf, f2) + c("v_num", f2.setvbuf, f2, 42) + c("v_nil", f2.setvbuf, f2, nil) + f2:close() + c("v_stdout", io.stdout.setvbuf, io.stdout, "no") + c("v_stdin", io.stdin.setvbuf, io.stdin, "full") + os.remove(p) + print(table.concat(out, "\n")) + """, }; + + /** + * {@code io.open} 的模式矩阵:{@code l_checkmode} 认的是 + * {@code [rwa]%+?b*},形状很容易写错({@code "rbb"} 合法而 {@code "rb+"} + * 不合法 —— 后缀是 {@code strspn(mode, "b")} 与全长比,不是「一个 b」)。 + * 34 种模式 ×「读 / 定位 / 写 / 位置 / 关 / 最终内容」,79 行。 + *

+ * 这一片咬出三处,其中两处是 C stdio 定的、本实现不打算建模, + * 第三处就是上面那句「写了之后位置是多少」——三处都量过才下的结论: + *

    + *
  • 更新模式({@code r+}/{@code w+}/{@code a+})上「读完不定位直接写」 + * 的落点由 stdio 的预读缓冲决定,ISO C 里这本就是未定义行为。 + * 实测:12 字节的文件写到 13(EOF),6002 字节的写到 4097(BUFSIZ+1), + * 而 {@code read(5000)} 又快又准地写到 5001(大块读绕过缓冲)。 + * 所以每次写之前都显式 {@code seek} 一次,把这条 UB 路径绕开 —— + * 绕开之后({@code read → seek → write})两边逐字节一致。
  • + *
  • append 档上写完的位置:{@code seek("set",0)} 再写 1 字节, + * 参考给 1({@code base+n}),我们给 13({@code EOF+n}, + * 这一边才符合 POSIX)。再量下去发现它也是缓冲定的: + * {@code n<=4095} 给 {@code base+n}、{@code n>=4096} 给 {@code EOF+n}。 + * 所以这里在量位置之前先 {@code flush} 把缓冲压掉,位置就落到 POSIX + * 那个唯一确定的答案上,两边才可比。写进去的内容两边始终一致, + * 分歧只在 {@code seek("cur",0)} 报的数。
  • + *
  • 标准流上的 {@code seek}取决于真实 fd 可不可定位:重定向到文件 + * 时给 0、{@code /dev/null} 给 0、管道与 tty 给 {@code "Illegal seek"}。 + * 本实现拿不到那个 fd,无法建模,故不比。
  • + *
+ */ + private static final String[] MODE = {""" + + -- io.open 的模式矩阵:l_checkmode 认的是 [rwa]%+?b*,形状很容易写错 + -- 两样东西不比,都是实测出来的,理由见 README: + -- 1. 更新模式(r+/w+/a+)上「读完不定位直接写」的落点由 C stdio 的**预读缓冲** + -- 决定(ISO C 里这本就是未定义行为):12 字节的文件写到 13(EOF), + -- 6002 字节的写到 4097(BUFSIZ+1),而 read(5000) 又写到 5001(大块读绕过缓冲)。 + -- 所以每个写之前都显式 seek 一次,把这条 UB 路径绕开。 + -- 2. 标准流上的 seek 取决于真实 fd 可不可定位:文件 / /dev/null 给 0, + -- 管道 / tty 给 "Illegal seek"。本实现拿不到那个 fd,无法建模。 + local out = {} + local function d(v) + local t = type(v) + if t == "nil" then return "nil" end + if t == "boolean" then return tostring(v) end + if t == "number" then return math.type(v) .. ":" .. tostring(v) end + if t == "string" then return string.format("q(%d)[%s]", #v, v) end + return t + end + local function probe(name, f, ...) + local r = table.pack(pcall(f, ...)) + local s = name .. " [" .. r.n .. "]" + for i = 2, r.n do s = s .. " " .. d(r[i]) end + if not r[1] then s = s .. " ERR " .. tostring(r[2]) end + out[#out + 1] = s + end + + local P = "/tmp/fkmd.txt" + local function seed() + local f = assert(io.open(P, "w")) + f:write("hello\\nworld\\n") + f:close() + end + + local modes = { + "r", "w", "a", "r+", "w+", "a+", + "rb", "wb", "ab", "r+b", "w+b", "a+b", + "rbb", "wbb", "abb", "r+bb", "w+bb", "a+bb", + "rb+", "wb+", "r b", "br", "rw", "ar", "r++", "", "x", "+r", "++r", + "rB", "R", "r\\t", "r\\n", "r ", " r", + } + for _, m in ipairs(modes) do + seed() + probe("open[" .. m:gsub("%c", "?") .. "]", function() + local f, e, en = io.open(P, m) + if f == nil then return f, e, en end + local r = {} + r[#r + 1] = io.type(f) + -- 追加 + 更新(a+ / a+b / a+bb)打开时的位置在哪是 + -- implementation-defined:glibc 给 0,于是读得到内容;Apple libc 给 + -- EOF,于是读出来是 nil。读这一步在这三种模式上不比(约定 #57)。 + if m:sub(1, 1) == "a" and m:find("+", 1, true) then + r[#r + 1] = "skip" + else + local ok1, v1 = pcall(function() return f:read("l") end) + r[#r + 1] = tostring(ok1) .. "/" .. tostring(v1) + end + -- 显式定位:把「读之后直接写」那条 UB 路径绕开 + local okp, vp, ep = pcall(function() return f:seek("set", 0) end) + r[#r + 1] = tostring(okp) .. "/" .. tostring(vp) .. "/" .. tostring(ep) + local ok2, v2 = pcall(function() return (f:write("Z") == f) end) + r[#r + 1] = tostring(ok2) .. "/" .. tostring(v2) + -- append 档上「写了之后位置是多少」是 stdio 缓冲定的:小写入给 base+n + -- (n<=4095),大写入给 EOF+n(n>=4096)。先 flush 把缓冲压掉,位置就落到 + -- POSIX 那个唯一确定的答案上,两边才可比。 + if m:sub(1, 1) == "a" then + local okf, vf = pcall(function() return f:flush() end) + r[#r + 1] = "flush=" .. tostring(okf) .. "/" .. tostring(vf) + end + local ok3, v3, e3 = pcall(function() return f:seek("cur", 0) end) + r[#r + 1] = tostring(ok3) .. "/" .. tostring(v3) .. "/" .. tostring(e3) + local ok4, v4 = pcall(function() return f:close() end) + r[#r + 1] = tostring(ok4) .. "/" .. tostring(v4) .. "/" .. tostring(io.type(f)) + -- 写完之后文件里到底是什么:这才是落点的真凭实据 + local g = io.open(P, "r") + r[#r + 1] = (g == nil) and "gone" or ("[" .. tostring(g:read("a")) .. "]") + if g then g:close() end + return table.concat(r, ";") + end) + end + + -- 模式参数的其它形状:不是字符串、缺失、数字 + seed() + probe("m_nil", io.open, P, nil) + probe("m_none", io.open, P) + probe("m_num", io.open, P, 42) + probe("m_bool", io.open, P, true) + probe("m_tbl", io.open, P, {}) + probe("m_nilfirst", io.open, nil, "r") + probe("m_noname", io.open) + probe("m_numname", io.open, 42, "r") + + seed() + probe("r_write", function() + local f = assert(io.open(P, "r")) + local ok, e, en = pcall(function() f:write("x") end) + f:close() + return ok, e, en + end) + probe("w_read", function() + local f = assert(io.open(P, "w")) + local ok, v, en = pcall(function() return f:read("l") end) + f:close() + return ok, v, en + end) + probe("w_seek_read", function() + local f = assert(io.open(P, "w")) + f:write("abc") + local p = f:seek("set", 0) + local ok, v = pcall(function() return f:read("a") end) + f:close() + return p, ok, v + end) + probe("a_seek_write", function() + local f = assert(io.open(P, "a")) + local p = f:seek("cur", 0) + f:write("END") + f:close() + local g = assert(io.open(P, "r")) + local c = g:read("a") + g:close() + return p, c + end) + probe("rplus", function() + local f = assert(io.open(P, "r+")) + local a = f:read("l") + f:seek("cur", 0) + local b = (f:write("Q") == f) + f:seek("set", 0) + local c = f:read("a") + f:close() + return a, b, c + end) + probe("wplus", function() + local f = assert(io.open(P, "w+")) + f:write("XY") + f:seek("set", 0) + local a = f:read("a") + f:close() + return a + end) + -- 打开瞬间的位置(p)不比:追加模式下它是 implementation-defined, + -- glibc 给 0、Apple libc 给 EOF(约定 #57)。只比写完定位后读回来的内容。 + probe("aplus", function() + local f = assert(io.open(P, "a+")) + f:write("Z") + f:seek("set", 0) + local a = f:read("a") + f:close() + return a + end) + + -- 标准流(seek 不比:见文件头第 2 条) + probe("close_stdout", function() return io.stdout:close() end) + probe("close_stdin", function() return io.stdin:close() end) + probe("write_stdout", function() return (io.write("") == io.stdout) end) + probe("stdin_write", function() local ok, v, e = pcall(function() return (io.stdin:write("x") == io.stdin) end); return ok, v, e end) + probe("stdout_type", function() return io.type(io.stdout), io.type(io.stderr), io.type(io.stdin) end) + + os.remove(P) + print(table.concat(out, "\\n")) + """ }; + + /** + * 照着 JaCoCo 的未覆盖行点名打的一批(39 行)。 + *

+ * 上一篇矩阵的选题是「按类排序挑缺口最大的」,这一篇再往下走一层: + * 打开 {@code target/site/jacoco/<包>/IoLib.java.html}({@code jacoco.xml} + * 里没有行级信息,HTML 有),把 {@code class="nc"} 的行号抄出来, + * 逐条问「这一行能不能从 Lua 里走到」。63 行未覆盖里大半是 + * {@code catch (IOException e)} —— 那些确实走不到;剩下这些是能走到 + * 却从没走到的,也就是真正漏测的行为: + *

    + *
  • 已关闭句柄的 {@code tostring} 是 {@code "file (closed)"} + * (不是 {@code io.type} 的 {@code "closed file"},两个字符串不一样);
  • + *
  • {@code io.lines()} 无参与显式 {@code nil}:走注册表里的 + * {@code IO_INPUT};
  • + *
  • {@code f:read("n")} 的各形状 —— 指数 {@code 1e5} / {@code 1e999} + * (给 inf)、十六进制 {@code 0x10} / {@code 0x1p4} / {@code 0x.8p1}、 + * 前导零 {@code 0777}(十进制不是八进制)、{@code 0x} 与 {@code 1e} + * 这种残缺前缀给 nil、超长数字串给 nil;
  • + *
  • 迭代途中把文件关掉:迭代器再调用报 {@code "file is already closed"};
  • + *
  • 大块写(5000 字节)绕开 {@code wbuf} 直接落盘的那条分支。
  • + *
+ * 39 行逐字节一致 —— 也就是说这些分支本来就是对的,只是没人跑过。 + * 它们的价值在回归:以后谁改了 {@code read("n")} 的前缀判断或 + * {@code wbuf} 的阈值,这里立刻红。 + */ + private static final String[] GAPS = {""" + -- 照着 JaCoCo 未覆盖行点名打:闭文件的 tostring、lines 无参、read("n") 各形状、 + -- 迭代途中关文件、大块写 + local out = {} + local function d(v) + local t = type(v) + if t == "nil" then return "nil" end + if t == "boolean" then return tostring(v) end + if t == "number" then return math.type(v) .. ":" .. tostring(v) end + if t == "string" then return string.format("q(%d)[%s]", #v, (v:gsub("\\n", "\\\\n"))) end + return t + end + local function probe(name, f, ...) + local r = table.pack(pcall(f, ...)) + local s = name .. " [" .. r.n .. "]" + for i = 2, r.n do s = s .. " " .. d(r[i]) end + if not r[1] then s = s .. " ERR " .. d(r[2]) end + out[#out + 1] = s + end + + local P = "/tmp/fkio2.txt" + local function put(s) + local f = assert(io.open(P, "w")) + f:write(s) + f:close() + end + + -- 1. 已关闭句柄的形状:tostring 给 "file (closed)",io.type 给 "closed file" + probe("ts_closed", function() + local f = io.open(P, "w") + f:close() + return tostring(f), io.type(f) + end) + probe("ts_open", function() + local f = io.open(P, "w") + local s = tostring(f) + f:close() + -- 只比形状:%p 打的是指针,长度也对不上(已知的 file (%p) 偏离) + return s:match("^file %(0x%x+%)$") ~= nil + end) + probe("ts_closed_concat", function() + local f = io.open(P, "w") + f:close() + return "x" .. tostring(f) + end) + + -- 2. lines 无参 / 显式 nil:走 registry 里的 IO_INPUT + put("a\\nb\\nc\\n") + probe("lines_noarg", function() + local f = assert(io.open(P, "r")) + io.input(f) + local r = {} + for l in io.lines() do r[#r + 1] = l end + io.input(io.stdin) + f:close() + return table.concat(r, ",") + end) + probe("lines_nil", function() + local f = assert(io.open(P, "r")) + io.input(f) + local r = {} + for l in io.lines(nil) do r[#r + 1] = l end + io.input(io.stdin) + f:close() + return table.concat(r, ",") + end) + probe("lines_noarg_fmt", function() + local f = assert(io.open(P, "r")) + io.input(f) + local r = {} + for l in io.lines("a") do r[#r + 1] = l end + io.input(io.stdin) + f:close() + return r[1] + end) + probe("lines_noarg_closed_input", function() + io.input(assert(io.open(P, "r"))) + io.input():close() + local ok, e = pcall(function() for l in io.lines() do end end) + io.input(io.stdin) + return ok, e + end) + + -- 3. read("n") 的各形状:指数、十六进制、前导零、超长 + local nums = { + "1e5", "1E5", "1e+5", "1e-5", "0x10", "0X10", "0x1p4", "0x.8p1", + "0777", "0z", "0", "0x", "1e", ".5", "5.", "- 5", " -3.5e-2 ", + "1e999", "0x1p999", "inf", "nan", "3abc", "--3", "+3", + } + for _, s in ipairs(nums) do + put(s .. "\\n") + probe("n[" .. s:gsub("%%", "%%%%") .. "]", function() + local f = assert(io.open(P, "r")) + local a, b = f:read("n") + f:close() + return a, b + end) + end + put(string.rep("1", 250) .. "\\n") + probe("n_250digits", function() + local f = assert(io.open(P, "r")) + local a = f:read("n") + f:close() + return a + end) + + -- 4. 迭代途中把文件关掉:迭代器再调用报 "file is already closed" + put("a\\nb\\nc\\n") + probe("lines_close_mid", function() + local f = assert(io.open(P, "r")) + local it = f:lines() + local first = it() + f:close() + local ok, e = pcall(it) + return first, ok, e + end) + probe("lines_close_mid_named", function() + local f = assert(io.open(P, "r")) + local first + for l in f:lines() do + first = l + f:close() + end + return first + end) + probe("lines_readerr", function() + local f = io.open(P, "w") -- 只写档:lines 读不出来 + local ok, e = pcall(function() for l in f:lines() do end end) + f:close() + return ok, e + end) + probe("lines_multi_fmt", function() + local r = {} + local f = assert(io.open(P, "r")) + for a, b in f:lines("l", "l") do r[#r + 1] = tostring(a) .. "/" .. tostring(b) end + f:close() + return table.concat(r, ";") + end) + + -- 5. 大块写:绕过 wbuf 直接落盘(wbuf 是 4096) + probe("write_big", function() + local f = assert(io.open(P, "w")) + f:write(string.rep("x", 5000)) + f:close() + local g = assert(io.open(P, "r")) + local s = g:read("a") + g:close() + return #s + end) + probe("write_mixed", function() + local f = assert(io.open(P, "w")) + f:write(string.rep("a", 4000)) + f:write(string.rep("b", 4000)) + f:write("c") + f:close() + local g = assert(io.open(P, "r")) + local s = g:read("a") + g:close() + return #s, s:sub(3999, 4002) + end) + probe("read_partial", function() + put("abc") + local f = assert(io.open(P, "r")) + local a = f:read(100) + local b = f:read(100) + f:close() + return a, b + end) + + os.remove(P) + print(table.concat(out, "\\n")) + """ }; + + /** + * io 的出错路径:管道上的 {@code seek}、错模式的 EBADF、 + * {@code popen} 子进程的流继承、{@code os.remove} / {@code os.rename} 的 errno。 + *

+ * 起因是覆盖率报告里 {@code IoLib} 有 54 行没走到,几乎全是 + * {@code catch (IOException)} —— 也就是「真出 I/O 错时给什么 errno」这一片 + * 从来没被扫过。顺着缺口点名咬出了三个真分歧: + *

    + *
  • {@code io.open(不存在的文件, "r+") 必须失败。C 的 + * {@code fopen("r+")} 走 {@code open(path, O_RDWR)},没带 + * {@code O_CREAT};Java 的 {@code RandomAccessFile(f, "rw")} + * 一定会建。修法是先用只读模式探一次存在性({@code "r"} 模式本身 + * 不创建),errno 照旧交给 {@code errnoOf} 反查;用 + * {@code File.exists()} 会把「父目录不可搜索」的 EACCES 错报成 + * ENOENT。
  • + *
  • {@code popen} 只接管一条流,其余继承。读模式接管子进程的 + * {@code stdout},写模式接管它的 {@code stdin},另外两条要原样 + * 落到 lua 的 stdout / stderr。Java 的 {@code ProcessBuilder} + * 默认三条全接成管道,于是全被吞掉;更糟的是读模式下子进程的 + * {@code stdin} 也是管道且没人关 —— {@code io.popen("cat", "r")} + * 会一直等下去。
  • + *
  • 上面那条还顺带决定了转发线程要 {@code join}:不 join 的话 + * 子进程的最后几行可能还没落进捕获缓冲,比对时好时坏。
  • + *
+ *

+ * 一处探针写法上的坑:文件句柄上没有 {@code tell} 方法(只有 + * {@code seek}),写 {@code f.tell} 得到的是 + * {@code attempt to call a nil value} —— 那是探针写错,不是实现的行为。 + */ + private static final String[] ERRPATH = {""" +-- io 的「出错路径」探针:管道上的 seek/tell、错的打开模式、popen/tempfile 的 +-- errno、close 的三元组。起因是覆盖率报告里 IoLib 有 54 行没走到,几乎全是 +-- catch (IOException) —— 也就是「真出 I/O 错时给什么 errno」这一片从来没扫过。 +local D = "target/rt" +os.execute("rm -rf " .. D .. " && mkdir -p " .. D) + +local function put(name, body) + local f = assert(io.open(D .. "/" .. name, "wb")) + f:write(body) + f:close() +end +put("a.txt", "line1\\nline2\\nline3\\n") +put("empty.txt", "") + +local function show(v) + local t = type(v) + if t == "nil" then return "nil" end + if t == "boolean" then return tostring(v) end + if t == "number" then return math.type(v) .. ":" .. tostring(v) end + if t == "string" then return string.format("q(%d)[%s]", #v, v) end + return t +end +local function probe(name, f, ...) + local r = table.pack(pcall(f, ...)) + local s = name .. " [" .. r.n .. "]" + for i = 2, r.n do s = s .. " " .. show(r[i]) end + if not r[1] then s = s .. " ERR " .. show(r[2]) end + print(s) +end + +-- ① 管道上的 seek / tell:C 的 fseek/ftell 在管道上给 ESPIPE(29) +-- io.popen 出来的句柄不是 RandomAccessFile,走的是 raf == null 那条路 +print("== 管道 seek/tell ==") +local pr = io.popen("echo hello", "r") +if pr then + print("R1", pr:read("a")) + probe("R.seek_set", pr.seek, pr, "set", 0) + probe("R.seek_cur", pr.seek, pr, "cur", 0) + probe("R.seek_end", pr.seek, pr, "end", 0) + probe("R.tell", pr.tell, pr) + probe("R.lines", pr.lines, pr) + probe("R.read_a2", pr.read, pr, "a") + probe("R.close", pr.close, pr) +end + +print("== 管道 write 侧 ==") +local pw = io.popen("cat > " .. D .. "/pw.out", "w") +if pw then + probe("W.write", pw.write, pw, "xyz\\n") + probe("W.seek", pw.seek, pw, "set", 0) + probe("W.tell", pw.tell, pw) + probe("W.flush", pw.flush, pw) + probe("W.read", pw.read, pw, "a") + probe("W.close", pw.close, pw) +end +do + local f = io.open(D .. "/pw.out", "rb") + print("W.result", f and f:read("a") or "NOFILE") + if f then f:close() end +end + +-- ② 错模式:只读文件上写、只写文件上读(EBADF 9) +-- f 上没有 tell 方法(seek("cur",0) 才是),用 f.tell 会得到 +-- "attempt to call a nil value" —— 那是探针写错,不是实现的行为 +print("== 错模式 ==") +local function modes() + for _, m in ipairs({ "r", "w", "a", "r+", "w+", "a+" }) do + local f, msg, en = io.open(D .. "/m.txt", m) + if f then + probe(m .. ".read1", f.read, f, 1) + probe(m .. ".read0", f.read, f, 0) + probe(m .. ".write_empty", f.write, f, "") + probe(m .. ".write_x", f.write, f, "x") + probe(m .. ".seek", f.seek, f, "set", 0) + probe(m .. ".seekcur", f.seek, f, "cur", 0) + probe(m .. ".flush", f.flush, f) + probe(m .. ".lines", f.lines, f) + probe(m .. ".close", f.close, f) + else + -- 打不开时连消息和 errno 一起比:r+ 在文件不存在时必须失败 + print(m .. " OPENFAIL " .. show(msg) .. " " .. show(en)) + end + os.remove(D .. "/m.txt") + end +end +put("m.txt", "abc") +modes() + +-- ②之二 文件不存在时逐个模式开一遍:C 的 fopen 只有 r / r+ 要求文件在 +print("== 不存在的文件 × 各模式 ==") +for _, m in ipairs({ "r", "w", "a", "r+", "w+", "a+", "rb", "r+b", "wb", "ab" }) do + local f, msg, en = io.open(D .. "/gone_" .. m, m) + if f then + print("gone " .. m .. " OK") + f:close() + else + -- 消息里的路径含模式名,两边一样;errno 才是要钉的 + print("gone " .. m .. " FAIL " .. show(en)) + end + os.remove(D .. "/gone_" .. m) +end + +-- ③ popen 的退出状态三元组 +print("== popen 退出状态 ==") +for _, cmd in ipairs({ "true", "false", "exit 7", "exit 255" }) do + local p = io.popen(cmd, "r") + if p then + probe("exit[" .. cmd .. "]", p.close, p) + end +end +probe("popen_badmode", io.popen, "true", "x") +probe("popen_nocmd", io.popen) +probe("popen_noname", io.popen, nil) +-- 42 会被 sh 当成命令名,子进程的 stderr 是继承的 —— 不 close 就等不到它, +-- 那行 "command not found" 出不出是竞态。必须 close 才确定。 +local pn = io.popen(42) +if pn then probe("popen_num_close", pn.close, pn) end + +-- ④ tmpfile / close 的 errno +print("== tmpfile ==") +local tf = io.tmpfile() +if tf then + probe("tf.write", tf.write, tf, "hello") + probe("tf.seek0", tf.seek, tf, "set", 0) + probe("tf.read", tf.read, tf, "a") + probe("tf.close", tf.close, tf) +end +probe("tmpfile_arg", io.tmpfile, "x") + +-- ⑤ 关掉的句柄再操作 +print("== 已关句柄 ==") +local f = io.open(D .. "/a.txt", "r") +f:close() +probe("closed.read", f.read, f, "a") +probe("closed.write", f.write, f, "a") +probe("closed.seek", f.seek, f, "set", 0) +probe("closed.close", f.close, f) +probe("closed.lines", f.lines, f) +probe("closed_tostring", tostring, f) + +-- ⑥ io.lines 的三种调用形状(无参 / nil / 文件名) +print("== io.lines 形状 ==") +probe("lines_nil", io.lines, nil) +probe("lines_file_argc", function() + local r = table.pack(io.lines(D .. "/a.txt")) + return r.n, type(r[1]), type(r[2]), type(r[3]), type(r[4]) +end) +probe("lines_noarg", function() return select("#", io.lines()) end) +probe("lines_missing", io.lines, D .. "/nope.txt") + +-- ⑦ os.remove / os.rename 的 errno +print("== os.remove/rename errno ==") +probe("rm_missing", os.remove, D .. "/nope.txt") +probe("rm_dir", os.remove, D) +probe("rm_file", os.remove, D .. "/empty.txt") +probe("rm_noname", os.remove) +probe("rm_num", os.remove, 42) +os.execute("mkdir -p " .. D .. "/nonempty && touch " .. D .. "/nonempty/x") +probe("rm_nonempty", os.remove, D .. "/nonempty") +put("r1.txt", "a") +probe("mv_ok", os.rename, D .. "/r1.txt", D .. "/r2.txt") +probe("mv_missing", os.rename, D .. "/nope1", D .. "/nope2") +probe("mv_dir", os.rename, D, D .. "/r3") +probe("mv_self", os.rename, D .. "/r2.txt", D .. "/r2.txt") +-- 类型冲突的 rename 只比「失败了」:errno 是内核定的,不由 POSIX 统一 +-- —— macOS 给 "Is a directory" 21,Linux 的「文件 -> 非空目录」给 +-- "Directory not empty" 39。两边都是各自内核的答案(约定 #98),连消息带 +-- errno 一起归掉,但仍然比「它失败了、给了三元组」。 +local function probe_xdir(name, ...) + local r = table.pack(pcall(os.rename, ...)) + out[#out + 1] = name .. " ok=" .. tostring(r[1]) .. " [" .. r.n .. "] " .. show(r[2]) +end +probe_xdir("mv_into_dir", D .. "/r2.txt", D) + +-- ⑧ io.open 的失败 errno +print("== io.open 失败 ==") +probe("open_missing", io.open, D .. "/nope.txt", "r") +probe("open_nodir", io.open, D .. "/nodir/x", "w") +probe("open_badmode", io.open, D .. "/a.txt", "q") +probe("open_nomode", io.open, D .. "/a.txt") +probe("open_nilname", io.open, nil, "r") +probe("open_num", io.open, 42, "r") +probe("open_dir_w", io.open, D, "w") +probe("open_dir_r", io.open, D, "r") + +print("DONE") + """ }; + + /** + * 文件方法 × 打开模式 × 每种参数:1262 行矩阵。 + *

+ * 九块内容:①六个方法({@code flush lines read seek setvbuf write})× 六个打开 + * 模式 × 21 种参数,外加「关掉之后再用」的第二遍;②{@code seek} 的双参数 + * 矩阵 —— 单参数时第二个值是 option,负数压根到不了偏移这条路,所以第一块对 + * 偏移是瞎的;③写完立刻定位;④{@code read} 的多格式 / 多参数 / {@code lines} + * 迭代;⑤{@code write} 的多参数;⑥目录句柄;⑦另开一个句柄去看 + * {@code flush} 到底有没有把字节推出去;⑧{@code ungetc} 退回来的那个字节要能被 + * 下一次 {@code read} 拿到;⑨{@code popen} 句柄的关闭({@code closeRaw} 的 + * {@code in} / {@code out} 两支)。 + *

+ * 咬出来的两个真分歧: + *

    + *
  • {@code f:read(-1)}:C 是 {@code size_t l = (size_t)luaL_checkinteger}, + * 负数转成天文数字后 {@code luaL_prepbuffsize} 分配失败,抛的是 + * {@code "not enough memory"},不是整数越界。我们原来直接 + * {@code new byte[(int)n]},是 {@code NegativeArraySizeException}—— + * 一个漏出 {@code pcall} 的 Java 异常。
  • + *
  • 成功的 {@code fseek} 会丢掉 {@code ungetc} 退回来的字节: + * {@code f:read("n")} 在 {@code "three"} 上失败时把 {@code 't'} 退了回去, + * 接着 {@code f:seek("set",0)} 再 {@code f:read(3)} —— 参考给 + * {@code "1 2"},我们给 {@code "t1 "}。
  • + *
+ *

+ * 实测灵敏度(单点破坏后红几行):{@code seek} 负位置不报 EINVAL 46、 + * {@code read} 不判目录 整片崩、{@code append} 不回末尾 5、{@code flush} 不 + * {@code flushW} 4、{@code seek} 不清 pushback 3、{@code seek} 目录基准恒 0 2、 + * {@code write} 不特判 0 字节 1。{@code tell} 里的 {@code +wlen} 是 0 + * —— {@code f:seek} 先 {@code flushW} 再 {@code tell},缓冲恒为空,那一刀 + * 观测不到,别当成已验证。⑧⑨两块是只补覆盖、不产生红的:去掉 + * {@code done >= len} 那条短路、或者不关 {@code popen} 的 {@code in},矩阵照样 + * 全绿 —— 它们的作用是让 JaCoCo 少说两句谎,不是多钉一条判定。 + */ + /** + * {@code io.lines()} 一个参数都不给的那条分支 —— 它读的是 + * {@code io.input} 指向的默认输入,所以必须喂 stdin 才走得到。 + *

+ * 覆盖率报告里 {@code IoLib} 的 {@code ioLines} 有两行(「一个参数都没有」 + * 那支)一直是白的:以前所有 {@code io.lines} 用例要么给文件名、要么显式给 + * {@code nil},两条走的都是另一支。 + *

+ * 顺带钉两件事:{@code io.input(f)} 换掉默认输入之后 {@code io.lines()} + * 读的是那个文件而不是 stdin;以及不带 {@code "l"} 时 + * {@code io.read} 的默认格式。 + */ + @Test + void defaultInputLinesMatchesReference() { + String src = """ + local out = {} + local function say(s) out[#out + 1] = s end + -- ① 无参 io.lines() 读默认输入(= stdin) + local n = 0 + for l in io.lines() do n = n + 1 say("A" .. n .. " [" .. l .. "]") end + say("A n=" .. n) + -- ② io.read 的几种默认/显式格式 + say("B " .. tostring(io.read())) + say("C " .. tostring(io.read("l"))) + say("D " .. tostring(io.read("n"))) + say("E " .. tostring(io.read("a"))) + say("F " .. tostring(select('#', io.read("l", "l")))) + -- ③ 换掉默认输入之后,无参 io.lines() 读的是那个文件 + local p = "target/rt/lines_in.txt" + os.remove(p) + local f = assert(io.open(p, "w")) + f:write("f1\\nf2\\nf3\\n") + f:close() + io.input(p) + for l in io.lines() do say("G [" .. l .. "]") end + say("H " .. tostring(io.read("l"))) + io.input(io.stdin) + os.remove(p) + print(table.concat(out, "\\n")) + """; + Diff.assertMatchesWithStdin(src, "s1\ns2\n42\nrest\n"); + Diff.assertMatchesWithStdin(src, ""); + Diff.assertMatchesWithStdin(src, "only-one-line\n"); + } + + @Test + void fileMethodGridMatchesReference() { + Diff.assertMatches(""" +-- 文件方法 × 打开模式 × 每种参数:顺着 getmetatable(io.stdout).__index 往下挖 +local function norm(e) return (tostring(e):gsub("0x%x+", "PTR")) end +local function show(...) + local n = select('#', ...) + local t = {} + for i = 1, n do t[i] = norm(tostring((select(i, ...)))) end + return table.concat(t, ",") +end +local function s(tag, f, ...) + local r = table.pack(pcall(f, ...)) + if not r[1] then + print(tag, norm(tostring(r[2]))) + else + print(tag, "OK", show(table.unpack(r, 2, r.n))) + end +end + +local P = "/tmp/fkio_fm.txt" +os.remove(P) -- 开头先删一次:上一趟崩掉留下的残骸会翻盘 +do + local f = io.open(P, "w") + f:write("abc\\n") + f:close() +end + +-- 第 1 项就是 nil,# 与 ipairs 会在第一格就停(踩过:只出 110 行) +local VALS = {} +local NV = 0 +local function add(v) NV = NV + 1 VALS[NV] = v end +for _, v in ipairs({ false, true, 0, 1, -1, 1.5, "s", "", {}, + "set", "cur", "end", "full", "line", "no", + "n", "a", "l", "L", 2 }) do add(v) end +add(nil) +local METHODS = { "flush", "lines", "read", "seek", "setvbuf", "write" } +-- a+ 不进这张表:打开时的位置(0 还是 EOF)是 implementation-defined, +-- glibc 与 Apple libc 相反,于是 read / lines 的每一步答案都相反(约定 #57)。 +local MODES = { "r", "w", "a", "r+", "w+" } + +for _, m in ipairs(MODES) do + local f = io.open(P, m) + print("mode", m, io.type(f)) + if f ~= nil then + for _, meth in ipairs(METHODS) do + s(m .. "|" .. meth .. "|a0", function() return f[meth](f) end) + for i = 1, NV do + local v = VALS[i] + s(m .. "|" .. meth .. "|" .. i, function() return f[meth](f, v) end) + end + end + -- 关掉之后再用:attempt to use a closed file + print(m .. "|close", f:close()) + print(m .. "|type", io.type(f)) + for _, meth in ipairs(METHODS) do + s(m .. "|closed|" .. meth, function() return f[meth](f) end) + s(m .. "|closed|" .. meth .. "|v", function() return f[meth](f, 1) end) + end + print(m .. "|close2", pcall(function() return f:close() end)) + end +end + +-- io.type 与 io.open 的返回值 +print("type_nil", io.type(nil)) +print("type_num", io.type(1)) +print("type_std", io.type(io.stdout)) +-- 打不开的文件:各模式 +print("open_dir", pcall(io.open, "/tmp", "r")) +print("open_noent", pcall(io.open, "/tmp/fkio_no_such_file", "r")) +print("open_badmode", pcall(io.open, P, "rw")) +print("open_badmode2", pcall(io.open, P, "")) +print("open_badmode3", pcall(io.open, P, "rb")) +print("open_badmode4", pcall(io.open, P, "r+b")) + + +-- ============ 第二块:seek 的双参数矩阵 ============ +-- 单参数时第二个值是 option,负数压根到不了偏移这条路 +local OFFS = { -2, -1, 0, 1, 3, 5, 100, 2^31, -2^31, 1.5, "0", "x" } +for _, m in ipairs(MODES) do + local f = io.open(P, m) + if f ~= nil then + for _, w in ipairs({ "set", "cur", "end" }) do + for _, off in ipairs(OFFS) do + s(m .. "|sk2|" .. w .. "|" .. tostring(off), function() return f:seek(w, off) end) + end + end + f:close() + end +end + +-- ============ 第三块:写完立刻定位(缓冲里的字节算不算位置) ============ +for _, m in ipairs({ "w", "a", "r+", "w+", "a+" }) do + local f = io.open(P, m) + if f ~= nil then + s(m .. "|w|seekcur0", function() f:write("hello") return f:seek("cur", 0) end) + s(m .. "|w|seekend0", function() f:write("hello") return f:seek("end", 0) end) + s(m .. "|w|seekset", function() f:write("hello") return f:seek("set") end) + s(m .. "|w|flush", function() f:write("hello") return f:flush() end) + s(m .. "|w|seekcur0b", function() f:write("hello") f:flush() return f:seek("cur", 0) end) + s(m .. "|w|reada", function() f:write("hello") f:flush() f:seek("set", 0) return f:read("a") end) + s(m .. "|w|readn", function() f:write("12") f:flush() f:seek("set", 0) return f:read("n") end) + f:close() + end +end + +-- ============ 第四块:read 的多格式 / 多参数 / lines 迭代 ============ +do + local w = io.open(P, "w") + w:write("1 2\\nthree\\n") + w:close() + local f = io.open(P, "r") + if f then + s("rd|a", function() return f:read("a") end) + f:seek("set", 0) + s("rd|l", function() return f:read("l") end) + s("rd|L", function() return f:read("L") end) + f:seek("set", 0) + s("rd|n", function() return f:read("n") end) + s("rd|n2", function() return f:read("n") end) + s("rd|n3", function() return f:read("n") end) + f:seek("set", 0) + s("rd|num", function() return f:read(3) end) + s("rd|num0", function() return f:read(0) end) + s("rd|eof0", function() f:read("a") return f:read(0) end) + s("rd|multi", function() f:seek("set", 0) return f:read("l", "L", "a", 2) end) + s("rd|bad", function() return f:read("z") end) + s("rd|star", function() f:seek("set", 0) return f:read("*l") end) + s("rd|empt", function() return f:read("") end) + s("rd|lines", function() f:seek("set", 0) local t = {} for l in f:lines() do t[#t + 1] = l end return table.concat(t, "|") end) + s("rd|linesL", function() f:seek("set", 0) local t = {} for l in f:lines("L") do t[#t + 1] = l end return table.concat(t, "|") end) + s("rd|linesn", function() f:seek("set", 0) local t = {} for l in f:lines("n") do t[#t + 1] = tostring(l) end return table.concat(t, "|") end) + s("rd|lines3", function() f:seek("set", 0) local t = {} for l in f:lines(3) do t[#t + 1] = l end return table.concat(t, "|") end) + s("rd|linesbad", function() f:seek("set", 0) local ok, e = pcall(function() for l in f:lines("z") do end end) return ok, e end) + f:close() + end +end + +-- ============ 第五块:write 的多参数 ============ +for _, m in ipairs({ "w", "a", "r+", "w+", "a+" }) do + local f = io.open(P, m) + if f then + s(m .. "|wr|multi", function() return f:write("a", 1, 2.5, "b") end) + s(m .. "|wr|num", function() return f:write(1) end) + s(m .. "|wr|bool", function() return f:write("a", true) end) + s(m .. "|wr|nil", function() return f:write("a", nil) end) + s(m .. "|wr|empty", function() return f:write("") end) + s(m .. "|wr|nada", function() return f:write() end) + f:close() + end +end + + +-- ============ 第六块:目录句柄 ============ +-- io.open 目录在两边都成功(C 的 fopen 不拦),真正现形的是读/定位/写 +local d = io.open("/tmp", "r") +if d then + print("dir|type", io.type(d)) + s("dir|reada", function() return d:read("a") end) + s("dir|readl", function() return d:read("l") end) + s("dir|readn", function() return d:read("n") end) + s("dir|read3", function() return d:read(3) end) + s("dir|read0", function() return d:read(0) end) + s("dir|seekcur0", function() return d:seek("cur", 0) end) + s("dir|seekset0", function() return d:seek("set", 0) end) + s("dir|seekneg", function() return d:seek("set", -1) end) + s("dir|seekcur1", function() return d:seek("cur", 1) end) + s("dir|seekcur7", function() return d:seek("cur", 7) end) + s("dir|seekneg2", function() return d:seek("cur", -1) end) + s("dir|flush", function() return d:flush() end) + s("dir|write", function() return d:write("x") end) + s("dir|setvbuf", function() return d:setvbuf("no") end) + s("dir|lines", function() local ok, e = pcall(function() for l in d:lines() do end end) return ok, e end) + s("dir|close", function() return d:close() end) + print("dir|type2", io.type(d)) +end + + +-- ============ 第七块:另开一个句柄去看 flush 到底有没有把字节推出去 ============ +-- 只有这一个办法能看见 flush 里的 flushW:seek 自己也 flush,会把痕迹抹掉 +for _, m in ipairs({ "w", "a", "w+", "r+" }) do + local f = io.open(P, m) + if f then + f:write("hello") + s(m .. "|x|nof", function() return (io.open(P, "r")):read("a") end) + s(m .. "|x|fl", function() f:flush() return (io.open(P, "r")):read("a") end) + f:close() + end + local f2 = io.open(P, m) + if f2 then + f2:write("XY") + s(m .. "|x|nof2", function() return (io.open(P, "r")):read("a") end) + s(m .. "|x|sk", function() f2:seek("cur", 0) return (io.open(P, "r")):read("a") end) + f2:close() + end +end + + +-- ============ 第八块:ungetc 退回来的那个字节要能被下一次 read 拿到 ============ +do + local w = io.open(P, "w") + w:write("three\\n") + w:close() + local f = io.open(P, "r") + if f then + s("pb|n", function() return f:read("n") end) + s("pb|1", function() return f:read(1) end) + s("pb|1b", function() return f:read(1) end) + s("pb|1c", function() return f:read(1) end) + s("pb|rest", function() return f:read("a") end) + f:close() + end +end + +-- ============ 第九块:popen 句柄也要真关(closeRaw 的 in / out 两支) ============ +do + local f = io.popen("echo hi", "r") + if f then + print("po|type", io.type(f)) + s("po|reada", function() return f:read("a") end) + s("po|readl", function() return f:read("l") end) + s("po|seek", function() return f:seek("set", 0) end) + s("po|flush", function() return f:flush() end) + s("po|write", function() return f:write("x") end) + s("po|setvbuf", function() return f:setvbuf("full") end) + print("po|close", f:close()) + print("po|type2", io.type(f)) + end + local g = io.popen("cat > /dev/null", "w") + if g then + print("po2|type", io.type(g)) + s("po2|write", function() return g:write("abc") end) + s("po2|read", function() return g:read("l") end) + s("po2|seek", function() return g:seek("cur", 0) end) + s("po2|flush", function() return g:flush() end) + print("po2|close", g:close()) + print("po2|type2", io.type(g)) + end +end + +os.remove(P) +print("done") + """); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/LexerTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/LexerTest.java new file mode 100644 index 0000000..bee7f78 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/LexerTest.java @@ -0,0 +1,477 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +/** + * 词法层测试。 + *

+ * 所有预期值都不是凭记忆写的:字面量子类型、转义结果、错误信息的完整文本 + * 都先跑 {@code lua5.4} 参考实现裁定过(见 {@code /tmp/probe.lua} 那类探针)。 + * 错误信息的逐字一致是硬性要求——后面阶段做两侧差分比对时,比的就是 + * 这些字符串。 + */ +class LexerTest { + + /** + * 报错里看到的源名({@code luaO_chunkid} 剥掉 {@code @} 之后的形态)。 + *

+ * 传给 {@link Lexer} 的必须带 {@code @}:不带前缀的源名会被 + * {@code luaO_chunkid} 包成 {@code [string "test.lua"]},而 {@code lua.c} + * 加载文件时加的就是 {@code @},真实脚本看到的都是裸路径。 + */ + private static final String SRC = "test.lua"; + + private static final String CHUNK = "@" + SRC; + + private static Lexer lexer(String src) { + return new Lexer(src.getBytes(StandardCharsets.UTF_8), CHUNK); + } + + /** 扫完整个输入,返回 token 序列(不含 TK_EOS) */ + private static List scan(String src) { + Lexer ls = lexer(src); + List out = new ArrayList<>(); + ls.next(); + while (ls.t.token != Lexer.TK_EOS) { + out.add(ls.t.copy()); + ls.next(); + } + return out; + } + + /** 只取一个 token */ + private static Lexer.Token first(String src) { + List ts = scan(src); + assertEquals(1, ts.size(), "期望恰好一个 token: " + src); + return ts.get(0); + } + + private static String render(List ts) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < ts.size(); i++) { + if (i > 0) { + sb.append(' '); + } + Lexer.Token t = ts.get(i); + switch (t.token) { + case Lexer.TK_NAME: + sb.append("NAME(").append(t.s.toJavaString()).append(')'); + break; + case Lexer.TK_STRING: + sb.append("STR(").append(t.s.toJavaString()).append(')'); + break; + case Lexer.TK_INT: + sb.append("INT(").append(t.i).append(')'); + break; + case Lexer.TK_FLT: + sb.append("FLT(").append(t.r).append(')'); + break; + default: + sb.append(Lexer.tokenName(t.token)); + } + } + return sb.toString(); + } + + private static String render(String src) { + return render(scan(src)); + } + + /** 断言扫描出错,且错误信息与上游逐字一致 */ + private static String errorOf(String src) { + LuaError e = assertThrows(LuaError.class, () -> scan(src), "期望词法错误: " + src); + return e.getMessage(); + } + + /* ---------- 基本 token ---------- */ + + @Test + void scansSimpleStatement() { + assertEquals("'local' NAME(x) '=' INT(1)", render("local x = 1")); + } + + @Test + void singleCharTokensUseTheirOwnCode() { + // 单字符 token 的值就是字符编码本身,这是整个 parser 的 switch 的基础 + assertEquals('+', first("+").token); + assertEquals('{', first("{").token); + assertEquals(';', first(";").token); + assertEquals('(', first("(").token); + assertTrue(Lexer.FIRST_RESERVED > 255, "FIRST_RESERVED 必须在单字节之外"); + } + + @Test + void scansAllReservedWords() { + String[] words = { + "and", "break", "do", "else", "elseif", "end", "false", "for", "function", + "goto", "if", "in", "local", "nil", "not", "or", "repeat", "return", + "then", "true", "until", "while" + }; + int[] expected = { + Lexer.TK_AND, Lexer.TK_BREAK, Lexer.TK_DO, Lexer.TK_ELSE, Lexer.TK_ELSEIF, + Lexer.TK_END, Lexer.TK_FALSE, Lexer.TK_FOR, Lexer.TK_FUNCTION, Lexer.TK_GOTO, + Lexer.TK_IF, Lexer.TK_IN, Lexer.TK_LOCAL, Lexer.TK_NIL, Lexer.TK_NOT, + Lexer.TK_OR, Lexer.TK_REPEAT, Lexer.TK_RETURN, Lexer.TK_THEN, Lexer.TK_TRUE, + Lexer.TK_UNTIL, Lexer.TK_WHILE + }; + assertEquals(words.length, Lexer.NUM_RESERVED); + for (int i = 0; i < words.length; i++) { + List ts = scan(words[i]); + assertEquals(1, ts.size(), words[i]); + assertEquals(expected[i], ts.get(0).token, words[i]); + } + } + + @Test + void scansOperators() { + assertEquals("NAME(a) '//' NAME(b) '..' NAME(c) '==' NAME(d) '>=' NAME(e) " + + "'<=' NAME(f) '~=' NAME(g) '<<' NAME(h) '>>' NAME(i)", + render("a // b .. c == d >= e <= f ~= g << h >> i")); + } + + @Test + void scansDoubleColonAndSingleColon() { + assertEquals("'::' NAME(l) '::' 'goto' NAME(l)", render("::l:: goto l")); + assertEquals("NAME(a) ':' NAME(b)", render("a:b")); + } + + @Test + void distinguishesDotConcatAndDots() { + assertEquals("NAME(a) '.' NAME(b)", render("a.b")); + assertEquals("NAME(a) '..' NAME(b)", render("a..b")); + assertEquals("'(' '...' ')'", render("(...)")); + assertEquals("'(' '.' ')'", render("(.)")); + assertEquals("INT(1) '..' INT(2)", render("1 ..2")); + } + + /* ---------- 数字 ---------- */ + + @Test + void keepsIntegerAndFloatSubtypes() { + // 参考实现裁定:math.type(1)=integer, math.type(1.0)=float, + // math.type(0x10)=integer, math.type(0x1p4)=float, math.type(0x.8)=float + Lexer.Token a = first("1"); + assertEquals(Lexer.TK_INT, a.token); + assertEquals(1L, a.i); + + Lexer.Token b = first("1.0"); + assertEquals(Lexer.TK_FLT, b.token); + assertEquals(1.0, b.r); + + Lexer.Token c = first("0x10"); + assertEquals(Lexer.TK_INT, c.token, "十六进制整数字面量是 integer"); + assertEquals(16L, c.i); + + Lexer.Token d = first("0x1p4"); + assertEquals(Lexer.TK_FLT, d.token, "带 p 指数的十六进制是 float"); + assertEquals(16.0, d.r); + + Lexer.Token e = first("0x.8"); + assertEquals(Lexer.TK_FLT, e.token); + assertEquals(0.5, e.r); + + Lexer.Token f = first(".5"); + assertEquals(Lexer.TK_FLT, f.token); + assertEquals(0.5, f.r); + + Lexer.Token g = first("1e2"); + assertEquals(Lexer.TK_FLT, g.token, "十进制指数一律是 float"); + assertEquals(100.0, g.r); + + Lexer.Token h = first("1e10"); + assertEquals(Lexer.TK_FLT, h.token); + assertEquals(1e10, h.r); + } + + @Test + void scansLargeIntegers() { + assertEquals(Long.MAX_VALUE, first("9223372036854775807").i); + Lexer.Token t = first("9223372036854775808"); + assertEquals(Lexer.TK_FLT, t.token, "溢出整数要退化成浮点而不是报错"); + assertEquals(9.223372036854776E18, t.r); + // 参考实现裁定:math.type(-9223372036854775808) 是 float —— 负号是独立 + // token,字面量本身 9223372036854775808 溢出整数范围,所以退化成浮点 + //(且是正的,取负发生在运行期)。这正是 Lua 要单开 math.mininteger 的原因。 + List ts = scan("(-9223372036854775808)"); + assertEquals(Lexer.TK_FLT, ts.get(2).token); + assertEquals(9.223372036854776E18, ts.get(2).r); + + ts = scan("(-9223372036854775807)"); + assertEquals(Lexer.TK_INT, ts.get(2).token); + assertEquals(Long.MAX_VALUE, ts.get(2).i); + } + + @Test + void rejectsMalformedNumbers() { + assertEquals(SRC + ":1: malformed number near '1a'", errorOf("1a")); + assertEquals(SRC + ":1: malformed number near '0x'", errorOf("0x")); + assertEquals(SRC + ":1: malformed number near '1e'", errorOf("1e")); + assertEquals(SRC + ":1: malformed number near '1e+'", errorOf("1e+")); + } + + @Test + void doesNotGlueNumberAcrossMinus() { + // 指数符号只在指数标记之后才合法,所以 3-4 必须是三个 token。 + // 注意 0xe 本身是合法的十六进制整数 14(参考实现 print(0xe-1) 得 13) + assertEquals("INT(3) '-' INT(4)", render("3-4")); + assertEquals("INT(14) '-' INT(1)", render("0xe-1")); + } + + /* ---------- 短字符串 ---------- */ + + @Test + void scansShortStringsWithBothQuotes() { + assertEquals("STR(hi)", render("'hi'")); + assertEquals("STR(hi)", render("\"hi\"")); + assertEquals("STR(it's)", render("\"it's\"")); + assertEquals("STR(say \"hi\")", render("'say \"hi\"'")); + assertEquals("STR()", render("''")); + } + + @Test + void scansSimpleEscapes() { + assertArrayEquals(new byte[] { 7, 8, 12, 10, 13, 9, 11 }, + first("\"\\a\\b\\f\\n\\r\\t\\v\"").s.bytes); + } + + @Test + void scansHexEscape() { + assertEquals("A", first("\"\\x41\"").s.toJavaString()); + LuaString t = first("\"\\x00\\xff\"").s; + assertEquals(2, t.length()); + assertEquals(0, t.byteAt(0)); + assertEquals(0xFF, t.byteAt(1)); + } + + @Test + void scansUtf8Escape() { + assertEquals("A", first("\"\\u{41}\"").s.toJavaString()); + LuaString zh = first("\"\\u{4E2D}\"").s; + assertEquals(3, zh.length(), "\\u{4E2D} 是 3 个字节"); + assertEquals("中", zh.toJavaString()); + assertEquals(4, first("\"\\u{1F600}\"").s.length()); + } + + @Test + void zapsSpacesAfterBackslashZ() { + assertEquals(1, first("\"\\z x\"").s.length()); + assertEquals("x", first("\"\\z x\"").s.toJavaString()); + assertEquals("ab", first("\"a\\z\n b\"").s.toJavaString(), "\\z 也吞换行且不产生字符"); + } + + @Test + void scansDecimalEscapes() { + assertEquals("A", first("\"\\65\"").s.toJavaString()); + assertEquals("AB", first("\"\\65\\066\"").s.toJavaString()); + LuaString nul = first("\"\\0\"").s; + assertEquals(1, nul.length()); + assertEquals(0, nul.byteAt(0)); + // 最多读三位:\000 吃掉三个 0,剩下的 '7' 是普通字符 + assertArrayEquals(new byte[] { 0, '7' }, first("\"\\0007\"").s.bytes); + } + + @Test + void scansEscapedNewlineAndQuotes() { + assertArrayEquals(new byte[] { 'a', '\n', 'b' }, first("\"a\\\nb\"").s.bytes); + assertArrayEquals(new byte[] { 'a', '\n', 'b' }, first("\"a\\\r\nb\"").s.bytes, + "\\r\\n 续行也归一化成一个 \\n"); + assertEquals("\"", first("\"\\\"\"").s.toJavaString()); + assertEquals("\\", first("\"\\\\\"").s.toJavaString()); + assertEquals("'", first("'\\''").s.toJavaString()); + } + + @Test + void acceptsRawBytesInLongString() { + // 除 ']' 与换行外的全部字节(含 \0 与非法 UTF-8)都要无损通过。 + // 换行要排除:长字符串会把 \n / \r / \r\n 统一归一化成 \n。 + byte[] payload = new byte[253]; + int n = 0; + for (int i = 0; i < 256; i++) { + if (i != ']' && i != '\n' && i != '\r') { + payload[n++] = (byte) i; + } + } + assertEquals(253, n); + byte[] src = new byte[n + 4]; + src[0] = '['; + src[1] = '['; + System.arraycopy(payload, 0, src, 2, n); + src[n + 2] = ']'; + src[n + 3] = ']'; + Lexer ls = new Lexer(src, CHUNK); + ls.next(); + assertEquals(Lexer.TK_STRING, ls.t.token); + assertArrayEquals(payload, ls.t.s.bytes); + } + + /* ---------- 长字符串与注释 ---------- */ + + @Test + void longStringSkipsFirstNewline() { + assertEquals("abc", first("[[\nabc]]").s.toJavaString()); + assertEquals(3, first("[[\nabc]]").s.length()); + assertEquals("\nabc", first("[[\n\nabc]]").s.toJavaString(), "只吞紧跟开头的第一个换行"); + } + + @Test + void longStringNormalizesCrLf() { + assertArrayEquals(new byte[] { 'a', '\n', 'b' }, first("[[\na\r\nb]]").s.bytes); + } + + @Test + void longStringRespectsLevel() { + assertEquals("abc", first("[==[abc]==]").s.toJavaString()); + assertEquals("a]]b", first("[==[a]]b]==]").s.toJavaString()); + assertEquals("", first("[[]]").s.toJavaString()); + // 结束符是贪婪匹配到的第一个同级 ']]':'[[]]]' 切成空长字符串 + 一个 ']'。 + // 参考实现 load("[[]]]") 报 "unexpected symbol near '[[]]'",正是这个切分。 + assertEquals("STR() ']'", render("[[]]]")); + } + + @Test + void skipsComments() { + assertEquals("NAME(a) '=' INT(1)", render("a = 1 -- 短注释")); + assertEquals("NAME(a) '=' INT(1)", render("a = 1 --[[ 长注释 ]]")); + assertEquals("NAME(a) '=' INT(1)", render("-- 开头\n a = 1")); + assertEquals("", render("--[[ 只有注释 ]]")); + assertEquals("", render("--[==[ 只有注释 ]==]")); + // 注释里的引号不能把词法状态搞乱 + assertEquals("NAME(a) '=' INT(1) NAME(a)", render("a = 1 -- it's a comment\na")); + } + + @Test + void unbalancedBracketInCommentFallsBackToShortComment() { + // '[==' 没有第二个 '[',退化成短注释,把整行吃掉 + assertEquals("NAME(a)", render("--[== not a long comment\na")); + } + + /* ---------- 错误信息(全部逐字对齐参考实现) ---------- */ + + @Test + void reportsUnfinishedString() { + assertEquals(SRC + ":1: unfinished string near ", errorOf("x = \"abc")); + assertEquals(SRC + ":1: unfinished string near '\"abc'", errorOf("x = \"abc\n")); + } + + @Test + void reportsUnfinishedLongString() { + assertEquals(SRC + ":1: unfinished long string (starting at line 1) near ", + errorOf("x = [[abc")); + assertEquals(SRC + ":3: unfinished long string (starting at line 2) near ", + errorOf("x = 1\n[[abc\n")); + } + + @Test + void reportsUnfinishedLongComment() { + assertEquals(SRC + ":1: unfinished long comment (starting at line 1) near ", + errorOf("--[[ x")); + } + + @Test + void reportsInvalidLongStringDelimiter() { + assertEquals(SRC + ":1: invalid long string delimiter near '[=='", errorOf("x = [==abc]==]")); + } + + @Test + void reportsInvalidEscapeSequence() { + assertEquals(SRC + ":1: invalid escape sequence near '\"a\\q'", errorOf("x = \"a\\qb\"")); + } + + @Test + void reportsDecimalEscapeTooLarge() { + assertEquals(SRC + ":1: decimal escape too large near '\"\\256\"'", errorOf("x = \"\\256\"")); + assertEquals(SRC + ":1: decimal escape too large near '\"\\654\"'", errorOf("x = \"\\654\"")); + } + + @Test + void reportsHexadecimalDigitExpected() { + assertEquals(SRC + ":1: hexadecimal digit expected near '\"\\xZ'", errorOf("x = \"\\xZ1\"")); + assertEquals(SRC + ":1: hexadecimal digit expected near '\"\\x4\"'", errorOf("x = \"\\x4\"")); + } + + @Test + void reportsUtf8EscErrors() { + assertEquals(SRC + ":1: UTF-8 value too large near '\"\\u{80000000'", + errorOf("x = \"\\u{80000000}\"")); + assertEquals(SRC + ":1: missing '}' near '\"\\u{41\"'", errorOf("x = \"\\u{41\"")); + assertEquals(SRC + ":1: missing '{' near '\"\\uA'", errorOf("x = \"\\uA\"")); + } + + @Test + void nonAsciiByteIsNotAnIdentifier() { + // 默认配置(未定义 LUA_UCID)下非 ASCII 字节不是字母,参考实现报 '<\228>' + byte[] src = new byte[] { 'x', ' ', (byte) 0xE4 }; + Lexer ls = new Lexer(src, SRC); + ls.next(); + ls.next(); + assertEquals(0xE4, ls.t.token, "非 ASCII 字节作为单字符 token 原样返回"); + assertEquals("'<\\228>'", Lexer.tokenName(0xE4)); + } + + /* ---------- 行号与前瞻 ---------- */ + + @Test + void countsLines() { + Lexer ls = lexer("a\nb\r\nc\n\rd"); + ls.next(); + assertEquals(1, ls.linenumber); + assertEquals(1, ls.lastline); + ls.next(); + assertEquals(2, ls.linenumber); + ls.next(); + assertEquals(3, ls.linenumber, "\\r\\n 只算一行"); + ls.next(); + assertEquals(4, ls.linenumber, "\\n\\r 也只算一行"); + ls.next(); + assertEquals(Lexer.TK_EOS, ls.t.token); + } + + @Test + void lookaheadDoesNotConsume() { + Lexer ls = lexer("a b"); + ls.next(); + assertEquals(Lexer.TK_NAME, ls.t.token); + assertEquals("a", ls.t.s.toJavaString()); + assertEquals(Lexer.TK_NAME, ls.lookahead()); + assertEquals(Lexer.TK_NAME, ls.t.token, "前瞻不能改动当前 token"); + assertEquals("a", ls.t.s.toJavaString()); + ls.next(); + assertEquals("b", ls.t.s.toJavaString(), "next 直接取用前瞻结果"); + ls.next(); + assertEquals(Lexer.TK_EOS, ls.t.token); + } + + @Test + void lineBreakInsideStringIsCounted() { + Lexer ls = lexer("'a\\\nb'\nc"); + ls.next(); + assertEquals(Lexer.TK_STRING, ls.t.token); + assertEquals(2, ls.linenumber, "续行转义要计行"); + ls.next(); + assertEquals("c", ls.t.s.toJavaString()); + } + + /* ---------- 驻留 ---------- */ + + @Test + void internsShortStrings() { + StringPool pool = new StringPool(); + Lexer a = new Lexer(new Zio("'abc' 'abc'".getBytes(StandardCharsets.UTF_8)), SRC, pool); + a.next(); + LuaString s1 = a.t.s; + a.next(); + LuaString s2 = a.t.s; + assertSame(s1, s2, "同一个短字符串字面量必须是同一个实例"); + assertEquals("abc", s1.toJavaString()); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/LibraryCallbackTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/LibraryCallbackTest.java new file mode 100644 index 0000000..cea234a --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/LibraryCallbackTest.java @@ -0,0 +1,223 @@ +package com.github.esrrhs.fakescript.lua; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +/** + * 库回调的异常行为矩阵 —— 阶段 12。 + *

+ * 为什么单开一类:刚抓到的那个崩溃(元方法里 yield 穿过 C 帧)属于一整族问题 + * ——「C 函数回调进 Lua,Lua 那边不规矩」。那一族里除了元方法,还有一批别的 + * 回调点:table.sort 的比较函数、string.gsub 的替换值、 + * print / tostring 的 __tostring、泛型 for 的迭代器。 + * 它们共用同一条「能不能 yield」的判据,也共用错误传播那一套。 + *

+ * 矩阵两维:回调点 × 不规矩形态,再乘两种外套(主线程 / 协程)。 + *

    + *
  • 回调点:sort 比较函数、gsub 函数替换、gsub 表替换(走 + * {@code __index})、{@code tostring} 的 {@code __tostring}、 + * {@code print} 的 {@code __tostring}、泛型 for 迭代器。
  • + *
  • 不规矩形态:抛 LuaError(level 0/1/2)、抛非字符串对象 + * (number / nil / table / boolean)、返回非布尔(sort)、 + * 返回非字符串({@code __tostring})、yield。
  • + *
+ *

+ * 两种外套给出两种答案,少一种都看不出续体写没写对: + *

    + *
  • sort / gsub / print 的 __tostring 走 + * {@code luaD_callnoyield}(第 5 条),回调里 yield 一律报 + * {@code attempt to yield across a C-call boundary}。
  • + *
  • 泛型 for 的迭代器 由 {@code OP_TFORCALL} 直接调, + * 不经过 C 帧,所以这里允许 yield —— 与上面正好相反。
  • + *
+ *

+ * 踩过的坑:恒返回非 nil 的回调放进 {@code for ... in} 里, + * 控制变量永远不会变成 nil,裸跑在主线程上就是死循环(亲手挂了一次, + * 烧了 3 分钟)。所以这一类必须限次。 + * + * @see LuaState#callNoYield + * @see LuaTm#callTmMightYield + */ +class LibraryCallbackTest { + + @BeforeAll + static void needReference() { + assumeTrue(ReferenceLua.find() != null, + "no lua5.4 reference binary; set LUA54_BIN"); + } + + /** 库回调的异常行为矩阵(480 行) */ + @Test + void misbehavingCallbacksMatchReference() { + Diff.assertMatches(""" +-- 库回调的异常行为矩阵 +-- +-- 为什么开这一片:刚抓到的那个崩溃(元方法里 yield 穿过 C 帧)属于一整族问题 +-- ——「C 函数回调进 Lua,Lua 那边不规矩」。那一族里除了元方法,还有一批别的 +-- 回调点:table.sort 的比较函数、string.gsub 的替换值、print/tostring 的 +-- __tostring、泛型 for 的迭代器。它们共用同一条「能不能 yield」的判据 +-- (luaT_callTMres 的 isLuacode 分支),也共用错误传播的那一套。 +-- +-- 矩阵两维: +-- 回调点 × 不规矩的形态(抛错 / 抛非字符串 / 返回错类型 / yield) +-- 每个组合再跑两种外套:主线程(pcall 兜住)与协程内(连 resume 三轮)。 + +local function show(v) + local t = type(v) + if t == "string" then return "s[" .. v .. "]" end + if t == "number" then + if math.type(v) == "integer" then return "i" .. tostring(v) end + return "f" .. tostring(v) + end + return t +end + +local function go(tag, f, ...) + local r = table.pack(pcall(f, ...)) + local out = { tag, tostring(r[1]) } + for i = 2, r.n do out[#out + 1] = show(r[i]) end + print(table.concat(out, " ")) +end + +--[[ + 两种外套。 + 主线程那一趟确认「错误传播 + 位置前缀」;协程那一趟确认「yield 能不能穿过去」 + ——这正是上一片咬出崩溃的地方。 +]] +local function runBoth(tag, build) + go("M." .. tag, build) + local co = coroutine.create(function() return pcall(build) end) + for i = 1, 3 do + local r = table.pack(coroutine.resume(co, "R" .. i)) + local out = { "C." .. tag, "r" .. i } + for j = 1, r.n do out[#out + 1] = show(r[j]) end + print(table.concat(out, " ")) + end +end + +print("BEGIN") + +--[[ + 「不规矩」的回调:抛错、抛非字符串、返回错类型、yield。 + 同一个表要在不同组合里反复用,所以每条都写成工厂—— + 直接拿一个函数当多个回调会互相污染。 +]] +local function badfns() + return { + { "err", function(a, b) error("boom") end }, + { "err0", function(a, b) error("boom", 0) end }, + { "err2", function(a, b) error("boom", 2) end }, + { "num", function(a, b) error(42) end }, + { "nil", function(a, b) error() end }, + { "tbl", function(a, b) error({}) end }, + { "bool", function(a, b) error(true) end }, + { "yield", function(a, b) return coroutine.yield("Y") end }, + { "retnil", function(a, b) return nil end }, + { "retnum", function(a, b) return 7 end }, + { "retstr", function(a, b) return "S" end }, + { "retbool", function(a, b) return true end }, + { "retnone", function(a, b) end }, + { "retmulti", function(a, b) return 1, 2, 3 end }, + } +end + +for _, e in ipairs(badfns()) do + local nm, bad = e[1], e[2] + + -- 1) table.sort 的比较函数 + runBoth("sort." .. nm, function() + local t = { 5, 3, 1, 4, 2 } + table.sort(t, bad) + return table.concat(t, ",") + end) + + -- 2) string.gsub 的替换值是函数 + runBoth("gsubf." .. nm, function() + return string.gsub("abcabc", "b", bad) + end) + + -- 3) string.gsub 的替换值是表(走 __index) + runBoth("gsubt." .. nm, function() + return string.gsub("abcabc", "b", setmetatable({}, { __index = bad })) + end) + + -- 4) table.concat 遇上 __tostring + runBoth("concat." .. nm, function() + return table.concat({ setmetatable({}, { __tostring = bad }) }, ",") + end) + + -- 5) tostring 的 __tostring + runBoth("tostr." .. nm, function() + return tostring(setmetatable({}, { __tostring = bad })) + end) + + -- 6) print 的 __tostring + runBoth("print." .. nm, function() + print(setmetatable({}, { __tostring = bad })) + return "after" + end) + + -- 7) 字符串拼接的 __concat(数字侧) + runBoth("concat2." .. nm, function() + return 1 .. setmetatable({}, { __concat = bad }) + end) + + --[[ + 8) 泛型 for 的迭代器。 + 必须限次:像 retnum / retstr 这种恒返回非 nil 的回调,控制变量 + 永远不会变成 nil,裸跑在主线程上就是死循环(刚亲手挂了一个)。 + 而且 OP_TFORCALL 是直接调迭代器、不经过 C 帧,所以这里允许 + yield —— 与 table.sort 那种走 callNoYield 的回调正好相反。 + ]] + runBoth("iter." .. nm, function() + local acc = {} + local i = 0 + for x in bad, "S", 0 do + i = i + 1 + acc[#acc + 1] = tostring(x) + if i >= 4 then break end + end + return table.concat(acc, ","), i + end) +end + +--[[ + 单独补两组:比较函数返回非布尔,以及比较函数不一致。 + 前者要报 "invalid order function for sorting",后者算法不同结果就不同 + ——所以这一档也是「排序算法必须与参考实现一致」的闸门。 +]] +local NONBOOL = { + { "nil", function() return nil end }, + { "num", function() return 7 end }, + { "str", function() return "S" end }, + { "tbl", function() return {} end }, + { "zero", function() return 0 end }, +} +for _, e in ipairs(NONBOOL) do + local nm, bad = e[1], e[2] + runBoth("nonbool." .. nm, function() + local t = { 5, 3, 1, 4, 2 } + table.sort(t, bad) + return table.concat(t, ",") + end) +end + +-- 比较函数在遍历途中改表:算法不同则结果不同 +runBoth("mutate", function() + local t = { 5, 3, 1, 4, 2 } + local n = 0 + table.sort(t, function(a, b) + n = n + 1 + if n == 3 then t[#t + 1] = 99 end + return a < b + end) + return table.concat(t, ",") +end) + +print("END") + """); + } + + private LibraryCallbackTest() { + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/LoadTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/LoadTest.java new file mode 100644 index 0000000..bf6ef13 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/LoadTest.java @@ -0,0 +1,330 @@ +package com.github.esrrhs.fakescript.lua; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +/** + * {@code load} / {@code loadfile} / {@code dofile} 的差分用例 —— 阶段 8。 + *

+ * 与前面几个库不同,这里还没做完:二进制 chunk 需要 {@code luaU_undump} + * 与 {@code luaU_dump}({@code lundump.c} / {@code ldump.c}),这一轮只把 + * 「文本那条路」接上,二进制那条路挡下来并照 C 的 {@code checkmode} 先判 + * 模式。所以: + *

    + *
  • 不比对 {@code string.dump}(还没实现)与任何以 + * {@code \x1bLua} 开头的输入;
  • + *
  • 模式校验、chunk 名、{@code _ENV} 参数、读函数这几条都逐字节比。
  • + *
+ *

+ * 盯住的几处: + *

    + *
  • {@code load} 失败给两个值:{@code nil, 消息},不抛错; + * 而 {@code dofile} 失败是抛错;
  • + *
  • chunk 名:{@code load(s)} 默认拿 {@code s} 自己当名字, + * {@code loadfile} 的是 {@code "@" + 路径},两者打印时都会被 + * {@code luaO_chunkid} 剥掉前缀;
  • + *
  • {@code load(s, name, mode, env)} 的第四参数替换主 chunk 的 + * {@code _ENV}(也就是它的第一个 upvalue);
  • + *
  • 读函数返回 nil 表示结束,返回非字符串非数字要报 + * {@code reader function must return a string}。
  • + *
+ */ +class LoadTest { + + @TestFactory + List loadScriptsMatchReference() { + List tests = new ArrayList<>(); + add(tests, "load", LOAD); + add(tests, "env", ENV); + add(tests, "reader", READER); + add(tests, "file", FILE); + add(tests, "err", ERRORS); + return tests; + } + + private static void add(List tests, String tag, String[] srcs) { + for (int i = 0; i < srcs.length; i++) { + String src = srcs[i]; + int n = i; + tests.add(DynamicTest.dynamicTest(tag + "[" + n + "]", () -> Diff.assertMatches(src))); + } + } + + /* ==================== load ==================== */ + + private static final String[] LOAD = { + """ + local f = load("return 1+1") + print(type(f), f()) + print(load("")()) + print(load("return")()) + local g = load("local a, b = 1, 2 return a + b") + print(g()) + print(load("return ...")(1, 2, 3)) + """, + + """ + -- 默认 chunk 名就是源码本身 + print(load("x++")) + print(load("return 1", "=myname")) + local f, e = load("x++", "=myname") + print(f, e) + local g = load("error('boom')", "=myname") + print(pcall(g)) + """, + + """ + -- chunk 名以 '@' 开头时打印要被剥掉 + local f, e = load("x++", "@/tmp/whatever.lua") + print(f, e) + local g = load("error('boom')", "@myfile.lua") + print(pcall(g)) + local h = load("error('boom')", "plain") + print(pcall(h)) + """, + + """ + -- 模式校验:'b' / 't' + print(load("return 1", nil, "b")) + print(load("return 1", nil, "t")) + print(load("return 1", nil, "bt")) + print(load("return 1", nil, "")) + print(load("return 1", nil, "x")) + print(load("return 1", nil, 42)) + """, + + """ + -- 数字也会当源码编(lua_tolstring) + local f = load(42) + print(type(f), f()) + local g = load(42, "name") + print(g()) + print(load("42")()) + """, + + """ + -- 加载出来的函数就是普通闭包 + local f = load("return function(a) return a * 2 end") + local inner = f() + print(inner(21)) + print(type(f), tostring(f):sub(1, 8)) + """, }; + + /* ==================== _ENV ==================== */ + + private static final String[] ENV = { + """ + -- 第四参数替换主 chunk 的 _ENV + local f = load("return x", nil, "t", {x = 99}) + print(f()) + local g = load("x = 7 return x", nil, "t", {}) + print(g()) + local env = {print = print} + local h = load("print(_ENV == nil)", nil, "t", env) + h() + """, + + """ + -- 传 nil 也照样设(_ENV 就变成 nil,取全局会报错) + local f = load("return x", nil, "t", nil) + print(pcall(f)) + local g = load("return 1", nil, "t", nil) + print(g()) + """, + + """ + -- 没传第四参数时 _ENV 是全局表 + X_GLOBAL = 5 + local f = load("return X_GLOBAL") + print(f()) + local g = load("X_GLOBAL = 6 return X_GLOBAL") + print(g(), X_GLOBAL) + """, + + """ + -- 设了 _ENV 之后改动不会漏到全局 + local env = setmetatable({}, {__index = _G, __newindex = _G}) + local f = load("y = 3 return y", nil, "t", env) + print(f(), y) + """, }; + + /* ==================== 读函数 ==================== */ + + private static final String[] READER = { + """ + -- 一段一段喂 + local pieces = {"return ", "1 +", " 2"} + local i = 0 + local f = load(function() i = i + 1 return pieces[i] end) + print(f()) + """, + + """ + -- 第一次就返回 nil:空 chunk + print(load(function() return nil end)()) + -- 返回数字也算字符串 + local n = 0 + print(pcall(load(function() n = n + 1 if n == 1 then return 42 else return nil end end))) + """, + + """ + -- 返回非字符串非数字 + print(pcall(load(function() return {} end))) + print(pcall(load(function() return true end))) + """, + + """ + -- 读函数报错会冒出去 + print(pcall(load(function() error("reader died") end))) + """, + + """ + -- 读函数版也有第二参数的默认名 "=(load)" + local f, e = load(function() return "x++" end) + print(f, e) + """, }; + + /* ==================== loadfile / dofile ==================== */ + + private static final String[] FILE = { + """ + local p = "/tmp/fkld_f1.lua" + local f = io.open(p, "w") + f:write("return 7\\n") + f:close() + print(loadfile(p)()) + print(dofile(p)) + os.remove(p) + """, + + """ + -- 每个 chunk 有自己的 _ENV 上值,改全局互相可见 + local p = "/tmp/fkld_f2.lua" + local f = io.open(p, "w") + f:write("SHARED = SHARED and SHARED + 1 or 1\\nreturn SHARED\\n") + f:close() + print(dofile(p)) + print(dofile(p)) + print(SHARED) + os.remove(p) + """, + + """ + -- 无参 loadfile 读 stdin(测试里是空流) + local f = loadfile() + print(type(f), f()) + print(dofile()) + """, + + """ + -- dofile 返回被调 chunk 的全部返回值 + local p = "/tmp/fkld_f4.lua" + local f = io.open(p, "w") + f:write("return 1, 2, 3\\n") + f:close() + print(dofile(p)) + print(select('#', dofile(p))) + os.remove(p) + """, + + """ + -- 文件里的错误带的是文件自己的位置 + local p = "/tmp/fkld_f5.lua" + local f = io.open(p, "w") + f:write("local a = 1\\nerror('in file')\\n") + f:close() + print(pcall(dofile, p)) + local g, e = loadfile(p) + print(pcall(g)) + os.remove(p) + """, + + """ + -- '#' 开头的首行会被跳过(Unix 脚本);行号靠补一个 \\n 保住 + local p = "/tmp/fkld_f6.lua" + local f = io.open(p, "w") + f:write("#!/usr/bin/lua\\nreturn 5\\n") + f:close() + print(dofile(p)) + local p2 = "/tmp/fkld_f7.lua" + f = io.open(p2, "w") + f:write("#!/usr/bin/lua\\nerror('here')\\n") + f:close() + print(pcall(dofile, p2)) + os.remove(p) + os.remove(p2) + """, + + """ + -- loadfile 的 mode 参数 + local p = "/tmp/fkld_f8.lua" + local f = io.open(p, "w") + f:write("return 1\\n") + f:close() + print(loadfile(p, "b")) + print(loadfile(p, "t")()) + print(loadfile(p, "x")) + os.remove(p) + """, }; + + /* ==================== 报错 ==================== */ + + private static final String[] ERRORS = { + """ + -- load 失败给 nil + 消息;dofile 失败是抛错 + print(load("x++")) + print(pcall(load, "x++")) + print(pcall(dofile, "/nonexistent_dir_zzz/x.lua")) + print(loadfile("/nonexistent_dir_zzz/x.lua")) + """, + + """ + -- 参数检查 + print(pcall(load)) + print(pcall(load, nil)) + print(pcall(load, true)) + print(pcall(load, {})) + print(pcall(loadfile, 42)) + """, + + """ + -- 二进制 chunk:模式不含 'b' 时报模式错(这一条已对齐) + print(load("\\27Lua")) + print(load("\\27Lua", nil, "b")) + """, + + """ + -- 消息处理函数是「抛出点」生效,不是「pcall 边界」生效。 + -- luaD_protectedparser 把 L->errfunc 原样传进内层保护区, + -- 于是读函数里的 error() 当场被外层 xpcall 的处理器接住。 + local function X(tag, fn) + local r = table.pack(xpcall(fn, function(m) return "H:" .. tostring(m) end)) + local out = tag + for i = 1, r.n do out = out .. " | " .. tostring(r[i]) end + print(out .. " | n=" .. r.n) + end + -- 走 luaD_throw 直跳:不带 H + X("syntax", function() return load("x++") end) + X("syntax2", function() return load(function() return "x++" end) end) + X("mode", function() return load("return 1", "=x", "q") end) + X("mode2", function() return loadfile("target/rt/case.lua", "q") end) + X("cannotopen", function() return loadfile("target/rt/nope.lua") end) + -- 走 luaG_errormsg:带 H + X("readererr", function() return load(function() error("boom") end) end) + X("readerbad", function() return load(function() return true end) end) + X("readerlvl0", function() return load(function() error("b", 0) end) end) + X("readernonstr", function() return load(function() error({ "t" }) end) end) + -- dofile 的打不开要走 lua_error,所以带 H + X("dofile", function() return dofile("target/rt/nope.lua") end) + -- 协程不继承 errfunc:新线程那份是 0 + X("co", function() + local co = coroutine.create(function() + return load(function() error("co") end) + end) + return select(2, coroutine.resume(co)) + end) + """, }; +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/LuaLibsTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/LuaLibsTest.java new file mode 100644 index 0000000..dd4c11b --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/LuaLibsTest.java @@ -0,0 +1,75 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * {@link LuaLibs#openAll} —— {@code linit.c: luaL_openlibs} 的对应物。 + *

+ * 不是「测一个循环」:这里钉住两件会悄悄退化的事 + *

    + *
  • 一个都不能少。少开 {@code PackageLib},{@code package.loaded} 里 + * 就没有 {@code _G},于是 {@code lauxlib.c: pushglobalfuncname} 查不到 + * C 函数的全局名,{@code pcall(string.len)} 的报错从 + * {@code bad argument #1 to 'string.len'} 退化成 {@code ... to 'len'}—— + * 而那套报错是别的用例在看的,退化会表现成「那边绿、这边莫名少个前缀」。
  • + *
  • 库表是同一张。{@code string} 库的 {@code __index} 挂在字符串元表上, + * 如果 {@code openAll} 里新建了库表而 {@code StringLib.open} 又建了一张, + * {@code ("x"):upper()} 就会用上错的那张。
  • + *
+ */ +final class LuaLibsTest { + + @BeforeAll + static void requireRef() { + // 只有第三条用例需要参考实现,前两条这里跳过也无所谓,但一次跳过更省事 + assumeTrue(ReferenceLua.find() != null, "参考实现二进制不存在,跳过一致性测试"); + } + + @Test + void opensEveryStandardLibrary() { + LuaState L = new LuaState(); + LuaLibs.openAll(L); + String[] names = { "string", "table", "math", "io", "os", "utf8", + "coroutine", "package", "debug" }; + for (String n : names) { + LuaValue v = L.globals.get(n); + assertTrue(v != null && v.isTable(), n + " 没开:" + v); + } + // base 库是散在全局上的函数,挑几个代表 + for (String fn : new String[] { "print", "pcall", "xpcall", "select", + "setmetatable", "load", "require", "tostring" }) { + LuaValue v = L.globals.get(fn); + assertTrue(v != null && v.isFunction(), fn + " 没开:" + v); + } + } + + @Test + void libraryTableIsTheOneTheMetatablePointsAt() { + LuaState L = new LuaState(); + LuaLibs.openAll(L); + LuaValue err = L.runProtected("return ('x'):upper()".getBytes(), "=t"); + assertEquals(null, err, "字符串元表的 __index 不是开出来的那张 string 库表"); + } + + /** + * 走 {@code Diff} 而不是自造断言:{@code print(pcall(string.len))} 的期望值 + * 是 {@code false bad argument #1 to 'string.len' (...)},那个 + * {@code 'string.len'} 完全来自 {@code _LOADED} 反查,自造断言等于把结论 + * 抄一遍——抄错了还绿。交给参考实现才是真的钉住。 + *

+ * 用 {@code pcall(string.len)} 而不是 {@code string.len(nil)}:后者有调用点, + * 名字走 {@code getfuncname} 那条路,查出来是 {@code 'len'}(参考实现也是)。 + * 要逼出 {@code pushglobalfuncname},调用者必须是 C 帧。 + */ + @Test + void globalFuncNamesSurvive() { + Diff.assertMatches("print(pcall(string.len))"); + Diff.assertMatches("print(pcall(string.len, nil))"); + Diff.assertMatches("print(pcall(table.concat))"); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/LuaStringTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/LuaStringTest.java new file mode 100644 index 0000000..20a4447 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/LuaStringTest.java @@ -0,0 +1,90 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * 字符串层。这里的用例全是「用 Java String 就会错」的场景: + * 内嵌 \0、任意非法 UTF-8 字节、按字节而非按字符的长度。 + */ +public class LuaStringTest { + + /** #s 数的是字节,不是字符,也不是 UTF-16 码元 */ + @Test + public void lengthCountsBytes() { + assertEquals(0, LuaString.EMPTY.length()); + assertEquals(3, LuaString.latin1("abc").length()); + // "中" 的 UTF-8 是 3 字节;Java String.length() 会是 1 + assertEquals(3, LuaString.utf8("中").length()); + // 4 字节 UTF-8(emoji),Java String.length() 会是 2 + assertEquals(4, LuaString.utf8("\uD83D\uDE00").length()); + } + + /** \0 是合法内容,且计入长度 */ + @Test + public void embeddedZeroBytes() { + LuaString s = LuaString.of(new byte[] { 'a', 0, 'b', 0 }); + assertEquals(4, s.length()); + assertEquals('a', s.byteAt(0)); + assertEquals(0, s.byteAt(1)); + assertEquals('b', s.byteAt(2)); + assertEquals(0, s.byteAt(3)); + } + + /** 任意字节(含非法 UTF-8)必须能原样保存 —— 字节串语义的硬指标 */ + @Test + public void arbitraryBytesRoundTrip() { + byte[] raw = new byte[256]; + for (int i = 0; i < 256; i++) { + raw[i] = (byte) i; + } + LuaString s = LuaString.of(raw); + assertEquals(256, s.length()); + for (int i = 0; i < 256; i++) { + assertEquals(i, s.byteAt(i)); + } + // 与 Java String 的往返不同:这里不能丢字节 + assertEquals(256, LuaString.of(s.toJavaString().getBytes(java.nio.charset.StandardCharsets.ISO_8859_1)).length()); + } + + @Test + public void concatenationAndSubstring() { + LuaString a = LuaString.latin1("ab"); + LuaString b = LuaString.of(new byte[] { 0, 'c' }); + assertEquals(4, a.concat(b).length()); + assertEquals(0, a.concat(b).byteAt(2)); + assertEquals("xy", LuaString.latin1("wxyz").substring(1, 3).toJavaString()); + } + + /** 相等与哈希基于内容 */ + @Test + public void contentEquality() { + LuaString a = LuaString.latin1("hello"); + LuaString b = LuaString.of(new byte[] { 'h', 'e', 'l', 'l', 'o' }); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + assertNotEquals(a, LuaString.latin1("hellp")); + } + + /** 超过 LUAI_MAXSHORTLEN 的不算短字符串(不参与驻留) */ + @Test + public void shortStringThreshold() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < Lua.MAXSHORTLEN; i++) { + sb.append('x'); + } + assertTrue(LuaString.latin1(sb.toString()).isShort()); + assertFalse(LuaString.latin1(sb.toString() + "x").isShort()); + } + + /** latin1() 只接受 0..255,超出应当报错而不是静默截断 */ + @Test + public void latin1RejectsOutOfRange() { + assertThrows(IllegalArgumentException.class, () -> LuaString.latin1("中")); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/LuaTableTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/LuaTableTest.java new file mode 100644 index 0000000..cd45296 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/LuaTableTest.java @@ -0,0 +1,279 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +/** 表层:两段结构、键的相等语义、#t 的边界、next 遍历、rehash */ +public class LuaTableTest { + + /** 取不到时 get 返回 null,语义上等价于 nil */ + private static boolean isNil(LuaValue v) { + return v == null || v.isNil(); + } + + @Test + public void getSetBasics() { + LuaTable t = new LuaTable(); + t.set(1, LuaValue.ofInteger(10)); + t.set("x", LuaValue.ofInteger(20)); + assertEquals(10, t.get(1).asInteger()); + assertEquals(20, t.get("x").asInteger()); + assertTrue(isNil(t.get(2))); + assertTrue(isNil(t.get("y"))); + } + + /** + * 整数键与字符串键是不同的项:t[1] 与 t["1"] 互不干扰。 + * 这是 Lua 表最容易在实现里被「顺手统一」掉的一条。 + */ + @Test + public void integerAndStringKeysAreDistinct() { + LuaTable t = new LuaTable(); + t.set(1, LuaValue.ofString(LuaString.latin1("int"))); + t.set("1", LuaValue.ofString(LuaString.latin1("str"))); + assertEquals("int", t.get(1).asString().toJavaString()); + assertEquals("str", t.get("1").asString().toJavaString()); + assertEquals(2, t.count()); + } + + /** 1 与 1.0 是同一个键(Lua 里 1 == 1.0) */ + @Test + public void integerAndFloatKeysAreTheSame() { + LuaTable t = new LuaTable(); + t.set(1, LuaValue.ofString(LuaString.latin1("first"))); + t.set(LuaValue.ofFloat(1.0), LuaValue.ofString(LuaString.latin1("second"))); + assertEquals("second", t.get(1).asString().toJavaString()); + assertEquals(1, t.count()); + } + + /** + * 整值浮点键在写入时被规范化成整数键。 + *

+ * 这条不是猜的,是拿 Lua 5.4.7 实测出来的: + *

+     *   t[1.0] = "a"  →  next(t) 键的 math.type 为 "integer",#t 为 1
+     *   t[0.0] = "a"  →  键为整数 0,但 0 不是数组下标,故 #t 仍为 0
+     *   t[1.5] = "a"  →  键保持浮点
+     *   t[2^63] = "a" →  超出 integer 范围,键保持浮点
+     * 
+ */ + @Test + public void integralFloatKeysAreNormalizedToInteger() { + LuaTable t = new LuaTable(); + t.set(LuaValue.ofFloat(1.0), LuaValue.ofInteger(1)); + LuaValue[] kv = t.next(LuaValue.ofNil()); + assertTrue(kv[0].isInteger(), "键应当是整数而不是浮点"); + assertEquals(1, kv[0].asInteger()); + // 规范化后它进了数组部分,所以 #t 是 1 + assertEquals(1, t.length()); + + // 0.0 → 整数 0,但 0 不是数组下标 + LuaTable z = new LuaTable(); + z.set(LuaValue.ofFloat(0.0), LuaValue.ofInteger(7)); + assertEquals(7, z.get(0).asInteger()); + assertEquals(0, z.length()); + + // 非整值浮点保持浮点 + LuaTable f = new LuaTable(); + f.set(LuaValue.ofFloat(1.5), LuaValue.ofInteger(2)); + assertTrue(f.next(LuaValue.ofNil())[0].isFloat()); + + // 超出 integer 范围的整值浮点保持浮点 + LuaTable big = new LuaTable(); + big.set(LuaValue.ofFloat(9.223372036854775808E18), LuaValue.ofInteger(3)); + assertTrue(big.next(LuaValue.ofNil())[0].isFloat()); + } + + /** 写 NaN 键报错,读 NaN 键返回 nil —— 同样是 Lua 5.4.7 实测的行为 */ + @Test + public void nanKey() { + LuaTable t = new LuaTable(); + // 读:不报错,返回 nil + assertTrue(isNil(t.get(LuaValue.ofFloat(Double.NaN)))); + // 写:报错 + LuaError e = org.junit.jupiter.api.Assertions.assertThrows(LuaError.class, + () -> t.set(LuaValue.ofFloat(Double.NaN), LuaValue.ofInteger(1))); + assertEquals("table index is NaN", e.value.asString().toJavaString()); + } + + /** 赋 nil 等于删除(键留在表里但值为 nil,与 C 版一致) */ + @Test + public void assigningNilRemovesValue() { + LuaTable t = new LuaTable(); + t.set(1, LuaValue.ofInteger(1)); + t.set("a", LuaValue.ofInteger(2)); + assertEquals(2, t.count()); + t.set(1, LuaValue.ofNil()); + t.set("a", LuaValue.ofNil()); + assertEquals(0, t.count()); + assertTrue(isNil(t.get(1))); + } + + /** 数组部分按 2 的幂增长,密集整数键会被安排进数组部分 */ + @Test + public void arrayPartGrowsForDenseIntegerKeys() { + LuaTable t = new LuaTable(); + assertEquals(0, t.arrayPartSize()); + t.set(1, LuaValue.ofInteger(1)); + assertEquals(1, t.arrayPartSize()); + t.set(2, LuaValue.ofInteger(2)); + assertEquals(2, t.arrayPartSize()); + for (int i = 3; i <= 10; i++) { + t.set(i, LuaValue.ofInteger(i)); + } + // 10 个密集键 → 数组部分至少 8(2 的幂),具体档位由 computesizes 决定 + assertTrue(t.arrayPartSize() >= 8, "array part = " + t.arrayPartSize()); + for (int i = 1; i <= 10; i++) { + assertEquals(i, t.get(i).asInteger()); + } + } + + /** 稀疏的大整数键不该撑爆数组部分 */ + @Test + public void sparseKeyDoesNotBlowUpArrayPart() { + LuaTable t = new LuaTable(); + t.set(1_000_000, LuaValue.ofInteger(1)); + assertTrue(t.arrayPartSize() <= 4, "array part = " + t.arrayPartSize()); + assertEquals(1, t.get(1_000_000).asInteger()); + assertEquals(1, t.count()); + } + + /** #t 返回一个合法边界:t[n] 非 nil 且 t[n+1] 为 nil */ + @Test + public void lengthReturnsABorder() { + LuaTable t = new LuaTable(); + for (int i = 1; i <= 5; i++) { + t.set(i, LuaValue.ofInteger(i)); + } + long n = t.length(); + assertFalse(isNil(t.get(n)), "t[" + n + "] 应当存在"); + assertTrue(isNil(t.get(n + 1)), "t[" + (n + 1) + "] 应当为 nil"); + } + + /** 边界藏在哈希部分时(数组满 + 哈希里有更大的整数键)也能找到 */ + @Test + public void lengthWithBorderInHashPart() { + LuaTable t = new LuaTable(); + for (int i = 1; i <= 3; i++) { + t.set(i, LuaValue.ofInteger(i)); + } + t.set("str", LuaValue.ofInteger(9)); // 逼出哈希部分 + t.set(4, LuaValue.ofInteger(4)); + t.set(5, LuaValue.ofInteger(5)); + long n = t.length(); + assertFalse(isNil(t.get(n))); + assertTrue(isNil(t.get(n + 1))); + } + + /** next 必须不重不漏地遍历所有项 */ + @Test + public void nextVisitsEveryEntryOnce() { + LuaTable t = new LuaTable(); + for (int i = 1; i <= 20; i++) { + t.set(i, LuaValue.ofInteger(i)); + } + t.set("a", LuaValue.ofInteger(100)); + t.set("b", LuaValue.ofInteger(200)); + t.set(LuaValue.ofFloat(2.5), LuaValue.ofInteger(300)); + + List seen = new ArrayList<>(); + LuaValue k = LuaValue.ofNil(); + while (true) { + LuaValue[] kv = t.next(k); + if (kv == null) { + break; + } + seen.add(kv[0].toString() + "=" + kv[1].asInteger()); + k = kv[0]; + } + assertEquals(23, seen.size()); + assertEquals(23, new java.util.HashSet<>(seen).size(), "next 不应重复: " + seen); + } + + /** 大量随机键反复插入/置 nil,最后存活的项必须与预期一致 */ + @Test + public void survivesMixedInsertDelete() { + LuaTable t = new LuaTable(); + java.util.Map intModel = new java.util.HashMap<>(); + java.util.Map strModel = new java.util.HashMap<>(); + java.util.Random r = new java.util.Random(0xC0FFEE); + + for (int step = 0; step < 3000; step++) { + if (r.nextInt(3) == 0) { + long k = r.nextInt(50) + 1; + t.set(k, LuaValue.ofNil()); + intModel.remove(k); + } else if (r.nextBoolean()) { + long k = r.nextInt(50) + 1; + long v = r.nextInt(1000); + t.set(k, LuaValue.ofInteger(v)); + intModel.put(k, v); + } else { + String k = "k" + r.nextInt(30); + long v = r.nextInt(1000); + t.set(k, LuaValue.ofInteger(v)); + strModel.put(k, v); + } + } + + assertEquals(intModel.size() + strModel.size(), t.count()); + for (java.util.Map.Entry e : intModel.entrySet()) { + LuaValue v = t.get(e.getKey()); + assertEquals(e.getValue().longValue(), v.asInteger(), "key " + e.getKey()); + } + for (java.util.Map.Entry e : strModel.entrySet()) { + assertEquals(e.getValue().longValue(), t.get(e.getKey()).asInteger(), "key " + e.getKey()); + } + } + + /** 表作为键按引用相等 */ + @Test + public void tableAsKey() { + LuaTable t = new LuaTable(); + LuaTable k1 = new LuaTable(); + LuaTable k2 = new LuaTable(); + t.set(LuaValue.ofTable(k1), LuaValue.ofInteger(1)); + t.set(LuaValue.ofTable(k2), LuaValue.ofInteger(2)); + assertEquals(1, t.get(LuaValue.ofTable(k1)).asInteger()); + assertEquals(2, t.get(LuaValue.ofTable(k2)).asInteger()); + assertEquals(2, t.count()); + } + + /** 布尔也能作键(true 与 false 是不同键) */ + @Test + public void booleanKeys() { + LuaTable t = new LuaTable(); + t.set(LuaValue.ofBoolean(true), LuaValue.ofInteger(1)); + t.set(LuaValue.ofBoolean(false), LuaValue.ofInteger(2)); + assertEquals(1, t.get(LuaValue.ofBoolean(true)).asInteger()); + assertEquals(2, t.get(LuaValue.ofBoolean(false)).asInteger()); + assertEquals(2, t.count()); + } + + /** 元表存取 */ + @Test + public void metatable() { + LuaTable t = new LuaTable(); + LuaTable mt = new LuaTable(); + assertNull(t.getMetatable()); + t.setMetatable(mt); + assertSame(mt, t.getMetatable()); + } + + /** 不存在的键传给 next 应当报错(对应 "invalid key to 'next'") */ + @Test + public void nextRejectsUnknownKey() { + LuaTable t = new LuaTable(); + t.set("a", LuaValue.ofInteger(1)); + org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, + () -> t.next(LuaValue.ofString(LuaString.latin1("absent")))); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/LuaValueTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/LuaValueTest.java new file mode 100644 index 0000000..6c104dd --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/LuaValueTest.java @@ -0,0 +1,83 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** 值层:类型标签、真假判定、原始相等(对应 lobject.c / lvm.c 的基础约定) */ +public class LuaValueTest { + + @Test + public void typeTags() { + assertEquals(Lua.TNIL, LuaValue.ofNil().baseType()); + assertEquals(Lua.TBOOLEAN, LuaValue.ofBoolean(true).baseType()); + assertEquals(Lua.TNUMBER, LuaValue.ofInteger(1).baseType()); + assertEquals(Lua.TNUMBER, LuaValue.ofFloat(1.0).baseType()); + assertEquals(Lua.TSTRING, LuaValue.ofString(LuaString.latin1("x")).baseType()); + assertEquals(Lua.TTABLE, LuaValue.ofTable(new LuaTable()).baseType()); + + assertEquals("nil", LuaValue.ofNil().typeName()); + assertEquals("boolean", LuaValue.ofBoolean(false).typeName()); + assertEquals("number", LuaValue.ofInteger(0).typeName()); + assertEquals("string", LuaValue.ofString(LuaString.latin1("")).typeName()); + assertEquals("table", LuaValue.ofTable(new LuaTable()).typeName()); + } + + /** 整数与浮点必须是不同的变体标签 —— math.type(1) 与 math.type(1.0) 靠它区分 */ + @Test + public void numberSubtypesAreDistinct() { + assertTrue(LuaValue.ofInteger(1).isInteger()); + assertFalse(LuaValue.ofInteger(1).isFloat()); + assertTrue(LuaValue.ofFloat(1.0).isFloat()); + assertFalse(LuaValue.ofFloat(1.0).isInteger()); + // 但二者都是「数字」 + assertTrue(LuaValue.ofInteger(1).isNumber()); + assertTrue(LuaValue.ofFloat(1.0).isNumber()); + } + + /** Lua 的真假:只有 nil 与 false 为假,0 与空字符串都是真 */ + @Test + public void truthiness() { + assertFalse(LuaValue.ofNil().toBoolean()); + assertFalse(LuaValue.ofBoolean(false).toBoolean()); + assertTrue(LuaValue.ofBoolean(true).toBoolean()); + assertTrue(LuaValue.ofInteger(0).toBoolean()); + assertTrue(LuaValue.ofFloat(0.0).toBoolean()); + assertTrue(LuaValue.ofString(LuaString.EMPTY).toBoolean()); + assertTrue(LuaValue.ofTable(new LuaTable()).toBoolean()); + } + + /** rawEqual 决定表键的行为:1 与 1.0 同键,1 与 "1" 不同键 */ + @Test + public void rawEqual() { + assertTrue(LuaValue.rawEqual(LuaValue.ofInteger(1), LuaValue.ofInteger(1))); + assertTrue(LuaValue.rawEqual(LuaValue.ofInteger(1), LuaValue.ofFloat(1.0))); + assertTrue(LuaValue.rawEqual(LuaValue.ofFloat(1.0), LuaValue.ofInteger(1))); + assertFalse(LuaValue.rawEqual(LuaValue.ofInteger(1), LuaValue.ofFloat(1.5))); + assertFalse(LuaValue.rawEqual(LuaValue.ofInteger(1), LuaValue.ofString(LuaString.latin1("1")))); + assertFalse(LuaValue.rawEqual(LuaValue.ofString(LuaString.latin1("a")), LuaValue.ofNil())); + assertTrue(LuaValue.rawEqual(LuaValue.ofNil(), LuaValue.ofNil())); + + // -0.0 与 0.0 相等(Lua 的数值相等如此) + assertTrue(LuaValue.rawEqual(LuaValue.ofFloat(-0.0), LuaValue.ofFloat(0.0))); + // NaN 与自身不等 + assertFalse(LuaValue.rawEqual(LuaValue.ofFloat(Double.NaN), LuaValue.ofFloat(Double.NaN))); + + // 表按引用 + LuaTable t = new LuaTable(); + assertTrue(LuaValue.rawEqual(LuaValue.ofTable(t), LuaValue.ofTable(t))); + assertFalse(LuaValue.rawEqual(LuaValue.ofTable(t), LuaValue.ofTable(new LuaTable()))); + } + + /** 字符串相等是逐字节的 */ + @Test + public void stringEqualityIsByBytes() { + LuaString a = LuaString.of(new byte[] { 'a', 0, 'b' }); + LuaString b = LuaString.of(new byte[] { 'a', 0, 'b' }); + LuaString c = LuaString.of(new byte[] { 'a', 0, 'c' }); + assertTrue(LuaValue.rawEqual(LuaValue.ofString(a), LuaValue.ofString(b))); + assertFalse(LuaValue.rawEqual(LuaValue.ofString(a), LuaValue.ofString(c))); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/MathLibTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/MathLibTest.java new file mode 100644 index 0000000..9c996be --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/MathLibTest.java @@ -0,0 +1,274 @@ +package com.github.esrrhs.fakescript.lua; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +/** + * math 库的差分用例 —— 阶段 7。 + *

+ * 挑例的原则与 {@link StringLibTest} 一致:只测「照 C 源码写会写错」的那些点。 + *

    + *
  • 子类型:{@code math.type} 与 {@code print} 的组合才看得出 + * integer/float 分裂——{@code math.floor(2.0)} 是 {@code 2}、 + * {@code math.modf(3)} 的第二个返回值是 {@code 0.0}、 + * {@code math.fmod(7,3)} 是 {@code 1} 而 {@code math.fmod(7.5,2)} 是 + * {@code 1.5}。
  • + *
  • {@code max/min} 走的是 {@code lua_compare}:会真的去比字符串, + * 也会真的报 {@code attempt to compare number with string}。
  • + *
  • {@code log} 的 base 2/10 专用分支:{@code math.log(8,2)} + * 必须是 {@code 3.0},走 {@code log(x)/log(base)} 会差一个 ulp。
  • + *
  • 随机序列逐位一致:显式 {@code randomseed} 之后,浮点序列、 + * 整数区间序列、{@code random(0)} 都要与参考实现相同。这是 + * xoshiro256** 唯一能被外部观测到的地方。
  • + *
  • 错误文本:{@code (value expected)}、{@code (zero)}、 + * {@code (interval is empty)}、{@code wrong number of arguments}。
  • + *
+ *

+ * 初始种子在参考实现里取自 {@code time(NULL)} 与 {@code L} 的地址,不可比, + * 所以每个随机用例都先显式播种。 + */ +class MathLibTest { + + @TestFactory + List mathScriptsMatchReference() { + List tests = new ArrayList<>(); + add(tests, "num", NUMBERS); + add(tests, "mm", MINMAX); + add(tests, "rand", RANDOM); + add(tests, "err", ERRORS); + return tests; + } + + private static void add(List tests, String tag, String[] srcs) { + for (int i = 0; i < srcs.length; i++) { + String src = srcs[i]; + int n = i; + tests.add(DynamicTest.dynamicTest(tag + "[" + n + "]", () -> Diff.assertMatches(src))); + } + } + + private static final String[] NUMBERS = { + + """ + print(math.pi, math.huge, math.maxinteger, math.mininteger) + print(math.type(1), math.type(1.0), math.type("x"), math.type(nil), math.type(true)) + print(math.type(math.maxinteger), math.type(math.huge)) + """, + + """ + print(math.abs(-3), math.abs(3), math.abs(-3.5), math.abs(math.mininteger)) + print(math.type(math.abs(-3)), math.type(math.abs(math.mininteger))) + print(math.abs("-7"), math.abs("-7.5")) + """, + + """ + print(math.floor(3.7), math.ceil(3.2), math.floor(-3.7), math.ceil(-3.2)) + print(math.type(math.floor(3.7)), math.type(math.floor(3.0))) + print(math.floor(3), math.ceil(3), math.type(math.floor(3))) + print(math.floor(-0.0), math.ceil(-0.0), math.type(math.floor(-0.0))) + print(math.floor(math.huge), math.type(math.floor(math.huge))) + print(math.floor(1e300), math.type(math.floor(1e300))) + print(math.floor("7.5"), math.ceil("-2.5")) + """, + + """ + print(math.modf(3.7), math.modf(-3.7), math.modf(3)) + print(math.type((math.modf(3.7))), math.type(select(2, math.modf(3.7)))) + print(math.modf(math.huge)) + local a, b = math.modf(0/0) + print(a ~= a, b ~= b) + print(math.modf(-0.0)) + """, + + """ + print(math.fmod(7,3), math.fmod(-7,3), math.fmod(7,-3)) + print(math.type(math.fmod(7,3)), math.type(math.fmod(7.5,2))) + print(math.fmod(7.5,2), math.fmod(-7.5,2), math.fmod(7.5,-2)) + print(math.fmod(math.mininteger, -1)) + """, + + """ + print(math.tointeger(3.0), math.tointeger(3.5), math.tointeger("7"), math.tointeger("7.5")) + print(math.tointeger(math.huge), math.tointeger(0/0), math.tointeger(2^70)) + print(math.tointeger(-3.0), math.tointeger(math.mininteger)) + print(math.type(math.tointeger(3.0)), math.tointeger(" 8 ")) + """, + + """ + print(math.ult(1,2), math.ult(2,1), math.ult(-1,2), math.ult(math.mininteger, -1)) + print(math.ult(0,0), math.ult(math.maxinteger, math.mininteger)) + """, + + // sqrt 是 IEEE 精确定义的,可以比到 17 位 + """ + print(string.format("%.17g", math.sqrt(2))) + print(math.log(8), math.log(8,2), math.log(100,10), math.log(1024,2)) + print(string.format("%.14g", math.log(1000,3)), string.format("%.14g", math.log(2,8))) + print(string.format("%.14g", math.log10(1000))) + print(math.log(math.huge), math.log(0), math.log(-1) ~= math.log(-1)) + print(string.format("%.14g", math.log(2, 2)), string.format("%.14g", math.log(1e300, 2))) + print(string.format("%.14g", math.log(0.5, 2)), string.format("%.14g", math.log(3, 2))) + """, + + // 超越函数只比到 14 位:Java 的 Math 是 fdlibm,参考实现链的是平台 + // libm,两者在最后一位上不同(详见 MathLib 的类注释) + """ + print(string.format("%.14g", math.sin(1)), string.format("%.14g", math.cos(1))) + print(string.format("%.14g", math.tan(1)), string.format("%.14g", math.asin(0.5))) + print(string.format("%.14g", math.acos(0.5)), string.format("%.14g", math.atan(0.5))) + print(string.format("%.14g", math.atan(1,2)), string.format("%.14g", math.atan2(1,2))) + print(string.format("%.14g", math.deg(math.pi)), string.format("%.14g", math.rad(180))) + print(string.format("%.14g", math.deg(1)), string.format("%.14g", math.rad(1))) + print(string.format("%.14g", math.sin(123456.789)), string.format("%.14g", math.cos(3.7))) + print(string.format("%.14g", math.exp(1)), string.format("%.14g", math.exp(-10))) + """, + + // frexp/ldexp 是纯位操作,可以比到 17 位 + """ + print(math.frexp(8)) + print(math.frexp(0), math.frexp(1), math.frexp(0.5), math.frexp(-3)) + print(math.frexp(math.huge), math.frexp(-math.huge)) + print(math.frexp(5e-324), math.frexp(1e300), math.frexp(2^53)) + print(string.format("%.17g", math.ldexp(3,2)), string.format("%.17g", math.ldexp(1,-1074))) + print(string.format("%.17g", math.ldexp(0.5, 1024)), string.format("%.17g", math.ldexp(0, 5))) + print(string.format("%.14g", math.pow(2,0.5)), string.format("%.17g", math.pow(2,10))) + print(string.format("%.14g", math.cosh(1)), string.format("%.14g", math.sinh(1))) + print(string.format("%.14g", math.tanh(1)), string.format("%.14g", math.pow(3, 0.3333))) + """, + }; + + private static final String[] MINMAX = { + + """ + print(math.max(1,2,3), math.min(1,2,3)) + print(math.max(1,2.0), math.min(1,2.0)) + print(math.type(math.max(1,2.0)), math.type(math.min(1,2.0))) + print(math.max(2.0,1), math.type(math.max(2.0,1))) + print(math.max(3), math.min(3), math.type(math.min(3))) + print(math.max(-0.0, 0.0), math.type(math.max(-0.0, 0.0))) + """, + + """ + print(math.max("a","b"), math.min("b","a"), math.max("abc","abd")) + print(math.max("", "a"), math.min("a", "")) + """, + + """ + print(math.max(1, 0/0), math.max(0/0, 1)) + print(math.min(1, 0/0), math.min(0/0, 1)) + print(math.max(math.huge, 1), math.min(-math.huge, 1)) + print(math.max(math.mininteger, math.maxinteger)) + """, + }; + + private static final String[] RANDOM = { + + """ + local s1, s2 = math.randomseed(42, 7) + print(s1, s2, math.type(s1), math.type(s2)) + local t = {} + for i = 1, 8 do t[i] = string.format("%.17g", math.random()) end + print(table.concat(t, " ")) + """, + + """ + math.randomseed(1234, 5678) + local t = {} + for i = 1, 10 do t[i] = tostring(math.random(1, 100)) end + print(table.concat(t, " ")) + """, + + """ + math.randomseed(99) + local t = {} + for i = 1, 10 do t[i] = tostring(math.random(-5, 5)) end + print(table.concat(t, " ")) + """, + + """ + math.randomseed(1, 2) + print(math.random(0), math.type(math.random(0))) + math.randomseed(1, 2) + print(math.random(0)) + """, + + """ + math.randomseed(7) + print(math.random(3,3), math.random(-3,-3), math.random(0,0)) + print(math.type(math.random(1,1))) + """, + + """ + -- 区间长度是 2 的幂与非 2 的幂各来一遍:前者走掩码、后者走梅森数重投 + math.randomseed(2024) + local t = {} + for i = 1, 8 do t[i] = math.random(0, 255) end + print(table.concat(t, " ")) + math.randomseed(2024) + local u = {} + for i = 1, 8 do u[i] = math.random(0, 100) end + print(table.concat(u, " ")) + """, + + """ + math.randomseed(5) + print(math.random(math.mininteger, math.maxinteger) ~= nil) + print(math.type(math.random(-1, 1))) + """, + + """ + local a, b = math.randomseed() + print(math.type(a), math.type(b)) + print(math.random(1, 10) >= 1) + """, + }; + + private static final String[] ERRORS = { + + """ + print(pcall(math.max)) + print(pcall(math.min)) + print(pcall(math.tointeger)) + print(pcall(math.type)) + print(pcall(math.abs)) + print(pcall(math.abs, "x")) + print(pcall(math.floor, "x")) + print(pcall(math.log, 8, "x")) + """, + + """ + print(pcall(math.fmod, 1)) + print(pcall(math.fmod, 1, 0)) + print(pcall(math.fmod, "a", 1)) + print(pcall(math.fmod, 1, "a")) + print(pcall(math.ult, 1)) + print(pcall(math.ult, 1.5, 2)) + print(pcall(math.ult, 1, "x")) + """, + + """ + print(pcall(math.random, 1, 2, 3)) + print(pcall(math.random, 5, 1)) + print(pcall(math.random, "a")) + print(pcall(math.random, 1.5)) + print(pcall(math.randomseed, 1.5)) + print(pcall(math.randomseed, "x")) + """, + + """ + print(pcall(math.max, 1, "b")) + print(pcall(math.min, "a", 1)) + local mt = setmetatable({}, {__lt = function(a,b) return true end}) + print(pcall(math.max, mt, 1)) + """, + + """ + local f = function() return math.fmod(nil, 1) end + print(pcall(f)) + print(pcall(math.fmod, nil, 1)) + """, + }; +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/MetamethodTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/MetamethodTest.java new file mode 100644 index 0000000..100b8aa --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/MetamethodTest.java @@ -0,0 +1,1167 @@ +package com.github.esrrhs.fakescript.lua; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +/** + * 元方法的差分矩阵 —— 阶段 12。 + *

+ * 为什么单开一类:前面几轮把三个层次都扫过了——「函数在不在这个库里」(符号 + * 对撞)、「传错参数报什么错」(错误参数网格)、「传对参数返回什么」(返回值 + * 网格)——但三层都绕开了元方法。而元方法是 Lua 里唯一能把运算符语义整 + * 个换掉的地方:{@code lvm.c} 的 {@code OP_ADD..OP_LE} 有一半分支挂在 + * {@code luaT_trybinTM} 上,另一半挂在 {@code luaV_finishget} / + * {@code luaV_finishset} / {@code luaV_objlen} / {@code OP_CONCAT} 上。 + *

+ * 四条线各自成片: + *

    + *
  • 片一:二元运算符 × 操作数类型 × 元表所在侧。 + * 八种类型两两配对、元表摆在左 / 右 / 两侧 / 不摆,再乘上元方法本身的 + * 六种形态。两侧都摆那一档是重点——算术取左边的、相等两边都试、小于与 + * 小于等于先试自己的再退回对方的,三条规则各不相同。
  • + *
  • 片二:__index / __newindex / __call / __len / 一元 / 拼接链。 + * 这些不是运算符,走的是另一条路。{@code __index} 是表时会循环再取 + * (不是递归),成环时报 {@code '__index' chain too long}; + * {@code OP_CONCAT} 是从右往左两两折的,三个值里只有中间那个有 + * {@code __concat} 时,调用顺序与「从左到右」的直觉相反。
  • + *
  • 片三:元方法里 yield。第 7 条:这类 bug 只在协程里现形。 + * 外套分三种(裸写 / {@code pcall} / C 函数内部),三种给出三种答案—— + * 少一种都看不出续体写没写对。
  • + *
  • 片四:__pairs / __close / raw* / 元表保护。协议类元方法与后门 + * 函数的参数校验。
  • + *
+ *

+ * 一个前提(实测,别凭记忆推):脚本里只有表和完整 userdata 能挂实例元表。 + * {@code setmetatable} 的第 1 个参数被 {@code luaL_checktype} 限成表;给别的 + * 值挂只能走 {@code debug.setmetatable},改的是全局类型元表。所以片一里对 + * 非表类型一律用 {@code debug.setmetatable},而且用完立刻还原原值 + * (字符串那份是 string 库自己装上去的,还成 nil 会把后面全炸掉)。 + * + * @see LuaTm + * @see Vm#finishGet + * @see Vm#finishSet + * @see Vm#objLen + */ +class MetamethodTest { + + @BeforeAll + static void needReference() { + assumeTrue(ReferenceLua.find() != null, + "no lua5.4 reference binary; set LUA54_BIN"); + } + + /** 第一片:二元运算符 × 操作数类型 × 元表所在侧 */ + @Test + void binaryOperatorGridMatchesReference() { + Diff.assertMatches(""" +-- 元方法矩阵 · 第一片:二元运算符 × 操作数类型 × 元表所在侧 +-- +-- 为什么单独开一片:前面几轮扫的是「函数在不在这个库里」「传错参数报什么错」 +-- 「传对参数返回什么」,三层都绕开了元方法——而元方法是 Lua 里唯一能把运算符 +-- 语义整个换掉的地方,lvm.c 的 OP_ADD..OP_LE 有一半分支挂在它上面。 +-- 更要紧的是:元方法缺失时抛的那条消息 +-- (attempt to perform arithmetic on a X value ...)由 varinfo 反推名字拼出来, +-- 跟前面那片 varinfo 网格是同一条链路,换个入口再压一遍。 +-- +-- 一个前提(实测,别凭记忆推):脚本里只有表和完整 userdata 能挂实例元表。 +-- setmetatable 的第 1 个参数被 luaL_checktype 限成表;给别的值挂,走的是 +-- debug.setmetatable,改的是全局类型元表(mt[LUA_TNIL] 那一档)。 +-- 所以下面对非表类型一律用 debug.setmetatable,并且用完立刻还原—— +-- 不然会把后面的用例全污染掉(字符串那份尤其:string 库自己就占着它)。 + +local TN = { "nil", "false", "true", "int", "flt", "str", "tbl", "fn" } + +local function mkv(t) + if t == "nil" then return nil end + if t == "false" then return false end + if t == "true" then return true end + if t == "int" then return 7 end + if t == "flt" then return 2.5 end + if t == "str" then return "ab" end + if t == "tbl" then return {} end + return print +end + +--[[ + 运算符 → 要试着挂的元方法名(是个列表)。 + 两条不显然的: +

    +
  • {@code ~=} 归 {@code __eq}、{@code >} 归 {@code __lt}、{@code >=} 归 + {@code __le} —— 它们没有自己的元方法,列进来只为压「取反 / 换位」。
  • +
  • {@code <=} 与 {@code >=} 挂两个:{@code __le} 缺失时 + {@code luaT_callorderTM} 会退回 {@code !(p2 < p1)}(LUA_COMPAT_LT_LE)。 + 只挂 {@code __le} 的话这条兜底一行也走不到 —— 第一版就是这样漏的。
  • +
+]] +local BIN = { + { "+", { "__add" } }, { "-", { "__sub" } }, { "*", { "__mul" } }, + { "/", { "__div" } }, { "//", { "__idiv" } }, { "%", { "__mod" } }, + { "^", { "__pow" } }, + { "&", { "__band" } }, { "|", { "__bor" } }, { "~", { "__bxor" } }, + { "<<", { "__shl" } }, { ">>", { "__shr" } }, + { "..", { "__concat" } }, + { "==", { "__eq" } }, { "~=", { "__eq" } }, + { "<", { "__lt" } }, { "<=", { "__le", "__lt" } }, + { ">", { "__lt" } }, { ">=", { "__le", "__lt" } }, +} + +--[[ + 元表摆在哪一侧。 + 「两侧都有」这一档最容易写错:算术取左边的;相等两边都试; + 小于 / 小于等于先试自己的、再退回对方的——三条规则各不相同, + 光跑「左边有」根本看不出来。 +]] +local MT = { + { "none", false, false }, + { "L", true, false }, + { "R", false, true }, + { "B", true, true }, +} + +-- 元方法本身的形态:正常返回 / 不返回 / 返回多个 / 不是函数 +local KIND = { "fn", "void", "multi", "num", "tbl", "str" } + +-- 结果归一:整数和浮点必须分开看,地址一律去掉(第 42 条)。 +-- 放在 mkmeth 前面,因为下面要用它当上值。 +local function show(v) + local t = type(v) + if t == "string" then return "s[" .. v .. "]" end + if t == "number" then + if math.type(v) == "integer" then return "i" .. tostring(v) end + return "f" .. tostring(v) + end + return t +end + +local function mkmeth(kind, tag) + if kind == "fn" then + return function(x, y) return tag .. ":" .. show(x) .. "," .. show(y) end + elseif kind == "void" then + return function(x, y) end + elseif kind == "multi" then + return function(x, y) return tag, "extra" end + elseif kind == "num" then + return 123 + elseif kind == "tbl" then + return { 1 } + else + return "notafunction" + end +end + +--[[ + 每次求值都在 pcall 里。 + 未捕获的抛错会让脚本当场死掉——后面几百行一条也不会比,而用例照样绿。 + 这是第 38 条,每一片网格都要照做。 +]] +local function eval(f, a, b) + local r = table.pack(pcall(f, a, b)) + local out = { tostring(r[1]) } + for i = 2, r.n do out[#out + 1] = show(r[i]) end + return table.concat(out, " ") +end + +--[[ + 给任意类型的值挂元表,返回「撤销函数」。 + 表走 setmetatable;其余类型走 debug.setmetatable(改的是全局类型元表)。 + 表不能用 debug.setmetatable 之外的路子,而别的类型只能用它, + 所以两条分岔都得走一遍——矩阵里 8 种类型正好把两条都压到。 +]] +local function withmt(v, mt) + if type(v) == "table" then + setmetatable(v, mt) + return function() setmetatable(v, nil) end + end + local old = debug.getmetatable(v) + debug.setmetatable(v, mt) + return function() debug.setmetatable(v, old) end +end + +print("BEGIN", #TN, #BIN, #MT, #KIND) + +for oi = 1, #BIN do + local op, names = BIN[oi][1], BIN[oi][2] + local f = load("local a, b = ... return a " .. op .. " b", "=X") + for ni = 1, #names do + local mname = names[ni] + for ai = 1, #TN do + for bi = 1, #TN do + for mi = 1, #MT do + local cfg = MT[mi] + -- 无元表这一档只跑一遍:摆法一样,多跑纯属浪费行数 + local kinds = (mi == 1) and { "fn" } or KIND + -- 只在第一个元方法名上跑「无元表」,后面的只跑有元表的 + if ni == 1 or mi > 1 then + for ki = 1, #kinds do + local a, b = mkv(TN[ai]), mkv(TN[bi]) + local undo = {} + if cfg[2] then + undo[#undo + 1] = withmt(a, { [mname] = mkmeth(kinds[ki], "L") }) + end + if cfg[3] then + undo[#undo + 1] = withmt(b, { [mname] = mkmeth(kinds[ki], "R") }) + end + local r = eval(f, a, b) + for i = #undo, 1, -1 do undo[i]() end + print(op, TN[ai], TN[bi], cfg[1], mname, kinds[ki], r) + end + end + end + end + end + end +end + +print("END") + """); + } + + /** 第二片:__index / __newindex / __call / __len / 一元 / 拼接链 */ + @Test + void indexCallLenUnaryConcatGridMatchesReference() { + Diff.assertMatches(""" +-- 元方法矩阵 · 第二片:索引 / 新索引 / 调用 / 长度 / 一元 / 拼接链 +-- +-- 第一片把所有运算符过了一遍,但元方法里有一半不是运算符: +-- __index / __newindex / __call / __len / __concat 这些走的是 +-- lvm.c 里完全另一条路(OP_GETTABUP 的 TM 分支、luaV_objlen、 +-- OP_CONCAT 的 catrecursively)。这一片专门扫它们。 +-- +-- 不变的规矩:每个求值都包在 pcall 里(第 38 条), +-- 结果里的地址一律归一化(第 42 条), +-- 整数和浮点分开看(第 11 条)。 + +local function show(v) + local t = type(v) + if t == "string" then return "s[" .. v .. "]" end + if t == "number" then + if math.type(v) == "integer" then return "i" .. tostring(v) end + return "f" .. tostring(v) + end + return t +end + +-- 每次求值都在 pcall 里:未捕获的抛错让脚本当场死掉, +-- 后面几百行一条也不会比,而用例照样绿。 +local function go(tag, f, ...) + local r = table.pack(pcall(f, ...)) + local out = { tag, tostring(r[1]) } + for i = 2, r.n do out[#out + 1] = show(r[i]) end + print(table.concat(out, " ")) +end + +-- 每次都建新表:元表状态会在用例之间互相污染 +local function tbl() return {} end + +--[[ + __index / __newindex 的六种形态。 + 「是表」这一档要单独盯:luaV_finishget 在 __index 是表时会递归再取一次, + 而且是循环不是递归(while 循环里换 t = tm)。__index 链成环时该报 + '__index' chain too long; possibly looping —— 那也是这一档的事。 +]] +local FORMS = { "absent", "nil", "func", "tbl", "num", "str", "tblchain" } + +local function mkform(name, kind, tag) + if kind == "absent" then return nil end + if kind == "nil" then return nil end + if kind == "func" then + return function(t, k) return tag .. ":" .. tostring(k) end + end + if kind == "tbl" then return { x = tag .. "X", y = tag .. "Y", [1] = tag .. "1" } end + if kind == "num" then return 42 end + if kind == "str" then return "plainstring" end + if kind == "tblchain" then + -- __index 是表,那张表自己又有 __index + local inner = setmetatable({}, { __index = { deep = tag .. "DEEP", x = tag .. "INX" } }) + return inner + end +end + +local KEYS = { "x", "y", "z", "deep" } + +print("BEGIN") + +-- ============ 1) __index:取值 ============ +for fi = 1, #FORMS do + local kind = FORMS[fi] + for ki = 1, #KEYS do + local k = KEYS[ki] + local t = tbl() + local m = mkform("__index", kind, "I") + if kind ~= "absent" then + setmetatable(t, { __index = m }) + end + go("IDX", function(tt, kk) return tt[kk] end, t, k) + end + -- 整数键与浮点键 + for _, k in ipairs({ 1, 2.0, 2.5, true }) do + local t = tbl() + local m = mkform("__index", kind, "I") + if kind ~= "absent" then + setmetatable(t, { __index = m }) + end + go("IDXK", function(tt, kk) return tt[kk] end, t, k) + end + -- nil 与 NaN 键:应当报错 + local t = tbl() + local m = mkform("__index", kind, "I") + if kind ~= "absent" then + setmetatable(t, { __index = m }) + end + go("IDXNIL", function(tt) return tt[nil] end, t) + go("IDXNAN", function(tt) return tt[0 / 0] end, t) +end + +-- ============ 2) __newindex:赋值 ============ +for fi = 1, #FORMS do + local kind = FORMS[fi] + for ki = 1, #KEYS do + local k = KEYS[ki] + local t = tbl() + local m + if kind == "func" then + m = function(tt, kk, v) rawset(tt, kk, "S" .. tostring(v)) end + else + m = mkform("__newindex", kind, "N") + end + if kind ~= "absent" then + setmetatable(t, { __newindex = m }) + end + go("NIDX", function(tt, kk) + tt[kk] = "V" + return rawget(tt, kk), tt[kk] + end, t, k) + end + -- 键已存在(rawget 命中):__newindex 不该被调用 + local t = { x = "old" } + local hit = false + if kind == "func" then + setmetatable(t, { __newindex = function(tt, kk, v) hit = true end }) + elseif kind ~= "absent" then + setmetatable(t, { __newindex = mkform("__newindex", kind, "N") }) + end + go("NIDXHIT", function(tt) + tt.x = "new" + return rawget(tt, "x"), hit + end, t) + -- nil 键 + local t2 = tbl() + if kind ~= "absent" then + setmetatable(t2, { __newindex = (kind == "func") + and function(tt, kk, v) end or mkform("__newindex", kind, "N") }) + end + go("NIDXNIL", function(tt) tt[nil] = 1 return rawget(tt, "x") end, t2) +end + +--[[ + ============ 3) __index / __newindex 成环 ============ + luaV_finishget 里的循环有 LUAI_MAXCCALLS 上限,超了报 + "'__index' chain too long; possibly looping"。 + 这一档是「循环 vs 递归」的分水岭——写成递归会 Java StackOverflow。 +]] +do + local a, b = tbl(), tbl() + setmetatable(a, { __index = b }) + setmetatable(b, { __index = a }) + go("IDXLOOP", function(tt) return tt.nope end, a) + + local c, d = tbl(), tbl() + setmetatable(c, { __newindex = d }) + setmetatable(d, { __newindex = c }) + go("NIDXLOOP", function(tt) tt.nope = 1 return "ok" end, c) +end + +-- ============ 4) __call ============ +do + local CALLS = { "absent", "func", "num", "str", "tbl" } + for i = 1, #CALLS do + local kind = CALLS[i] + local m + if kind == "func" then + m = function(self, ...) return "C", select("#", ...), ... end + elseif kind == "num" then m = 7 + elseif kind == "str" then m = "s" + elseif kind == "tbl" then m = {} + end + local t = tbl() + if kind ~= "absent" then setmetatable(t, { __call = m }) end + for na = 0, 3 do + go("CALL", function(tt, n) + local args = {} + for j = 1, n do args[j] = j end + return tt(table.unpack(args, 1, n)) + end, t, na) + end + end + -- userdata 上的 __call + local ud = io.stdout + local old = debug.getmetatable(ud) + debug.setmetatable(ud, { __call = function(self, x) return "UD", x end }) + go("CALLUD", function(u) return u(9) end, ud) + debug.setmetatable(ud, old) +end + +-- ============ 5) __len:返回值类型决定一切 ============ +do + local LENS = { + { "int", function() return 5 end }, + { "fltint", function() return 6.0 end }, + { "fltfrac", function() return 6.5 end }, + { "str", function() return "7" end }, + { "strbad", function() return "x" end }, + { "tbl", function() return {} end }, + { "bool", function() return true end }, + { "nil", function() return nil end }, + { "void", function() end }, + { "neg", function() return -3 end }, + { "huge", function() return math.huge end }, + { "nan", function() return 0 / 0 end }, + { "maxi", function() return math.maxinteger end }, + { "mini", function() return math.mininteger end }, + } + for i = 1, #LENS do + local nm, f = LENS[i][1], LENS[i][2] + local t = setmetatable({ 1, 2, 3 }, { __len = f }) + go("LEN", function(tt) return #tt end, t) + go("RAWLEN", function(tt) return rawlen(tt) end, t) + print("LENNAME", nm) + end + -- 没有 __len 的表与字符串 + local plain = { 1, 2, 3 } + go("LENPLAIN", function(tt) return #tt end, plain) + go("LENSTR", function(s) return #s end, "hello") +end + +-- ============ 6) 一元:- 与 ~ ============ +do + local TN = { "nil", "false", "true", "int", "flt", "str", "tbl", "fn" } + local function mkv(t) + if t == "nil" then return nil end + if t == "false" then return false end + if t == "true" then return true end + if t == "int" then return 7 end + if t == "flt" then return 2.5 end + if t == "str" then return "12" end + if t == "tbl" then return {} end + return print + end + local UN = { "-", "~", "#", "not" } + for ui = 1, #UN do + local op = UN[ui] + for ai = 1, #TN do + local fneg = load("local a = ... return " .. op .. " a", "=U") + local afn = load("local a = ... return " .. op .. " a", "=U") + -- 无元表 + go("UN" .. op, fneg, mkv(TN[ai])) + -- 挂 __unm / __bnot / __len + for _, nm in ipairs({ "__unm", "__bnot", "__len" }) do + local v = mkv(TN[ai]) + -- 改全局类型元表要存下原值再还回去,别还成 nil(字符串那份是 + -- string 库装上去的),否则后面所有 ("x"):len() 都会炸 + local old = (type(v) == "table") and nil or debug.getmetatable(v) + if type(v) == "table" then + setmetatable(v, { [nm] = function(x) return nm .. show(x) end }) + else + debug.setmetatable(v, { [nm] = function(x) return nm .. show(x) end }) + end + go("UN" .. op .. nm, afn, v) + if type(v) == "table" then + setmetatable(v, nil) + else + debug.setmetatable(v, old) + end + end + end + end +end + +--[[ + ============ 7) 拼接链 ============ + OP_CONCAT 是「一批一批拼」:luaV_concat 一次收栈顶若干个, + 有 __concat 就走 catrecursively —— 它是从后往前两两拼的, + 而且一次只调一个元方法。所以三个值里只有中间那个有 __concat 时, + 调用顺序与「从左到右」的直觉是反的。 +]] +do + local function mk(tag) + -- 用 show 而不是 tostring:表地址每次都不同,不归一化整片假红(第 42 条) + return setmetatable({}, { __concat = function(x, y) return "(" .. show(x) .. tag .. show(y) .. ")" end }) + end + for mask = 0, 7 do + local a, b, c = "a", "b", "c" + if mask & 1 ~= 0 then a = mk("A") end + if mask & 2 ~= 0 then b = mk("B") end + if mask & 4 ~= 0 then c = mk("C") end + go("CAT", function(x, y, z) return x .. y .. z end, a, b, c) + print("CATMASK", mask) + end + -- 四五个值:一批拼不完的情况 + for mask = 0, 15 do + local v = {} + for i = 1, 4 do + v[i] = (mask & (1 << (i - 1)) ~= 0) and mk("" .. i) or ("" .. i) + end + go("CAT4", function(a, b, c, d) return a .. b .. c .. d end, + v[1], v[2], v[3], v[4]) + print("CAT4MASK", mask) + end +end + +print("END") + """); + } + + /** 第三片:元方法里 yield */ + @Test + void yieldInsideMetamethodMatchesReference() { + Diff.assertMatches(""" +-- 元方法矩阵 · 第三片:元方法里 yield +-- +-- 为什么单开一片:协程侧只有 pcall / xpcall 注册了续体(第 5 条), +-- 而元方法是由 VM 指令调的——OP_ADD 走 callTmMightYield, +-- OP_CONCAT 走 catrecursively,__call 走普通调用。三条路的续体各不相同, +-- 而且「能不能 yield」取决于外面套的是 C 函数还是 Lua 函数 +-- (跨 C 边界 yield 要报 attempt to yield across a C-call boundary)。 +-- 前面几片全在主线程跑,这一片是把同一批元方法塞进 coroutine.create 再跑一遍 +-- ——第 7 条:这类 bug 只在协程里现形。 + +local function show(v) + local t = type(v) + if t == "string" then return "s[" .. v .. "]" end + if t == "number" then + if math.type(v) == "integer" then return "i" .. tostring(v) end + return "f" .. tostring(v) + end + return t +end + +local function go(tag, f, ...) + local r = table.pack(pcall(f, ...)) + local out = { tag, tostring(r[1]) } + for i = 2, r.n do out[#out + 1] = show(r[i]) end + print(table.concat(out, " ")) +end + +--[[ + 每个事件跑三种「外套」: + bare —— 协程里裸写这个表达式 + pcall —— 包 pcall(续体已注册,应当能 yield 并被 pcall 收住) + cfn —— 包在 C 函数里(table.insert / table.sort),应当报跨 C 边界 + 同一条表达式,三种外套给出三种答案,少一种都看不出续体写没写对。 +]] +local function runcase(tag, build) + for _, outer in ipairs({ "bare", "pcall", "cfn" }) do + local co = coroutine.create(function() + if outer == "bare" then + return build() + elseif outer == "pcall" then + return pcall(build) + else + return pcall(function() + local t = { 1, 2, 3 } + table.insert(t, 4) -- 触发 __len 的 C 边界 + return "ins" + end) + end + end) + local ok, a = coroutine.resume(co) + print(tag, outer, "r1", tostring(ok), show(a)) + local ok2, b = coroutine.resume(co, "R1") + print(tag, outer, "r2", tostring(ok2), show(b)) + local ok3, c = coroutine.resume(co, "R2") + print(tag, outer, "r3", tostring(ok3), show(c)) + end +end + +print("BEGIN") + +--[[ + ============ 1) 算术 / 位运算的 __add 里 yield ============ + 注意 OP_ADD 这类指令是「先算再写」:元方法的结果先是落在栈顶, + 再由 Protect 之后的写回指令搬进目标寄存器。yield 恰好卡在中间, + 恢复时那个临时结果不能再算一遍。 +]] +do + local mtadd = { __add = function(x, y) return coroutine.yield("in-add") end } + runcase("ADD", function() + local a, b = setmetatable({}, mtadd), setmetatable({}, mtadd) + local v = a + b + return "sum", v + end) + + local mtconcat = { __concat = function(x, y) return coroutine.yield("in-concat") end } + runcase("CONCAT", function() + local a, b = setmetatable({}, mtconcat), setmetatable({}, mtconcat) + local v = a .. b + return "cat", v + end) + + local mtunm = { __unm = function(x) return coroutine.yield("in-unm") end } + runcase("UNM", function() + local a = setmetatable({}, mtunm) + local v = -a + return "neg", v + end) + + local mtlen = { __len = function(x) return coroutine.yield("in-len") end } + runcase("LEN", function() + local a = setmetatable({ 1, 2 }, mtlen) + local v = #a + return "len", v + end) + + local mteq = { __eq = function(x, y) return coroutine.yield("in-eq") end } + runcase("EQ", function() + local a, b = setmetatable({}, mteq), setmetatable({}, mteq) + local v = (a == b) + return "eq", v + end) + + local mtlt = { __lt = function(x, y) return coroutine.yield("in-lt") end } + runcase("LT", function() + local a, b = setmetatable({}, mtlt), setmetatable({}, mtlt) + local v = (a < b) + return "lt", v + end) + + -- __le 缺失 → 退回 __lt,续体里还要取反(CallInfo.leq) + local mtle = { __lt = function(x, y) return coroutine.yield("in-lt-for-le") end } + runcase("LE", function() + local a, b = setmetatable({}, mtle), setmetatable({}, mtle) + local v = (a <= b) + return "le", v + end) + + local mtindex = { __index = function(t, k) return coroutine.yield("in-index") end } + runcase("INDEX", function() + local a = setmetatable({}, mtindex) + local v = a.foo + return "idx", v + end) + + local mtnewindex = { __newindex = function(t, k, v) return coroutine.yield("in-newindex") end } + runcase("NEWINDEX", function() + local a = setmetatable({}, mtnewindex) + a.foo = 1 + return "nidx", "ok" + end) + + local mtcall = { __call = function(self, ...) return coroutine.yield("in-call") end } + runcase("CALL", function() + local a = setmetatable({}, mtcall) + local v = a(1, 2) + return "call", v + end) +end + +--[[ + ============ 2) 在 C 函数里触发 __len / __lt ============ + 这些才是「跨 C 边界」的真正入口:table.insert 取长度、table.sort 比较。 + 参考实现报 attempt to yield across a C-call boundary(或 cannot resume + dead coroutine 之类),语义跟 Lua 函数里的 yield 完全不同。 +]] +--[[ + 每个 case 在协程里跑,然后连 resume 三轮: + r1 —— 第一次 resume,多半是 yield 出来的值 + r2 —— 带着 "R1" 恢复,元方法应当返回它 + r3 —— 协程已死,应当报 cannot resume dead coroutine + 重点在 table.insert / table.sort 那几条:它们没有注册续体, + 所以元方法里 yield 必须报 attempt to yield across a C-call boundary。 +]] +local function resume3(tag, fn) + -- 直接把 pcall 的结果摊开返回:resume 于是给 (true, ok, err...), + -- 错误消息看得见。包成 table 的话只能看到 "table: 0x..."。 + local co = coroutine.create(function() return pcall(fn) end) + for i = 1, 3 do + local r = table.pack(coroutine.resume(co, "R" .. i)) + local out = { tag, "r" .. i } + for j = 1, r.n do out[#out + 1] = show(r[j]) end + print(table.concat(out, " ")) + end +end + +do + local ylen = setmetatable({ 1, 2, 3 }, { + __len = function() return coroutine.yield("in-len-c") end, + }) + local ylt = setmetatable({ 3, 1, 2 }, { + __lt = function(a, b) return coroutine.yield("in-lt-c") end, + }) + local yindex = setmetatable({}, { + __index = function(t, k) return coroutine.yield("in-index-c") end, + }) + local ynewindex = setmetatable({}, { + __newindex = function(t, k, v) return coroutine.yield("in-newindex-c") end, + }) + local ycall = setmetatable({}, { + __call = function(s, ...) return coroutine.yield("in-call-c") end, + }) + local yconcat = setmetatable({}, { + __concat = function(a, b) return coroutine.yield("in-concat-c") end, + }) + + resume3("CFN_INSERT", function() table.insert(ylen, 9) return "ok" end) + resume3("CFN_REMOVE", function() return table.remove(ylen) end) + resume3("CFN_CONCAT", function() return yconcat .. "x" end) + resume3("CFN_SORT", function() table.sort(ylt) return "sorted" end) + -- 这里正是要验「tostring 不认 __index」,结果必然是 "table: 0x..."; + -- 地址每次都不同,不抹掉这一片假红(第 42 条) + resume3("CFN_TOSTRING", function() + return (tostring(yindex):gsub("0x%x+", "PTR")) + end) + resume3("CFN_RAWGET", function() return rawget(yindex, "k") end) + resume3("CFN_RAWEQUAL", function() return rawequal(ylen, ylt) end) + resume3("CFN_GSUB", function() return string.gsub("abc", "b", yindex) end) + resume3("CFN_CALL", function() return ycall(1) end) + resume3("CFN_NEWIDX", function() ynewindex.x = 1 return "ok" end) + resume3("CFN_LEN", function() return #ylen end) +end + +--[[ + ============ 3) 跨 C 边界:在 C 函数内部触发 yield ============ + 第 5 条:只有 pcall / xpcall 注册了续体,所以 table.sort 的比较函数里 + yield 必须报错。这条要与「Lua 函数里 yield 成功」成对出现才说明续体写对了。 +]] +do + local co = coroutine.create(function() + return pcall(function() + return table.sort({ 3, 1, 2 }, function(a, b) + coroutine.yield("from-comp") + return a < b + end) + end) + end) + print("SORTYIELD r1", tostring(coroutine.resume(co))) + + -- pcall 里的 yield 应当成功 + local co2 = coroutine.create(function() + return pcall(function() return coroutine.yield("in-pcall") end) + end) + print("PCALLYIELD r1", tostring(coroutine.resume(co2))) + print("PCALLYIELD r2", tostring(coroutine.resume(co2, "back"))) + + -- xpcall 里的 yield + local co3 = coroutine.create(function() + return xpcall(function() return coroutine.yield("in-xpcall") end, + function(e) return "H:" .. tostring(e) end) + end) + print("XPCALLYIELD r1", tostring(coroutine.resume(co3))) + print("XPCALLYIELD r2", tostring(coroutine.resume(co3, "back2"))) +end + +--[[ + ============ 4) 元方法里抛错:错误对象与位置前缀 ============ + 元方法抛的错要经过 C 边界传回去;非字符串的错误对象在 pcall 之外 + 会被 msghandler 处理成 (error object is a X value)。 +]] +--[[ + ============ 3) 元方法里抛错:错误对象与位置前缀 ============ + 第 4 条:只有 LuaError 会被 pcall 接住,实现自身的 Java 异常必须继续上冒。 + 这里压的是「错误对象不是字符串」那一档——脚本里 error(42) 传到宿主手上时 + 要变成 (error object is a number value)。 +]] +do + local ERR = { + { "string", function() error("boom") end }, + { "string0", function() error("boom", 0) end }, + { "num", function() error(42) end }, + { "nil", function() error() end }, + { "tbl", function() error({}) end }, + { "bool", function() error(true) end }, + { "strmt", function() error(setmetatable({}, { __tostring = function() return "TS" end })) end }, + } + local EVENTS = { + "__add", "__sub", "__mul", "__div", "__idiv", "__mod", "__pow", + "__band", "__bor", "__bxor", "__shl", "__shr", "__unm", "__bnot", + "__concat", "__len", "__eq", "__lt", "__le", "__index", "__newindex", + "__call", + } + for ei = 1, #EVENTS do + local ev = EVENTS[ei] + for ri = 1, #ERR do + local nm, f = ERR[ri][1], ERR[ri][2] + local t = setmetatable({}, { [ev] = f }) + if ev == "__unm" or ev == "__bnot" or ev == "__len" then + local op = (ev == "__unm") and "-" or ((ev == "__bnot") and "~" or "#") + go("ERR" .. ev .. nm, load("local a = ... return " .. op .. " a", "=E"), t) + elseif ev == "__index" then + go("ERR" .. ev .. nm, function(tt) return tt.zz end, t) + elseif ev == "__newindex" then + go("ERR" .. ev .. nm, function(tt) tt.zz = 1 end, t) + elseif ev == "__call" then + go("ERR" .. ev .. nm, function(tt) return tt() end, t) + else + local op = ({ + __add = "+", __sub = "-", __mul = "*", __div = "/", + __idiv = "//", __mod = "%", __pow = "^", + __band = "&", __bor = "|", __bxor = "~", + __shl = "<<", __shr = ">>", __concat = "..", + __eq = "==", __lt = "<", __le = "<=", + })[ev] + -- __eq 只在同类型的表之间才试,所以给 b 也挂上同一份元表 + local b = setmetatable({}, getmetatable(t)) + go("ERR" .. ev .. nm, load("local a, b = ... return a " .. op .. " b", "=E"), t, b) + end + end + end +end + +print("END") + """); + } + + /** 第四片:__pairs / __close / raw* / get-setmetatable 边界 */ + @Test + void pairCloseRawMetatableEdgesMatchReference() { + Diff.assertMatches(""" +-- 元方法矩阵 · 第四片:__pairs / __close / raw* / get-setmetatable 边界 +-- +-- 前两片扫的是运算符与索引,这一片补剩下的「协议类」元方法: +-- __pairs(迭代协议)、__close(作用域退出协议)、__metatable(元表保护)、 +-- __name(错误消息里的类型名),以及四个 raw* 的越界行为。 +-- +-- 规矩照旧:一个求值一层 pcall(第 38 条);地址归一(第 42 条); +-- 整数浮点分开(第 11 条);绝不打印字符串键的 pairs 顺序(第 43 条)。 + +local function show(v) + local t = type(v) + if t == "string" then return "s[" .. v .. "]" end + if t == "number" then + if math.type(v) == "integer" then return "i" .. tostring(v) end + return "f" .. tostring(v) + end + return t +end + +-- 每次求值都在 pcall 里:未捕获的抛错让脚本当场死掉, +-- 后面几百行一条也不会比,而用例照样绿。 +local function go(tag, f, ...) + local r = table.pack(pcall(f, ...)) + local out = { tag, tostring(r[1]) } + for i = 2, r.n do out[#out + 1] = show(r[i]) end + print(table.concat(out, " ")) +end + +print("BEGIN") + +--[[ + ============ 1) __pairs ============ + luaB_pairs 的逻辑是:先查 __pairs,有就整个替代迭代协议, + 返回三个值;没有才退化成 (next, t, nil)。 + 关键在于「有 __pairs 但返回的不是函数」时,报错发生在 + generic for 的第一次调用,而不是 pairs 里。 +]] +do + local FORMS = { + { "absent", nil }, + { "func3", function(t) return function(s, c) + c = (c or 0) + 1 + if c > 3 then return nil end + return c, c * c + end, "STATE", nil end }, + { "func2", function(t) return next, { a = 1, b = 2 } end }, + { "func1", function(t) return next end }, + { "func0", function(t) return end }, + { "num", 42 }, + { "str", "s" }, + { "tbl", {} }, + { "bool", true }, + } + for i = 1, #FORMS do + local nm, v = FORMS[i][1], FORMS[i][2] + local t = setmetatable({}, (nm == "absent") and nil or { __pairs = v }) + -- pairs() 返回什么 + go("PAIRS" .. nm, function(tt) + local a, b, c = pairs(tt) + return type(a), type(b), tostring(c) + end, t) + -- 真跑一轮 generic for + go("PAIRSFOR" .. nm, function(tt) + local acc = {} + for k, val in pairs(tt) do acc[#acc + 1] = tostring(k) .. "=" .. tostring(val) end + table.sort(acc) + return table.concat(acc, ",") + end, t) + end + -- 普通表上的 pairs,确认退化路径 + go("PAIRSPLAIN", function() + local acc = {} + for k, val in pairs({ a = 1, b = 2, c = 3 }) do acc[#acc + 1] = tostring(k) end + table.sort(acc) + return table.concat(acc, ",") + end) +end + +--[[ + ============ 2) raw* 的越界行为 ============ + 这四个是「绕过元方法」的后门,所以它们的参数校验才是重点: + 给错类型要报错,而且报错的位置前缀来自 libError(第 8 条)。 +]] +do + local TN = { "nil", "false", "true", "int", "flt", "str", "tbl", "fn" } + local function mkv(t) + if t == "nil" then return nil end + if t == "false" then return false end + if t == "true" then return true end + if t == "int" then return 7 end + if t == "flt" then return 2.5 end + if t == "str" then return "ab" end + if t == "tbl" then return {} end + return print + end + for _, t in ipairs(TN) do + local v = mkv(t) + go("RAWLEN" .. t, function(x) return rawlen(x) end, v) + go("RAWGET" .. t, function(x) return rawget(x, "k") end, v) + end + -- rawequal:8 × 8 全比 + for ai = 1, #TN do + for bi = 1, #TN do + go("RAWEQ", rawequal, mkv(TN[ai]), mkv(TN[bi])) + end + end + -- rawset 的非法键 + -- 这里不能用 ipairs:第一个元素就是 nil,ipairs 会当场截断, + -- NaN 那条一次也跑不到(第 47 条)。按下标手动取。 + local RAWK = { [1] = "nil", [2] = "nan", [3] = "tbl", [4] = "bool" } + local RAWV = { [1] = nil, [2] = 0 / 0, [3] = {}, [4] = true } + for i = 1, 4 do + go("RAWSET", function(t, kk) rawset(t, kk, 1) return rawlen(t) end, {}, RAWV[i]) + print("RAWSETK", RAWK[i]) + end + go("RAWSETN", function() rawset({}, 1, 1) return "ok" end) + go("RAWSETSTR", function() rawset({}, "a", 1) return "ok" end) + go("RAWSETFLT", function() rawset({}, 2.0, 1) return "ok" end) +end + +--[[ + ============ 3) getmetatable / setmetatable 边界 ============ + __metatable 保护:getmetatable 返回它,setmetatable 直接报错。 + 注意 setmetatable 的第 1 个参数被 luaL_checktype 限成表, + 给别的值设元表只能走 debug.setmetatable(改的是全局类型元表)。 +]] +do + local TN = { "nil", "false", "true", "int", "flt", "str", "tbl", "fn" } + local function mkv(t) + if t == "nil" then return nil end + if t == "false" then return false end + if t == "true" then return true end + if t == "int" then return 7 end + if t == "flt" then return 2.5 end + if t == "str" then return "ab" end + if t == "tbl" then return {} end + return print + end + for _, t in ipairs(TN) do + go("GETMT" .. t, function(x) return getmetatable(x) end, mkv(t)) + end + + -- __metatable 保护 + local prot = setmetatable({}, { __metatable = "LOCKED" }) + go("PROT_GET", function(t) return getmetatable(t) end, prot) + go("PROT_SET", function(t) return setmetatable(t, {}) end, prot) + go("PROT_SETNIL", function(t) return setmetatable(t, nil) end, prot) + + -- setmetatable 的参数校验 + go("SETMT_NUM", function() return setmetatable(1, {}) end) + go("SETMT_NIL", function() return setmetatable(nil, {}) end) + go("SETMT_NOTBL", function() return setmetatable({}, 42) end) + go("SETMT_STR", function() return setmetatable({}, "s") end) + go("SETMT_NILMT", function() + local t = setmetatable({}, { __index = { x = 1 } }) + setmetatable(t, nil) + return t.x + end) + go("SETMT_REPLACE", function() + local t = setmetatable({}, { __index = { x = 1 } }) + setmetatable(t, { __index = { x = 2 } }) + return t.x + end) + + -- 元表自己指向自己 + go("SETMT_SELF", function() + local t = {} + setmetatable(t, t) + return t.nope + end) +end + +--[[ + ============ 4) __close ============ + 三条要分开盯: + <1> 正常退出作用域,err 参数是 nil + <2> 出错解绕,err 是那个错误对象 + <3> __close 自己抛错——这个新错要顶掉原来的错 + 以及非可调用的 __close(报 attempt to call a X value), + 还有 __close 里 yield。 +]] +do + local log = {} + local function rec(s) log[#log + 1] = s end + + -- <1> 正常退出 + do + local closed = false + do + local x = setmetatable({}, { __close = function(self, err) + closed = true; rec("c1:" .. tostring(err)) + end }) + rec("body") + end + rec("closed=" .. tostring(closed)) + end + + -- <2> 出错解绕 + do + local ok + local function f() + local x = setmetatable({}, { __close = function(self, err) + rec("c2:" .. tostring(err)) + end }) + error("boom2") + end + go("CLOSE_ERR", f) + end + + -- <3> __close 自己抛错 + do + local function f() + local x = setmetatable({}, { __close = function(self, err) error("in-close") end }) + error("orig") + end + go("CLOSE_THROW", f) + end + + -- 非可调用的 __close + do + local function f() + local x = setmetatable({}, { __close = 42 }) + end + go("CLOSE_NUM", f) + end + do + local function f() + local x = setmetatable({}, { __close = "str" }) + end + go("CLOSE_STR", f) + end + do + local function f() + local x = setmetatable({}, { __close = {} }) + end + go("CLOSE_TBL", f) + end + + -- 多个 tbc:按逆序关 + do + local function f() + local a = setmetatable({}, { __close = function() rec("a") end }) + local b = setmetatable({}, { __close = function() rec("b") end }) + error("multi") + end + go("CLOSE_MULTI", f) + end + + -- __close 里 yield + do + local co = coroutine.create(function() + local x = setmetatable({}, { __close = function() return coroutine.yield("y-close") end }) + return "body-done" + end) + for i = 1, 3 do + local r = table.pack(coroutine.resume(co, "R" .. i)) + local out = { "CLOSE_YIELD", "r" .. i } + for j = 1, r.n do out[#out + 1] = show(r[j]) end + print(table.concat(out, " ")) + end + end + + --[[ + 的重复赋值是编译期错误,不是运行时错误, + 所以不能直接写在脚本里——那样整份脚本都加载不了。 + 必须用 load 现造,让错误发生在被保护的调用里。 + ]] + do + local f, e = load("local c = 1 c = 2 return c", "=C") + print("CONST_LOAD", type(f), (type(e) == "string") and (e:gsub("0x%x+", "PTR")) or tostring(e)) + if f then go("CONST", f) end + end + -- 的元方法在声明时就要检查,同样是编译期的事 + do + local f, e = load("local x = 5", "=C2") + print("CLOSE_LOAD", type(f), (type(e) == "string") and (e:gsub("0x%x+", "PTR")) or tostring(e)) + if f then go("CLOSE_DECL", f) end + end + + -- 打印收集到的顺序 + print("LOG", table.concat(log, "|")) +end + +--[[ + ============ 5) __name ============ + 第 65 条:只有表与完整 userdata 认 __name,其余类型连类型元表都不看。 + 错误消息里的名字来自 __name,所以这里直接压「报错」这条路。 +]] +do + local named = setmetatable({}, { __name = "MyType" }) + local namedstr = setmetatable({}, { __name = "StrType" }) + local namednum = setmetatable({}, { __name = 42 }) + + go("NAME_ADD", function(t) return t + 1 end, named) + go("NAME_INDEX", function(t) return t.zz end, named) + go("NAME_CALL", function(t) return t() end, named) + go("NAME_LEN", function(t) return #t end, named) + go("NAME_CONCAT", function(t) return t .. "x" end, named) + go("NAME_CMP", function(t) return t < 1 end, named) + go("NAME_STR", function(t) return t .. "x" end, namedstr) + go("NAME_NUM", function(t) return t + 1 end, namednum) + + -- userdata 上的 __name + local ud = io.stdout + local old = debug.getmetatable(ud) + debug.setmetatable(ud, { __name = "UDType" }) + go("NAME_UD", function(u) return u + 1 end, ud) + debug.setmetatable(ud, old) + + --[[ + 其余类型:__name 应当被忽略。 + 第 65 条:只有表与完整 userdata 认它——其余类型连类型元表都不看, + 所以错误消息里出现的还是真实类型名(string / number / ...), + 不是 __name 给的那个。 + ]] + --[[ + 改全局类型元表必须存下原值再还回去,不能还成 nil—— + 字符串那份是 string 库自己装上去的({@code mt[LUA_TSTRING]}), + 抹掉之后后面任何 {@code ("x"):len()} 都会炸。 + ]] + local function withmt(v, mt) + if type(v) == "table" then + setmetatable(v, mt) + return function() setmetatable(v, nil) end + end + local old = debug.getmetatable(v) + debug.setmetatable(v, mt) + return function() debug.setmetatable(v, old) end + end + + local IGN = { + { "number", 1 }, + { "boolean", true }, + { "string", "s" }, + { "function", print }, + } + for _, e in ipairs(IGN) do + local nm, v = e[1], e[2] + local undo = withmt(v, { __name = "IGNORED" }) + go("NAME_IGN_" .. nm, function(x) return x + 1 end, v) + undo() + end +end + +print("END") + """); + } + + private MetamethodTest() { + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/OpCodeTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/OpCodeTest.java new file mode 100644 index 0000000..d0689af --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/OpCodeTest.java @@ -0,0 +1,191 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * 指令编解码测试。 + *

+ * 操作码的顺序、名字与 {@code luaP_opmodes} 属性表不是靠肉眼对齐的: + * 用脚本直接从 {@code lua-5.4.7/src/lopcodes.h} 与 {@code lopcodes.c} 抽出来 + * 逐项比对过(83 项全等)。这里锁的是编解码本身——尤其是有符号字段与 + * 「指令字是负数」这两种最容易在本实现里出错的情形:C 的 {@code Instruction} + * 是无符号 32 位,Java 的 {@code int} 不是。 + */ +class OpCodeTest { + + @Test + void fieldLayoutMatchesUpstream() { + assertEquals(7, OpCode.SIZE_OP); + assertEquals(8, OpCode.SIZE_A); + assertEquals(8, OpCode.SIZE_B); + assertEquals(8, OpCode.SIZE_C); + assertEquals(17, OpCode.SIZE_BX); + assertEquals(25, OpCode.SIZE_AX); + assertEquals(25, OpCode.SIZE_SJ); + assertEquals(0, OpCode.POS_OP); + assertEquals(7, OpCode.POS_A); + assertEquals(15, OpCode.POS_K); + assertEquals(16, OpCode.POS_B); + assertEquals(24, OpCode.POS_C); + assertEquals(131071, OpCode.MAXARG_BX); + assertEquals(65535, OpCode.OFFSET_SBX); + assertEquals(33554431, OpCode.MAXARG_AX); + assertEquals(16777215, OpCode.OFFSET_SJ); + assertEquals(127, OpCode.OFFSET_SC); + assertEquals(255, OpCode.NO_REG); + assertEquals(50, OpCode.LFIELDS_PER_FLUSH); + } + + @Test + void abckRoundTrips() { + int i = OpCode.createABCk(OpCode.OP_SETTABLE, 200, 100, 50, 1); + assertEquals(OpCode.OP_SETTABLE, OpCode.getOpCode(i)); + assertEquals(200, OpCode.getArgA(i)); + assertEquals(100, OpCode.getArgB(i)); + assertEquals(50, OpCode.getArgC(i)); + assertEquals(1, OpCode.getArgK(i)); + + int j = OpCode.createABCk(OpCode.OP_ADD, 0, 1, 2, 0); + assertEquals(0, OpCode.getArgK(j)); + assertEquals(OpCode.IABC, OpCode.getOpMode(OpCode.getOpCode(j))); + } + + @Test + void abxRoundTrips() { + int i = OpCode.createABx(OpCode.OP_LOADK, 3, OpCode.MAXARG_BX); + assertEquals(OpCode.OP_LOADK, OpCode.getOpCode(i)); + assertEquals(3, OpCode.getArgA(i)); + assertEquals(OpCode.MAXARG_BX, OpCode.getArgBx(i)); + // Bx 取满时指令字最高位是 1(Java int 为负),取字段仍然必须正确 + assertTrue(i < 0, "Bx 取满时指令字在 Java 里是负数"); + assertEquals(OpCode.OP_LOADK, OpCode.getOpCode(i)); + assertEquals(3, OpCode.getArgA(i)); + } + + @Test + void signedBxUsesOffset() { + int i = OpCode.createABx(OpCode.OP_LOADI, 0, 0); + assertEquals(-OpCode.OFFSET_SBX, OpCode.getArgSBx(i), "0 表示最小的 sBx"); + i = OpCode.setArgSBx(i, 0); + assertEquals(0, OpCode.getArgSBx(i)); + assertEquals(OpCode.OFFSET_SBX, OpCode.getArgBx(i)); + i = OpCode.setArgSBx(i, -5); + assertEquals(-5, OpCode.getArgSBx(i)); + i = OpCode.setArgSBx(i, OpCode.OFFSET_SBX); + assertEquals(OpCode.OFFSET_SBX, OpCode.getArgSBx(i)); + } + + @Test + void signedCUsesOffset() { + assertEquals(127, OpCode.int2sC(0)); + assertEquals(0, OpCode.sC2int(127)); + assertEquals(255, OpCode.int2sC(128)); + assertEquals(128, OpCode.sC2int(255)); + int i = OpCode.createABCk(OpCode.OP_ADDI, 1, 2, OpCode.int2sC(-3), 0); + assertEquals(-3, OpCode.getArgSC(i)); + } + + @Test + void axAndSjRoundTrip() { + int i = OpCode.createAx(OpCode.OP_EXTRAARG, OpCode.MAXARG_AX); + assertTrue(i < 0); + assertEquals(OpCode.OP_EXTRAARG, OpCode.getOpCode(i)); + assertEquals(OpCode.MAXARG_AX, OpCode.getArgAx(i)); + assertEquals(OpCode.IAX, OpCode.getOpMode(OpCode.OP_EXTRAARG)); + + int j = OpCode.createsJ(OpCode.OP_JMP, 0, 0); + assertEquals(-OpCode.OFFSET_SJ, OpCode.getArgSJ(j)); + j = OpCode.setArgSJ(j, 0); + assertEquals(0, OpCode.getArgSJ(j)); + j = OpCode.setArgSJ(j, -1); + assertEquals(-1, OpCode.getArgSJ(j)); + assertEquals(OpCode.ISJ, OpCode.getOpMode(OpCode.OP_JMP)); + } + + @Test + void settingOneFieldDoesNotDisturbOthers() { + int i = OpCode.createABCk(OpCode.OP_GETTABLE, 1, 2, 3, 0); + i = OpCode.setArgA(i, 250); + assertEquals(2, OpCode.getArgB(i)); + assertEquals(3, OpCode.getArgC(i)); + assertEquals(0, OpCode.getArgK(i)); + assertEquals(OpCode.OP_GETTABLE, OpCode.getOpCode(i)); + + i = OpCode.setArgB(i, 254); + assertEquals(250, OpCode.getArgA(i)); + assertEquals(3, OpCode.getArgC(i)); + + i = OpCode.setArgC(i, 253); + assertEquals(254, OpCode.getArgB(i)); + + i = OpCode.setArgK(i, 1); + assertEquals(253, OpCode.getArgC(i)); + assertEquals(250, OpCode.getArgA(i)); + + i = OpCode.setOpCode(i, OpCode.OP_GETFIELD); + assertEquals(1, OpCode.getArgK(i), "改操作码不能动 k"); + assertEquals(253, OpCode.getArgC(i)); + } + + @Test + void opModesMatchUpstream() { + assertEquals(OpCode.IABC, OpCode.getOpMode(OpCode.OP_MOVE)); + assertEquals(OpCode.IASBX, OpCode.getOpMode(OpCode.OP_LOADI)); + assertEquals(OpCode.IASBX, OpCode.getOpMode(OpCode.OP_LOADF)); + assertEquals(OpCode.IABX, OpCode.getOpMode(OpCode.OP_LOADK)); + assertEquals(OpCode.ISJ, OpCode.getOpMode(OpCode.OP_JMP)); + assertEquals(OpCode.IAX, OpCode.getOpMode(OpCode.OP_EXTRAARG)); + + // 写 A 的指令 vs 不写的 + assertTrue(OpCode.testAMode(OpCode.OP_MOVE)); + assertFalse(OpCode.testAMode(OpCode.OP_SETUPVAL), "SETUPVAL 不写寄存器 A"); + assertFalse(OpCode.testAMode(OpCode.OP_SETTABLE)); + assertFalse(OpCode.testAMode(OpCode.OP_JMP)); + + // 测试指令:下一条必须是跳转 + assertTrue(OpCode.testTMode(OpCode.OP_EQ)); + assertTrue(OpCode.testTMode(OpCode.OP_TEST)); + assertTrue(OpCode.testTMode(OpCode.OP_TESTSET)); + assertTrue(OpCode.testTMode(OpCode.OP_LEI)); + assertFalse(OpCode.testTMode(OpCode.OP_ADD)); + + // 元方法指令 + assertTrue(OpCode.testMMMode(OpCode.OP_MMBIN)); + assertTrue(OpCode.testMMMode(OpCode.OP_MMBINI)); + assertTrue(OpCode.testMMMode(OpCode.OP_MMBINK)); + assertFalse(OpCode.testMMMode(OpCode.OP_ADD)); + + // 只有 CALL 同时 OT 与 IT + assertTrue(OpCode.testOTMode(OpCode.OP_CALL)); + assertTrue(OpCode.testITMode(OpCode.OP_CALL)); + assertFalse(OpCode.testOTMode(OpCode.OP_RETURN)); + assertTrue(OpCode.testITMode(OpCode.OP_RETURN)); + } + + @Test + void topFlagsFollowCAndB() { + // isOT:OT 类指令且 C == 0;TAILCALL 恒为 OT + assertTrue(OpCode.isOT(OpCode.createABCk(OpCode.OP_CALL, 1, 2, 0, 0))); + assertFalse(OpCode.isOT(OpCode.createABCk(OpCode.OP_CALL, 1, 2, 3, 0))); + assertTrue(OpCode.isOT(OpCode.createABCk(OpCode.OP_TAILCALL, 1, 2, 3, 0))); + + // isIT:IT 类指令且 B == 0 + assertTrue(OpCode.isIT(OpCode.createABCk(OpCode.OP_CALL, 1, 0, 3, 0))); + assertFalse(OpCode.isIT(OpCode.createABCk(OpCode.OP_CALL, 1, 2, 3, 0))); + assertTrue(OpCode.isIT(OpCode.createABCk(OpCode.OP_SETLIST, 1, 0, 3, 0))); + } + + @Test + void opcodeCountAndNames() { + assertEquals(83, OpCode.NUM_OPCODES); + assertEquals(OpCode.OP_EXTRAARG + 1, OpCode.NUM_OPCODES); + assertEquals("MOVE", OpCode.name(OpCode.OP_MOVE)); + assertEquals("EXTRAARG", OpCode.name(OpCode.OP_EXTRAARG)); + assertEquals("LFALSESKIP", OpCode.name(OpCode.OP_LFALSESKIP)); + assertEquals("VARARGPREP", OpCode.name(OpCode.OP_VARARGPREP)); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/OsLibTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/OsLibTest.java new file mode 100644 index 0000000..0f97bfd --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/OsLibTest.java @@ -0,0 +1,818 @@ +package com.github.esrrhs.fakescript.lua; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; + +/** + * os 库的差分用例 —— 阶段 7。 + *

+ * os 库天生平台相关,所以选例的原则是「只比对平台无关的那部分」: + * 所有 {@code os.date} / {@code os.time} 用例都用固定的时间戳, + * 且尽量走 {@code !}(UTC);用到本地时区的地方,参考实现与本实现跑在同一台 + * 机器上、同一个 {@code TZ},所以结果也必须一致。 + *

+ * 几处专门盯着写: + *

    + *
  • {@code strftime} 的每个转换符逐个比对,包括 {@code E} / {@code O} + * 修饰、{@code %U} / {@code %W} / {@code %V} 三种「第几周」的差别、 + * {@code %e} 的空格补位、{@code %I} 的 0 点算 12。
  • + *
  • {@code os.time} 的归一化:{@code month=13, day=32, hour=25} 会被 + * 正规化,而且结果回写进传入的表({@code wday} / {@code yday} / + * {@code isdst} 会被填出来)。
  • + *
  • {@code luaL_fileresult} 的三元组:{@code os.remove} 失败时给 + * {@code nil, "<路径>: No such file or directory", 2},成功时只给 + * {@code true}(不是三元组)。
  • + *
  • {@code os.execute} 成功也返回三个值:{@code true, "exit", 0}。
  • + *
+ *

+ * 不比对的:{@code os.clock} 的数值(只比类型)、{@code os.tmpname} 的路径、 + * 无参 {@code os.time} / {@code os.date} 的当前时刻。 + */ +class OsLibTest { + + @TestFactory + List osScriptsMatchReference() { + List tests = new ArrayList<>(); + add(tests, "fmt", FORMAT); + add(tests, "time", TIME); + add(tests, "file", FILES); + add(tests, "err", ERRORS); + add(tests, "errno", ERRNO); + return tests; + } + + private static void add(List tests, String tag, String[] srcs) { + for (int i = 0; i < srcs.length; i++) { + String src = srcs[i]; + int n = i; + tests.add(DynamicTest.dynamicTest(tag + "[" + n + "]", () -> Diff.assertMatches(src))); + } + } + + private static final String[] FORMAT = { + + """ + local t = 1700000000 -- 2023-11-14 22:13:20 UTC + local one = {"a","A","b","B","c","C","d","D","e","F","g","G","h","H","I","j"} + local r = {} + for _,o in ipairs(one) do r[#r+1] = o.."=["..os.date("!%"..o, t).."]" end + print(table.concat(r, " ")) + local two = {"m","M","n","p","r","R","S","t","T","u","U","V","w","W","x","X"} + r = {} + for _,o in ipairs(two) do r[#r+1] = o.."=["..os.date("!%"..o, t).."]" end + print(table.concat(r, " ")) + -- %z 与 %Z 都不比:%z 带 '!' 时 macOS 的 strftime 给本地标准偏移而 + -- 不是 +0000(实测 TZ=America/New_York 时 !%z 给 -0500、%z 给 + -- -0400);%Z 是「时区名」,glibc 给 GMT、Apple libc 给 UTC。 + -- 两个都是平台自己的答案,都不是错(约定 #53)。 + local three = {"y","Y","%"} + r = {} + for _,o in ipairs(three) do r[#r+1] = o.."=["..os.date("!%"..o, t).."]" end + print(table.concat(r, " ")) + """, + + """ + local t = 1700000000 + local eo = {"Ec","EC","Ex","EX","Ey","EY","Od","Oe","OH","OI","Om","OM","OS"} + local r = {} + for _,o in ipairs(eo) do r[#r+1] = o.."=["..os.date("!%"..o, t).."]" end + print(table.concat(r, " ")) + local eo2 = {"Ou","OU","OV","Ow","OW","Oy"} + r = {} + for _,o in ipairs(eo2) do r[#r+1] = o.."=["..os.date("!%"..o, t).."]" end + print(table.concat(r, " ")) + """, + + """ + print(os.date("!%c", 0)) + print(os.date("!%F %T", 0)) + print(os.date("!%F %T", 86399)) + print(os.date("!%F %T", 951782400)) + print(os.date("!%F %T", -1)) + print(os.date("!%F %T", -86400)) + print(os.date("!%e", os.time{year=2023, month=1, day=5})) + print(os.date("!%j", os.time{year=2023, month=1, day=1})) + print(os.date("!%j", os.time{year=2020, month=12, day=31})) + """, + + """ + -- %I 在 0 点与 12 点的表现,以及 %p 的翻转点 + local r = {} + for h = 0, 23, 3 do + local t = os.time{year=2023, month=6, day=15, hour=h, min=5, sec=9} + r[#r+1] = os.date("!%H=%I%p", t) + end + print(table.concat(r, " ")) + print(os.date("!%I %p", os.time{year=2023, month=6, day=15, hour=12})) + """, + + """ + -- U / W / V 三种周编号在跨年处各不相同 + local days = {{2023,1,1},{2021,1,1},{2016,1,3},{2019,12,30},{2020,12,31},{2024,2,29}} + for _, d in ipairs(days) do + local t = os.time{year=d[1], month=d[2], day=d[3], hour=12} + print(d[1].."-"..d[2].."-"..d[3], os.date("!%U %W %V %G %g", t)) + end + """, + + """ + print(os.date("!*t", 0).year, os.date("!*t", 0).month, os.date("!*t", 0).day) + print(os.date("!*t", 0).wday, os.date("!*t", 0).yday, os.date("!*t", 0).isdst) + local d = os.date("!*t", 1700000000) + print(d.year, d.month, d.day, d.hour, d.min, d.sec, d.wday, d.yday, d.isdst) + local n = 0 + for k in pairs(d) do n = n + 1 end + print(n) + """, + + """ + -- 本地时区:本实现与参考实现跑在同一台机器上,结果必须一致 + local t = 1700000000 + print(os.date("%Y-%m-%d %H:%M:%S", t)) + -- 只比 %z:%Z 是「时区名」,glibc 给 GMT、Apple libc 给 UTC, + -- 两边都不是错。见 MEMORY 约定 #53。 + print(os.date("%z", t)) + local d = os.date("*t", t) + print(d.year, d.month, d.day, d.hour, d.min, d.sec, d.wday, d.yday, d.isdst) + """, + + """ + -- 数字会被当成格式串;非转换字符原样输出 + print(os.date(42, 0)) + print(os.date("abc", 0)) + print(os.date("", 0)) + print(os.date("100%%", 0)) + print(os.date("!%Y-%m-%dT%H:%M:%S", 1700000000)) + """, + }; + + private static final String[] TIME = { + + """ + local tt = {year=2023, month=11, day=14, hour=22, min=13, sec=20} + local e = os.time(tt) + print(e, math.type(e)) + print(tt.year, tt.month, tt.day, tt.hour, tt.min, tt.sec, tt.wday, tt.yday, tt.isdst) + """, + + """ + -- 归一化:13 月 32 日 25 点 61 分 61 秒 + local t = {year=2023, month=13, day=32, hour=25, min=61, sec=61} + print(os.time(t)) + print(t.year, t.month, t.day, t.hour, t.min, t.sec, t.wday, t.yday, t.isdst) + """, + + """ + local t = {year=2020, month=2, day=29, hour=12} + print(os.time(t), t.month, t.day, t.yday) + local u = {year=2023, month=2, day=28, hour=12} + print(os.time(u), u.yday) + local v = {year=2024, month=12, day=31, hour=23, min=59, sec=59} + print(os.time(v), v.yday) + """, + + """ + -- hour/min/sec 有默认值:12:00:00 + print(os.time{year=2023, month=11, day=14} == os.time{year=2023, month=11, day=14, hour=12}) + print(os.time{year=2023, month=11, day=14, hour=0}) + print(math.type(os.time{year=1970, month=1, day=1, hour=0, min=0, sec=0})) + """, + + """ + print(os.difftime(100, 50), math.type(os.difftime(100, 50))) + print(os.difftime(50, 100)) + print(os.difftime(0, 0)) + print(math.type(os.time())) + print(os.time() > 1600000000) + print(math.type(os.clock())) + print(os.clock() >= 0) + """, + + """ + -- 本时区没有夏令时,isdst=false 与自动判定应当一致 + local a = os.time{year=2023, month=11, day=14, hour=22, min=13, sec=20} + local b = os.time{year=2023, month=11, day=14, hour=22, min=13, sec=20, isdst=false} + print(a == b) + """, + }; + + private static final String[] FILES = { + + """ + print(os.execute()) + print(os.execute("true")) + print(os.execute("false")) + print(os.execute("exit 7")) + """, + + """ + print(os.getenv("PATH") ~= nil) + print(os.getenv("__NO_SUCH_ENV_VAR_XYZ__")) + print(pcall(os.getenv)) + print(pcall(os.getenv, 42)) + """, + + """ + -- tmpname 建出来的文件是存在的;路径本身含随机部分,只比目录前缀 + local n = os.tmpname() + print(n:sub(1, 5)) + print(os.remove(n)) + print(select(3, os.remove(n))) -- 只比 errno,消息里含随机路径 + """, + + """ + local a = os.tmpname() + local b = a .. ".moved" + print(os.rename(a, b)) + print(os.remove(b)) + print(os.rename(a, b)) -- 源已经不在了 + """, + + """ + local n = os.tmpname() + print(type(n), #n > 0) + """, + + """ + -- 前三个(查/设 "C" 与 "POSIX")不比:返回的名字由 libc 决定, + -- glibc 说 "C"、Apple libc 说 "POSIX",两边都合法。 + print(os.setlocale("xx_XX")) + print(os.setlocale(nil, "time")) + print(os.setlocale(nil, "all")) + """, + }; + + private static final String[] ERRORS = { + + """ + print(pcall(os.date, "!%Q", 0)) + print(pcall(os.date, "%", 0)) + print(pcall(os.date, "%QQ", 0)) + print(pcall(os.date, "!%Z%Q", 0)) + print(pcall(os.date, 42, 2^62)) + """, + + """ + print(pcall(os.time, "x")) + print(pcall(os.time, 42)) + print(pcall(os.time, {})) + print(pcall(os.time, {year="x"})) + print(pcall(os.time, {year=2023, month="y"})) + print(pcall(os.time, {year=2023, month=1})) + print(pcall(os.time, {year=2023, month=1, day=1, hour=1.5})) + print(pcall(os.time, {year=2^62, month=1, day=1})) + """, + + """ + print(pcall(os.difftime, 1)) + print(pcall(os.rename, "/tmp/__fk_only_one__")) + print(pcall(os.remove)) + print(pcall(os.setlocale, nil, "bogus")) + print(pcall(os.execute, 42)) + """, + + """ + -- 元方法也要生效:字段读走 __index、回写走 __newindex + local store = {year=2023, month=11, day=14, hour=22, min=13, sec=20} + local written = {} + local t = setmetatable({}, { + __index = function(_, k) return store[k] end, + __newindex = function(_, k, v) written[k] = v end, + }) + print(os.time(t)) + local keys = {} + for k in pairs(written) do keys[#keys+1] = k end + table.sort(keys) + print(table.concat(keys, ",")) + print(written.year, written.month, written.wday ~= nil, written.yday ~= nil) + """, + }; + + /** + * {@code os.remove} / {@code os.rename} 的 errno 形状表(22 行)。 + * 也是照着 {@code LuaAux} 的未覆盖行点名打的 —— 那些 + * {@code errnoOf} / {@code strError} 里 {@code ENOTEMPTY} / {@code EACCES} + * 之类的分支从来没走到过。 + *

+ * 咬出五处: + *

    + *
  • {@code ENOTEMPTY} 的数值是操作系统定的:Linux 39、macOS/BSD 66。 + * 这里写死 39,于是 {@code os.remove(非空目录)} 消息对了、数字差 27。 + * 改成按 {@code os.name} 取值({@code LuaAux.ENOTEMPTY})。
  • + *
  • {@code strError} 不认 66:只补了数字的话会「数字 66 对、 + * 消息却是 Input/output error」—— 数值与文本得一起改。
  • + *
  • {@code os.remove("")} :C 给 {@code ENOENT}(2),我们给 + * {@code EIO}(5)。与 {@code loadfile("")} 同一类「C 与 Java 判路径 + * 不一致」的角落。
  • + *
  • {@code os.rename} 的形状表整个没对:文件 → 已存在的目录是 + * {@code EISDIR}(21)、目录 → 已存在的文件是 {@code ENOTDIR}(20)、 + * 目录 → 空目录是成功(替换)、文件 → 已存在的文件也是 + * 替换。之前一律走不带 {@code REPLACE_EXISTING} 的 {@code Files.move}, + * 于是「目标已存在」全报 {@code EEXIST}。
  • + *
+ * 一处不比:{@code os.remove("/etc")} 在本机是 {@code EPERM}(1),取决于 + * SIP 与这台机器的权限,不是 Lua 的事实。改用 {@code chmod 000} 的目录来走 + * {@code EACCES}(13) 那条路。 + */ + private static final String[] ERRNO = {""" + -- os.remove / os.rename 的 errno 分支:ENOTEMPTY / EACCES / EEXIST / ENOENT + local out = {} + local function d(v) + local t = type(v) + if t == "nil" then return "nil" end + if t == "boolean" then return tostring(v) end + if t == "number" then return math.type(v) .. ":" .. tostring(v) end + if t == "string" then return string.format("q(%d)[%s]", #v, (v:gsub("\\n", "\\\\n"))) end + return t + end + local function probe(name, f, ...) + local r = table.pack(pcall(f, ...)) + local s = name .. " [" .. r.n .. "]" + for i = 2, r.n do s = s .. " " .. d(r[i]) end + if not r[1] then s = s .. " ERR " .. d(r[2]) end + out[#out + 1] = s + end + -- 类型冲突的 rename(文件 -> 目录、目录 -> 文件)只比「失败了」。 + -- errno 是内核定的,不由 POSIX 统一:macOS 给 + -- "Is a directory" 21 / "Not a directory" 20,Linux 的 + -- os.rename(文件, 非空目录) 给 "Directory not empty" 39。两边都是各自 + -- 内核的答案(约定 #98),连消息带 errno 一起归掉 —— 但仍然比「它 + -- 失败了、给了三元组」,这一条是跨平台的。 + local function probe_xdir(name, f, ...) + local r = table.pack(pcall(f, ...)) + out[#out + 1] = name .. " ok=" .. tostring(r[1]) .. " [" .. r.n .. "] " + .. d(r[2]) .. " " .. type(r[3]) .. " " .. type(r[4]) + end + + local D = "/tmp/fkerm" + os.execute("rm -rf " .. D .. " && mkdir -p " .. D) + local function put(p, body) + local f = assert(io.open(p, "w")); f:write(body or "x"); f:close() + end + + -- remove + os.execute("mkdir -p " .. D .. "/empty " .. D .. "/full && touch " .. D .. "/full/inner") + probe("rm_missing", os.remove, D .. "/nope_xyz") + probe("rm_file", function() put(D .. "/f1"); return os.remove(D .. "/f1") end) + probe("rm_emptydir", os.remove, D .. "/empty") + probe("rm_fulldir", os.remove, D .. "/full") -- ENOTEMPTY(39) + probe("rm_root", os.remove, "/etc/hosts") -- EACCES(13) + -- 目录权限不足(chmod 000)时删它里面的文件:EACCES(13)。 + -- 不用 os.remove("/etc"):那条在本机是 EPERM(1),取决于 SIP 与这台机器的权限, + -- 不是 Lua 的事实,无法建模也不该进比对。 + probe("rm_noperm", function() + os.execute("mkdir -p " .. D .. "/np && touch " .. D .. "/np/f && chmod 000 " .. D .. "/np") + local r = table.pack(os.remove(D .. "/np/f")) + os.execute("chmod 755 " .. D .. "/np") + return table.unpack(r, 1, r.n) + end) + probe("rm_noname", os.remove) + probe("rm_nil", os.remove, nil) + probe("rm_num", os.remove, 42) + probe("rm_empty", os.remove, "") + + -- rename + put(D .. "/a") + probe("rn_ok", function() return os.rename(D .. "/a", D .. "/b") end) + -- 文件 -> 已存在的文件:C 的 rename 是替换 + probe("rn_over_file", function() + put(D .. "/x1", "1") + put(D .. "/x2", "2") + local ok = os.rename(D .. "/x1", D .. "/x2") + local g = io.open(D .. "/x2") + local c = g and g:read("a") or "gone" + if g then g:close() end + local still = io.open(D .. "/x1") + if still then still:close() end + return ok, c, still ~= nil + end) + put(D .. "/c") + os.execute("mkdir -p " .. D .. "/d") + probe_xdir("rn_file_over_dir", os.rename, D .. "/c", D .. "/d") + put(D .. "/e") + os.execute("mkdir -p " .. D .. "/f/g") + probe("rn_dir_over_fulldir", os.rename, D .. "/f", D .. "/d") -- ENOTEMPTY(39) + probe("rn_missing", os.rename, D .. "/nope_xyz", D .. "/z") + probe("rn_same", function() put(D .. "/s"); return os.rename(D .. "/s", D .. "/s") end) + probe("rn_to_root", function() put(D .. "/t"); return os.rename(D .. "/t", "/etc/nope_xyz") end) + probe("rn_noargs", os.rename) + probe("rn_nil", os.rename, nil, nil) + probe("rn_num", os.rename, 42, 43) + probe("rn_empty", os.rename, "", "") + probe_xdir("rn_dir_to_file", function() + os.execute("mkdir -p " .. D .. "/dd") + put(D .. "/ff") + return os.rename(D .. "/dd", D .. "/ff") + end) + + os.execute("rm -rf " .. D) + print(table.concat(out, "\\n")) + """ }; + + /** + * {@code os.date} / {@code os.time} 的形状矩阵(337 行)。 + *

+ * 已有的 {@code FORMAT} 组只跑了 {@code !}(UTC)那一半;这里补齐另外半边: + * 本地时区的每个转换符,以及同一批转换符在多个时间点上的值 + * ({@code 0} / 2038 边界 / 1900 / {@code -1} / 公元 1 年),外加 + * 非法转换符的报错形状、{@code os.time} / {@code os.difftime} / + * {@code os.setlocale} / {@code os.getenv} 的边角。 + *

+ * 咬出一处真 bug:{@code *t} 写成了「以 {@code *t} 开头」而 C 是 + * {@code strcmp(s, "*t") == 0}。于是 {@code os.date("*t x")} 本该走 + * strftime 那条路、把 {@code *} {@code t} 空格 {@code x} 全当字面量返回 + * {@code "*t x"},本实现却返回了表。修的时候又带出第二个细节: + * {@code strcmp} 见到 {@code \0} 就停,所以 {@code os.date("*t\0x")} 在 C 里 + * 仍然是取表 —— 只比长度不算完,得先截到第一个 NUL。 + *

+ * 敏感性:把 {@code LUA_STRFTIMEOPTIONS} 里 {@code E} / {@code O} 两字符那组 + * 删掉红 38 行;把 {@code *t} 退回前缀匹配红 1 行;把 {@code *t} 的长度 + * 改成用 {@code f.length}(不截 NUL)红 2 行。 + *

+ * 不进矩阵的:{@code !%z}(macOS 的 strftime 在 gmtime 下给的是本地 + * 偏移,glibc 给 {@code +0000};本实现按 POSIX 给 {@code +0000},只能在 + * Linux 上对得上)、以及无参的 {@code os.date} / {@code os.time}(墙上时钟)。 + */ + @Test + void dateShapeMatrixMatchesReference() { + Diff.assertMatches(""" + + local T0 = 0 + local T1 = 1700000000 + local T2 = -946080000 -- 1940 前后 + local T3 = 2147483647 -- 2038 边界 + local T4 = -2208988800 -- 1900-01-01 + local T5 = -1 + + local function show(r) + if type(r) == "table" then + return string.format("%d-%d-%d %d:%d:%d yday=%d wday=%d isdst=%s", + r.year, r.month, r.day, r.hour, r.min, r.sec, r.yday, r.wday, + tostring(r.isdst)) + end + return tostring(r) + end + + local function d(tag, fmt, t) + local ok, r = pcall(os.date, fmt, t) + print(tag, ok and show(r) or ("ERR " .. tostring(r))) + end + + local function e(tag, f, ...) + local ok, r = pcall(f, ...) + print(tag, ok and show(r) or ("ERR " .. tostring(r))) + end + + -- 单字符转换符:C99 那一组(照 LUA_STRFTIMEOPTIONS 抄) + local singles = "aAbBcCdDeFgGhHIjmMnprRStTuUVwWxXyYzZ%" + for i = 1, #singles do + local c = singles:sub(i, i) + d("S_" .. c, "%" .. c, T1) + end + -- 同一批时间戳下的 UTC(! 前缀) + for i = 1, #singles do + local c = singles:sub(i, i) + -- %z / %Z 在 '!' 下两边 libc 不一致:macOS 的 strftime 用全局 timezone + -- (给本地偏移),glibc 用 tm_gmtoff(给 +0000 / GMT)。不进矩阵。 + if c ~= "z" and c ~= "Z" then + d("U_" .. c, "!%" .. c, T1) + end + end + -- 另一个时间点:1970 与 2038 边界 + for i = 1, #singles do + local c = singles:sub(i, i) + d("Z_" .. c, "%" .. c, T0) + d("Y_" .. c, "%" .. c, T3) + end + -- 1900 与负数秒:zeroPad 的负数分支 + for i = 1, #singles do + local c = singles:sub(i, i) + if c ~= "z" and c ~= "Z" then + d("N_" .. c, "!%" .. c, T4) + d("M_" .. c, "!%" .. c, T5) + end + end + + -- 两字符转换符(E / O 修饰) + local doubles = { "Ec", "EC", "Ex", "EX", "Ey", "EY", "Od", "Oe", "OH", "OI", + "Om", "OM", "OS", "Ou", "OU", "OV", "Ow", "OW", "Oy" } + for _, c in ipairs(doubles) do + d("D_" .. c, "%" .. c, T1) + d("E_" .. c, "!%" .. c, T1) + end + + -- *t 与 !*t + d("t1", "*t", T1) + d("t2", "!*t", T1) + d("t3", "*t", T0) + d("t4", "!*t", T0) + d("t5", "*t", T2) + d("t6", "!*t", T5) + d("t7", "*t x", T1) + d("t8", "%*t", T1) + + -- 非法转换符 + d("x1", "%q", T1) + d("x2", "%E", T1) + d("x3", "%O", T1) + d("x4", "%", T1) + d("x5", "abc%", T1) + d("x6", "%Ez", T1) + d("x7", "%Oq", T1) + d("x8", "%s", T1) + d("x9", "%k", T1) + d("xa", "%l", T1) + d("xb", "%%", T1) + d("xc", "", T1) + d("xd", "hello %Y world", T1) + d("xe", "!", T1) + d("xf", "!*", T1) + + -- 时间的形状 + d("v1", "%Y", "1700000000") + d("v2", "%Y", 1700000000.0) + d("v3", "%Y", 1700000000.5) + d("v4", "%Y", true) + d("v5", "%Y", {}) + d("v6", "%Y", 2^62) + d("v7", "%Y", -2^62) + d("v8", "%Y", 1e300) + d("v9", "%Y", 0/0) + e("vb", os.date, "%Y") + -- 公元 1 年那两条不比:%Y / %C 要不要补零是 libc 说了算 + -- (glibc 给 "1" / "0",Apple libc 给 "0001" / "00")。 + + -- os.time + local function tt(tag, t) + e(tag, os.time, t) + end + tt("m1", { year = 2023, month = 11, day = 14, hour = 22, min = 13, sec = 20 }) + tt("m2", { year = 2023, month = 11, day = 14 }) + tt("m3", { year = 2023, month = 11, day = 14, isdst = true }) + tt("m4", { year = 2023, month = 11, day = 14, isdst = false }) + tt("m5", { year = 1970, month = 1, day = 1, hour = 0 }) + tt("m6", { year = 1900, month = 1, day = 1 }) + tt("m7", { year = 2023, month = 13, day = 1 }) + tt("m8", { year = 2023, month = 0, day = 1 }) + tt("m9", { year = 2023, month = 1, day = 32 }) + tt("ma", { year = 2023, month = 1 }) + tt("mb", { month = 1, day = 1 }) + tt("mc", { year = 2023, month = 1, day = "1" }) + tt("md", { year = 2023, month = 1, day = 1.5 }) + tt("me", { year = 2023, month = 1, day = true }) + tt("mf", { year = 2^40, month = 1, day = 1 }) + tt("mg", {}) + tt("mh", { year = 2023, month = 1, day = 1, hour = 25 }) + e("mj", os.time, 42) + + -- os.difftime / os.clock 之外的杂项 + e("n1", os.difftime, 100, 50) + e("n2", os.difftime, 50, 100) + e("n3", os.difftime, 0, 0) + e("n4", os.difftime, "100", 50) + e("n5", os.difftime) + e("n6", os.difftime, 1) + + -- os.setlocale + e("l1", os.setlocale) + e("l2", os.setlocale, "C") + e("l3", os.setlocale, "C", "all") + e("l4", os.setlocale, "xx") + e("l5", os.setlocale, nil, "bogus") + e("l6", os.setlocale, "C", "bogus") + e("l7", os.setlocale, 42) + e("l8", os.setlocale, "C", "time") + + -- os.getenv + e("g1", os.getenv, "NO_SUCH_VAR_XYZ_12345") + e("g2", os.getenv, "PATH") + e("g3", os.getenv, 42) + e("g4", os.getenv, true) + e("g5", os.getenv) + + -- 内嵌 \\0:C 的 strcmp 见到 \\0 就停 + d("w1", "*t\\0x", T1) + d("w2", "!*t\\0x", T1) + d("w3", "%Y\\0abc", T1) + d("w4", "\\0*t", T1) + print("done") + """); + } + /** + * {@code os.time} / {@code os.date} 网格:1152 行。 + *

+ * 这两个函数被「库函数 × 错误参数」那张网格排除过——无参时读时钟。但带上 + * 显式参数就是确定性的,于是那张网格留下的一整片没人看。这里补上: + * {@code os.time} 的七个字段 × 23 种值、缺字段的默认值({@code hour} 默认 12, + * {@code year/month/day} 没有默认)、{@code setallfields} 回写归一后的字段、 + * {@code os.date} 的 60 种格式 × 15 个时间戳、{@code os.difftime}、 + * {@code os.getenv}、{@code os.setlocale}。 + *

+ * 咬出来一个真分歧:{@code os.time{year=2147483647}} 给的是垃圾。参考实现 + * 给 {@code 67767976201968000},我们给 {@code -6019000092870206}。原因是 + * {@code GregorianCalendar} 存的是毫秒,这种年份 {@code getTimeInMillis()} + * 直接溢出折回负数。改成 {@link OsLib#civilToEpoch}(前推格里历的纯 {@code long} + * 算术)之后两边一致——而 {@code 67767976201968000} 正好是 + * {@code 86400 * days_from_civil} 减时区偏移,纯算术,两边 libc 都一样。 + *

+ * 两处不进表,是量出来的不是猜的: + *

    + *
  • 1900 年以前:Apple libc 的 {@code mktime} 拒绝(实测 {@code year=1900} + * 成、{@code 1899} 不成),glibc 不拒绝。两边各自推导谁也不算对,所以 + * 探针里这一档直接打 {@code SKIP}。
  • + *
  • {@code os.time(nil)} / {@code os.time()}:走 + * {@code lua_isnoneornil},读的是时钟。
  • + *
+ *

+ * 实测灵敏度:转换符报错文案 54、{@code civilToEpoch} 的月份基准({@code tmMon} + * 是 0 起)29、{@code isdst} 恒为 0 / {@code isdst=1} 不挪一小时 24、 + * {@code setAllFields} 的字段名 22、退回 {@code Calendar}(溢出)3。 + * {@code os.date} 的 {@code MAX_SEC} 挡是 0:{@code 2^60} 在放宽前后都落在 + * 挡外,那一刀这条网格照不到。 + */ + @Test + void timeAndDateGridMatchesReference() { + Diff.assertMatches(""" + +-- os.time / os.date 矩阵:这两个函数被「库函数 × 错误参数」网格排除过 +-- (无参时读时钟),但带上显式参数就是确定性的 +local function norm(e) return (tostring(e):gsub("0x%x+", "PTR")) end +local function ttbl(t) + if type(t) ~= "table" then return tostring(t) end + local ks = { "year", "month", "day", "hour", "min", "sec", "wday", "yday", "isdst" } + local out = {} + for _, k in ipairs(ks) do out[#out + 1] = k .. "=" .. tostring(t[k]) end + return "{" .. table.concat(out, ",") .. "}" +end +local function show(...) + local n = select('#', ...) + local t = {} + for i = 1, n do + local v = (select(i, ...)) + t[i] = type(v) == "table" and ttbl(v) or norm(tostring(v)) + end + return table.concat(t, ",") +end +local function s(tag, f, ...) + local r = table.pack(pcall(f, ...)) + if not r[1] then + print(tag, norm(tostring(r[2]))) + else + print(tag, "OK", show(table.unpack(r, 2, r.n))) + end +end + +-- ---------- os.time:每个字段 × 每种值 ---------- +local BASE = { year = 1970, month = 1, day = 1, hour = 0, min = 0, sec = 0 } +local NAMES = { "year", "month", "day", "hour", "min", "sec", "isdst" } +local VALS = { false, true, 0, 1, -1, 1970, 2000, 12, 13, 31, 32, 59, 60, 61, + 1.5, 2.0, "1", "x", {}, -2147483648, 2147483647, 2 ^ 31, 2 ^ 40 } +local NV = #VALS + +-- Apple libc 的 mktime 拒绝 1900 年以前(实测:year=1900 成、1899 不成), +-- glibc 不拒绝。落到这一档的输入两边各自推导、谁也不算对,所以不进差分表。 +-- 判据只挑「过了 getfield 那一关、但算出来的日期远早于 1900」的那些值: +-- out-of-bound 的(getfield 先报错)和 1.5 这种(报 not an integer)都要留着。 +local function mktimeRefuses(n, v) + if n ~= "year" and n ~= "day" and n ~= "hour" and n ~= "min" and n ~= "sec" then + return false + end + local num = tonumber(v) + if num == nil or num % 1 ~= 0 then return false end + -- year 的 -2147483648 会先被 getfield 判成 out-of-bound,mktime 根本没跑到 + if n == "year" and num <= -2147483648 then return false end + if n == "year" then return num < 1900 end + if n == "day" then return 1970 + num / 365.25 < 1900 end + if n == "hour" then return 1970 + num / 8766 < 1900 end + if n == "min" then return 1970 + num / 525960 < 1900 end + if n == "sec" then return 1970 + num / 31557600 < 1900 end + return false +end + +for _, n in ipairs(NAMES) do + for i = 1, NV do + local v = VALS[i] + local tag = "t|" .. n .. "|" .. i + if mktimeRefuses(n, v) then + print(tag, "SKIP") + else + s(tag, function() + local t = {} + for k, bv in pairs(BASE) do t[k] = bv end + t[n] = v + return os.time(t) + end) + end + end +end + +-- ---------- os.time:缺字段(year/month/day 没有默认值) ---------- +s("t|miss|year", function() return os.time({ month = 1, day = 1 }) end) +s("t|miss|month", function() return os.time({ year = 1970, day = 1 }) end) +s("t|miss|day", function() return os.time({ year = 1970, month = 1 }) end) +s("t|miss|all", function() return os.time({}) end) +s("t|def", function() return os.time({ year = 1970, month = 1, day = 1 }) end) +s("t|def2", function() return os.time({ year = 2000, month = 6, day = 15 }) end) +s("t|isdst|nil", function() return os.time({ year = 1970, month = 1, day = 1, isdst = nil }) end) +s("t|isdst|false", function() return os.time({ year = 1970, month = 1, day = 1, isdst = false }) end) +s("t|isdst|zero", function() return os.time({ year = 1970, month = 1, day = 1, isdst = 0 }) end) +s("t|isdst|true", function() return os.time({ year = 1970, month = 1, day = 1, isdst = true }) end) + +-- ---------- os.time:非表 / 多余参数 ---------- +-- os.time(nil) 与 os.time() 走 lua_isnoneornil,读的是时钟,不进差分表 +s("t|arg|num", function() return os.time(0) end) +s("t|arg|str", function() return os.time("x") end) +s("t|arg|bool", function() return os.time(true) end) +s("t|arg|extra", function() return os.time({ year = 1970, month = 1, day = 1 }, 5) end) +s("t|arg|strnum", function() return os.time({ year = "1970", month = 1, day = 1 }) end) +s("t|arg|floatint", function() return os.time({ year = 1970.0, month = 1, day = 1 }) end) + +-- ---------- os.time:表被改写(setallfields 的归一) ---------- +s("t|norm|over", function() + local t = { year = 2000, month = 13, day = 40, hour = 25, min = 61, sec = 61 } + local r = os.time(t) + return r, ttbl(t) +end) +s("t|norm|neg", function() + local t = { year = 1969, month = 0, day = 0, hour = -1, min = -1, sec = -1 } + local r = os.time(t) + return r, ttbl(t) +end) +s("t|norm|extra", function() + local t = { year = 1970, month = 1, day = 1, zzz = 1 } + local r = os.time(t) + return r, ttbl(t), t.zzz +end) + +-- ---------- os.time:结果表示不出来 ---------- +s("t|big|1", function() return os.time({ year = 2 ^ 40 }) end) +s("t|big|2", function() return os.time({ year = -2 ^ 40 }) end) +s("t|big|3", function() return os.time({ year = 1970, month = 1, day = 1, sec = 2 ^ 40 }) end) +s("t|big|4", function() return os.time({ year = 2147483647, month = 1, day = 1 }) end) + +-- ---------- os.date:格式 × 时间 ---------- +local FMT = { + "%c", "%a", "%A", "%b", "%B", "%d", "%H", "%I", "%j", "%m", "%M", "%p", + "%S", "%U", "%w", "%W", "%x", "%X", "%y", "%Y", "%%", + "%e", "%F", "%g", "%G", "%h", "%n", "%r", "%R", "%t", "%T", "%u", "%V", + "%z", + "%Ec", "%EC", "%Ex", "%EY", "%Od", "%OH", "%OI", "%Om", "%OS", "%Ou", "%Oy", + "%Q", "%", "%E", "%O", "%Ey", "%v", "%%%", + "", "plain", "a%db", "%Y-%m-%d %H:%M:%S", "*t", "!*t", "!%c", "!%Y", +} +local TIMES = { 0, 1, -1, 1000000000, 2147483647, -2147483648, 2 ^ 31, 2 ^ 32, + 2 ^ 40, 2 ^ 60, -2 ^ 60, 1.5, 0.5, 1e300, -1e300 } +local NT = #TIMES +for _, f in ipairs(FMT) do + for i = 1, NT do + local tv = TIMES[i] + s("d|" .. f .. "|" .. i, function() return os.date(f, tv) end) + end +end + +-- ---------- os.date:格式与时间这两个参数本身 ---------- +s("d|arg|nilfmt", function() return os.date(nil, 0) end) +s("d|arg|numfmt", function() return os.date(1, 0) end) +s("d|arg|boolfmt", function() return os.date(true, 0) end) +s("d|arg|tblfmt", function() return os.date({}, 0) end) +s("d|arg|nostime", function() return os.date("%Y", nil) end) +s("d|arg|strtime", function() return os.date("%Y", "0") end) +s("d|arg|booltime", function() return os.date("%Y", true) end) +s("d|arg|nul", function() return os.date("*t\\0x", 0) end) +s("d|arg|fmtnul", function() return os.date("%c\\0junk", 0) end) + +-- ---------- os.difftime ---------- +local D = { 0, 1, -1, 1000000000, 1.5, "1", true, {}, 2 ^ 40 } +for i = 1, #D do + s("df|" .. i, function() return os.difftime(D[i], 0) end) + s("df|0|" .. i, function() return os.difftime(0, D[i]) end) +end +s("df|none", function() return os.difftime(0) end) +s("df|one", function() return os.difftime() end) +s("df|three", function() return os.difftime(10, 3, 1) end) + +-- ---------- os.getenv ---------- +s("env|none", function() return os.getenv("FK_NO_SUCH_ENV_VAR_XYZ") end) +s("env|bad", function() return os.getenv() end) +s("env|num", function() return os.getenv(1) end) +s("env|bool", function() return os.getenv(true) end) +s("env|nul", function() return os.getenv("FK_X\\0Y") end) + +-- ---------- os.setlocale(放最后:它会改后面 os.date 的输出) ---------- +s("loc|get", function() return os.setlocale() end) +s("loc|c", function() return os.setlocale("C") end) +s("loc|get2", function() return os.setlocale() end) +s("loc|bad", function() return os.setlocale("no_such_locale_xyz") end) +s("loc|cat", function() return os.setlocale("C", "all") end) +s("loc|badcat", function() return os.setlocale("C", "zzz") end) +s("loc|num", function() return os.setlocale(1) end) +s("loc|restore", function() return os.setlocale("C") end) + +print("done") + """); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/PackTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/PackTest.java new file mode 100644 index 0000000..2de7a34 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/PackTest.java @@ -0,0 +1,312 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +/** + * {@code string.pack} / {@code string.unpack} / {@code string.packsize} + * 的差分用例。 + *

+ * 这一组用例与其它库有两点不同,写在前面: + *

    + *
  • 结果是平台相关的。{@code l} / {@code L} 是 C 的 {@code long}、 + * {@code T} 是 {@code size_t}、{@code !} 的默认对齐取 + * {@code LUAI_MAXALIGN} 的偏移、默认字节序取宿主的。这些在 + * {@code StringPack} 里按参考二进制所在的 LP64 + 小端平台钉死了。
  • + *
  • {@code X} 会吃掉后一项。{@code string.packsize("Xi4")} 是 + * {@code 0} 而不是 {@code 4}。看着像 bug,但参考实现就是这样, + * 而且这是最容易「顺手改对」的一处。
  • + *
+ *

+ * 字节一律用 {@code string.byte} / {@code string.char} 看,不直接 {@code print} + * 二进制——{@code print} 出来的原始字节在两边都是不可比的东西(终端编码、 + * 缓冲),而差分比的是 stdout 的文本。 + */ +class PackTest { + + @BeforeAll + static void needReference() { + assumeTrue(ReferenceLua.available(), + "no lua5.4 reference binary; set LUA54_BIN"); + } + + @TestFactory + List packScriptsMatchReference() { + List tests = new ArrayList<>(); + add(tests, "int", INT); + add(tests, "float", FLOAT); + add(tests, "str", STR); + add(tests, "align", ALIGN); + add(tests, "wide", WIDE); + add(tests, "err", ERRORS); + add(tests, "keys", KEYS); + return tests; + } + + private static void add(List tests, String tag, String[] srcs) { + for (int i = 0; i < srcs.length; i++) { + String src = srcs[i]; + int n = i; + tests.add(DynamicTest.dynamicTest(tag + "[" + n + "]", () -> Diff.assertMatches(src))); + } + } + + /* ==================== 整数 ==================== */ + + private static final String[] INT = { + """ + local function hex(s) local t = {} for i = 1, #s do t[i] = string.format("%02x", string.byte(s, i)) end return table.concat(t, " ") end + print(hex(string.pack("i4", 7))) + print(hex(string.pack(">i4", 7))) + print(hex(string.pack("粘的:一次设定,之后一直有效 + print(string.unpack("i2I2", "\\1\\0\\2\\0")) + print(string.unpack(">i2I2", "\\0\\1\\0\\2")) + print(string.unpack("i2", "\\1\\0\\0\\2")) + print(string.unpack("=i2", "\\1\\0")) + """, }; + + /* ==================== 浮点 ==================== */ + + private static final String[] FLOAT = { + """ + local function hex(s) local t = {} for i = 1, #s do t[i] = string.format("%02x", string.byte(s, i)) end return table.concat(t, " ") end + print(hex(string.pack("f", 1.5)), #string.pack("f", 1.5)) + print(hex(string.pack("d", 1.5)), #string.pack("d", 1.5)) + print(hex(string.pack("n", 1.5))) + print(hex(string.pack(">d", 1.5))) + print(hex(string.pack("f", 0.5))) + print(string.unpack(">f", string.pack("f", 0.5))) + print(math.type(select(1, string.unpack("d", string.pack("d", 1.5))))) + """, + + """ + -- 整数也能喂给浮点格式(luaL_checknumber 会转) + print(string.unpack("d", string.pack("d", 3))) + print(string.unpack("f", string.pack("f", 3))) + print(pcall(string.pack, "d", "1.5")) + print(string.unpack("d", string.pack("d", "1.5"))) + """, }; + + /* ==================== 字符串 ==================== */ + + private static final String[] STR = { + """ + local function q(s) return (s:gsub(".", function(c) return string.format("\\\\%03d", string.byte(c)) end)) end + print(q(string.pack("c0", ""))) + print(q(string.pack("c3", "ab"))) + print(q(string.pack("c3", "abc"))) + print(q(string.pack("z", "ab"))) + print(q(string.pack("s1", "ab"))) + print(q(string.pack("s4", "ab"))) + print(q(string.pack("s8", "ab"))) + print(q(string.pack(">s1", "ab"))) + """, + + """ + -- 往返 + print(string.unpack("c3", string.pack("c3", "ab"))) + print(string.unpack("z", string.pack("z", "ab"))) + print(string.unpack("s1", string.pack("s1", "abc"))) + print(string.unpack("s8", string.pack("s8", "abc"))) + print(select("#", string.unpack("s1", string.pack("s1", "abc")))) + print(string.unpack("c0z", "\\0")) + """, + + """ + -- 后面还有数据时,最后那个返回值是下一次的位置 + local s = string.pack("i4i4", 1, 2) + print(string.unpack("i4", s)) + print(string.unpack("i4", s, 5)) + print(select("#", string.unpack("i4i4", s))) + print(string.unpack("c2c2", "abcd")) + print(string.unpack("i4", s, -5)) + print(string.unpack("i4", s, -1)) + """, + + """ + -- 空格式与配置类选项 + print(#string.pack("")) + print(string.pack("")) + print(string.unpack("", "abc")) + print(#string.pack(" ", 1)) + print(#string.pack("x")) + print(#string.pack("xxx")) + print(string.packsize("xi4x")) + """, }; + + /* ==================== 对齐 ==================== */ + + private static final String[] ALIGN = { + """ + print(string.packsize("i4")) + print(string.packsize("d")) + print(string.packsize("i1d")) + print(string.packsize("!1i1d")) + print(string.packsize("!4i4d")) + print(string.packsize("!8i4d")) + print(string.packsize("i4Xi4")) + print(#string.pack("i1d", 1, 2.5)) + print(#string.pack("!1i1d", 1, 2.5)) + """, + + """ + -- 'X' 会吃掉后一项:这一条是本组最反直觉的 + print(string.packsize("Xi4")) + print(#string.pack("Xi4", 7)) + print(#string.pack("i1Xi4", 7)) + print(pcall(string.packsize, "X")) + print(pcall(string.packsize, "Xc3")) + print(pcall(string.packsize, "Xz")) + """, + + """ + -- 对齐只按「当前偏移」算,与前面有几项无关 + local s = string.pack("i1i1d", 1, 2, 3.5) + print(#s, string.unpack("i1i1d", s)) + print(string.packsize("!4i1d")) + print(#string.pack("!4i1d", 1, 2.5)) + print(#string.pack(">i1d", 1, 2.5)) + """, }; + + /* ==================== 宽整数 ==================== */ + + private static final String[] WIDE = { + """ + -- size 比 lua_Integer 还宽:pack 要把多出来的高位补成符号位, + -- unpack 要检查那些没读进来的字节是否也是符号位 + local function hex(s) local t = {} for i = 1, #s do t[i] = string.format("%02x", string.byte(s, i)) end return table.concat(t, " ") end + print(hex(string.pack("i9", -1))) + print(hex(string.pack("i9", 1))) + print(hex(string.pack("i16", -1))) + print(hex(string.pack("i16", 1))) + print(hex(string.pack("I16", -1))) + print(string.unpack("i9", string.pack("i9", -1))) + print(string.unpack("i12", string.rep("\\255", 12))) + print(string.unpack("i16", string.rep("\\255", 16))) + """, + + """ + -- 未读的高位字节不是符号位 → 装不进 Lua 整数 + print(pcall(string.unpack, "i9", "\\1" .. string.rep("\\255", 8))) + print(pcall(string.unpack, "i9", string.rep("\\255", 8) .. "\\0")) + print(pcall(string.unpack, "I9", string.rep("\\255", 8) .. "\\1")) + print(string.unpack("I9", string.rep("\\255", 9))) + """, + + """ + local function hex(s) local t = {} for i = 1, #s do t[i] = string.format("%02x", string.byte(s, i)) end return table.concat(t, " ") end + print(hex(string.pack("T", 7)), hex(string.pack("J", 7))) + print(string.unpack("T", string.pack("T", 7))) + print(string.unpack("J", string.pack("J", -1))) + print(hex(string.pack(">I16", 1))) + print(string.unpack(">I16", string.pack(">I16", 1))) + print(hex(string.pack("l", -1)), hex(string.pack("L", -1))) + """, }; + + /* ==================== 报错 ==================== */ + + private static final String[] ERRORS = { + """ + print(pcall(string.pack, "i4", 1.5)) + print(pcall(string.pack, "i4", 2^53)) + print(pcall(string.pack, "i1", 200)) + print(pcall(string.pack, "I1", -1)) + print(pcall(string.pack, "i17", 1)) + print(pcall(string.pack, "i0", 1)) + print(pcall(string.pack, "Q", 1)) + print(pcall(string.pack, "!0i4", 1)) + print(pcall(string.pack, "!17i4", 1)) + print(pcall(string.pack, 42, 1)) + """, + + """ + print(pcall(string.pack, "c", "x")) + print(pcall(string.pack, "c2", "xyz")) + print(pcall(string.pack, "s1", string.rep("a", 300))) + print(pcall(string.pack, "z", "a\\0b")) + print(pcall(string.pack, "s1", 42)) + print(pcall(string.pack, "c2")) + print(pcall(string.packsize, "s1")) + print(pcall(string.packsize, "z")) + print(pcall(string.packsize, 42)) + """, + + """ + print(pcall(string.unpack, "i4", "abc")) + print(pcall(string.unpack, "i4", "abcd", 5)) + print(pcall(string.unpack, "i4", "abcd", 100)) + print(pcall(string.unpack, "z", "abc")) + print(pcall(string.unpack, "s1", "\\3ab")) + print(pcall(string.unpack, "!3i4", "\\1\\0\\0\\0")) + print(pcall(string.unpack, "i4")) + print(pcall(string.unpack, "i4", 42)) + print(pcall(string.unpack, "i4", "abcd", "x")) + """, }; + + /* ==================== 命名空间 ==================== */ + + /** + * 枚举整个 {@code string} 表的键。 + *

+ * 看着琐碎,但这是「漏了一个函数」唯一的自动检查——之前 + * {@code dump} 只占着名字不干活,就是为了保住这一个比对。现在 + * {@code pack} / {@code packsize} / {@code unpack} 齐了,键集合应当 + * 与参考实现完全一致。 + */ + private static final String[] KEYS = { + """ + local ks = {} + for k in pairs(string) do ks[#ks + 1] = k end + table.sort(ks) + print(#ks, table.concat(ks, " ")) + """, }; +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/PackageLibTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/PackageLibTest.java new file mode 100644 index 0000000..14da5fe --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/PackageLibTest.java @@ -0,0 +1,651 @@ +package com.github.esrrhs.fakescript.lua; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +/** + * package 库的差分用例 —— 阶段 7。 + *

+ * 只比「纯 Lua 那条路」:{@code preload} 搜索器与 {@code Lua} 搜索器的行为 + * 可以与参考实现逐字节一致,C 搜索器不能——参考二进制是用 {@code dlopen} 编 + * 出来的,本实现没有动态库。所以: + *

    + *
  • 不比对 {@code package.loadlib} 的返回值(消息是 + * {@code dlerror()} 的文本,平台相关);
  • + *
  • 不比对「能找到 {@code .so}」的 {@code require}——测试里根本 + * 不建 {@code .so} 文件,C 搜索器一律停在「找不到文件」,两边一致;
  • + *
  • {@code package.path} / {@code cpath} 的数值不比:那是编译期 + * 常量,本实现照抄了默认值,但 {@code LUA_PATH} 环境变量会改它。用例里 + * 都先显式设成临时目录再 require。
  • + *
+ *

+ * 真正盯住的: + *

    + *
  • {@code require} 返回两个值:模块本身与 loader 数据(Lua 模块 + * 是文件名,preload 是 {@code ":preload:"});
  • + *
  • 模块返回 nil 时 {@code _LOADED[name]} 是 {@code true}, + * {@code require} 也返回 {@code true};
  • + *
  • {@code require} 的缓存:第二次调用不再跑模块体;
  • + *
  • 找不到时的错误消息是各搜索器消息用 {@code \n\t} 拼起来的;
  • + *
  • {@code package.searchpath} 的 {@code sep} / {@code dirsep} + * 替换与 {@code no file '...'} 消息的拼法。
  • + *
+ */ +class PackageLibTest { + + @TestFactory + List packageScriptsMatchReference() { + List tests = new ArrayList<>(); + add(tests, "cfg", CONFIG); + add(tests, "search", SEARCHPATH); + add(tests, "preload", PRELOAD); + add(tests, "lua", LUAMOD); + add(tests, "err", ERRORS); + add(tests, "deep", DEEP); + add(tests, "cfallback", CFALLBACK); + return tests; + } + + private static void add(List tests, String tag, String[] srcs) { + for (int i = 0; i < srcs.length; i++) { + String src = srcs[i]; + int n = i; + tests.add(DynamicTest.dynamicTest(tag + "[" + n + "]", () -> Diff.assertMatches(src))); + } + } + + /* ==================== 表结构 ==================== */ + + private static final String[] CONFIG = { + """ + print(package.config) + print(type(package.loaded), type(package.preload), type(package.searchers)) + print(#package.searchers) + print(type(package.searchers[1]), type(package.searchers[2])) + print(type(package.searchers[3]), type(package.searchers[4])) + print(type(package.path), type(package.cpath)) + print(type(package.loadlib), type(package.searchpath)) + print(package.loaded.package == package) + print(package.loaded.string == string) + print(package.loaded["no_such_module_zzz"]) + print(require) + """, + + """ + -- config 的五个字段:目录分隔符、路径分隔符、模板占位符、 + -- 可执行文件目录占位符、忽略标记 + local a,b,c,d,e = package.config:match("(%S+)\\n(%S+)\\n(%S+)\\n(%S+)\\n(%S+)\\n") + print(a, b, c, d, e) + print(package.path:find("./", 1, true) ~= nil) + print(package.cpath:sub(-6)) + """, + + """ + -- searchers 是裸表:改动它会影响 require + print(#package.searchers) + local n = 0 + for k, v in ipairs(package.searchers) do n = n + 1 end + print(n) + print(package.searchers[5]) + print(package.searchers[0]) + """, }; + + /* ==================== searchpath ==================== */ + + private static final String[] SEARCHPATH = { + """ + -- 找到就返回文件名 + local p = "/tmp/fkpk_sp1.lua" + local f = io.open(p, "w") + f:write("return {}\\n") + f:close() + print(package.searchpath("fkpk_sp1", "/tmp/?.lua")) + print(package.searchpath("fkpk_sp1", "/tmp/?.lua;/tmp/?/init.lua")) + os.remove(p) + """, + + """ + -- 找不到:nil + "no file '...'" 逐条拼起来 + print(package.searchpath("zzz_nope", "./?.lua;./?/init.lua")) + print(package.searchpath("zzz_nope", "./?.lua")) + print(package.searchpath("zzz_nope", "a;;b")) + """, + + """ + -- sep / dirsep:名字里的 sep 换成 dirsep 再去套模板 + print(package.searchpath("a.b", "./?.lua", ".", "/")) + print(package.searchpath("a.b", "./?.lua")) + print(package.searchpath("a.b", "./?.lua", "|", "-")) + print(package.searchpath("a.b", "./?.lua", ".", "::")) + print(package.searchpath("a", "./?.lua", "", "")) + """, + + """ + -- 参数检查 + print(pcall(package.searchpath)) + print(pcall(package.searchpath, "x")) + print(pcall(package.searchpath, "x", 42)) + print(package.searchpath("x", "")) + """, }; + + /* ==================== preload 搜索器 ==================== */ + + private static final String[] PRELOAD = { + """ + package.preload["fkpk_p1"] = function(name) return {v = 1, n = name} end + local m, ld = require("fkpk_p1") + print(m.v, m.n, ld) + print(package.loaded["fkpk_p1"] == m) + -- 第二次走缓存:模块体不再跑 + package.preload["fkpk_p1"] = function() return {v = 2} end + print(require("fkpk_p1").v) + """, + + """ + -- 模块返回 nil:_LOADED[name] 变成 true + package.preload["fkpk_p2"] = function() return nil end + print(require("fkpk_p2")) + print(package.loaded["fkpk_p2"]) + -- 模块什么都不返回也是 true + package.preload["fkpk_p3"] = function() end + print(require("fkpk_p3")) + print(package.loaded["fkpk_p3"]) + """, + + """ + -- 模块返回 false / 0 也算「设了值」(只有 nil 不算) + package.preload["fkpk_p4"] = function() return false end + local m = require("fkpk_p4") + print(m) + print(package.loaded["fkpk_p4"]) + print(require("fkpk_p4")) + """, + + """ + -- 模块名里带点也走 preload(Croot 搜索器只在 cpath 命中时才插话) + package.preload["fkpk.p5"] = function(n) return {n = n} end + package.path = "/tmp/?.lua" + package.cpath = "/tmp/?.so" + local m, ld = require("fkpk.p5") + print(m.n, ld) + print(package.loaded["fkpk.p5"] == m) + """, }; + + /* ==================== Lua 搜索器 ==================== */ + + private static final String[] LUAMOD = { + """ + -- 从文件加载;require 的第二个返回值是文件名 + local p = "/tmp/fkpk_l1.lua" + local f = io.open(p, "w") + f:write("local M = {v = 42}\\nreturn M\\n") + f:close() + package.path = "/tmp/?.lua" + local m, ld = require("fkpk_l1") + print(m.v, ld) + print(package.loaded["fkpk_l1"] == m) + os.remove(p) + """, + + """ + -- 子模块:a.b 会去找 a/b.lua + local p = "/tmp/fkpk_l2.lua" + local f = io.open(p, "w") + f:write("return {deep = true}\\n") + f:close() + package.path = "/tmp/?.lua" + local m = require("fkpk_l2") + print(m.deep) + os.remove(p) + """, + + """ + -- 模块里的错误带的是文件的 chunk 名(@ + 路径),不是调用点 + local p = "/tmp/fkpk_l3.lua" + local f = io.open(p, "w") + f:write("error('boom')\\n") + f:close() + package.path = "/tmp/?.lua" + print(pcall(require, "fkpk_l3")) + os.remove(p) + """, + + """ + -- 模块有语法错误:searcher_Lua 报 error loading module + local p = "/tmp/fkpk_l4.lua" + local f = io.open(p, "w") + f:write("this is not lua\\n") + f:close() + package.path = "/tmp/?.lua" + print(pcall(require, "fkpk_l4")) + os.remove(p) + """, + + """ + -- package.path 不是字符串 + package.path = 42 + print(pcall(require, "fkpk_l5")) + package.path = true + print(pcall(require, "fkpk_l5")) + """, + + """ + -- 标准库都在 _LOADED 里,require 它们直接命中缓存 + local a = require("table") + local b = require("table") + print(a == table, b == table) + print(require("string") == string) + print(require("math") == math) + print(require("io") == io) + print(require("os") == os) + print(require("utf8") == utf8) + print(require("coroutine") == coroutine) + """, }; + + /* ==================== 报错 ==================== */ + + private static final String[] ERRORS = { + """ + -- 找不到:四个搜索器的消息依次拼起来 + package.path = "/tmp/?.lua" + package.cpath = "/tmp/?.so" + print(pcall(require, "no_such_module_zzz")) + """, + + """ + -- 带点的名字会多出 Croot 那一条 + package.path = "/tmp/?.lua" + package.cpath = "/tmp/?.so" + print(pcall(require, "no.such.module")) + """, + + """ + -- searchers 被弄坏 + print(pcall(require)) + print(pcall(require, 42)) + package.searchers = 42 + print(pcall(require, "fkpk_e3")) + package.searchers = {} + print(pcall(require, "fkpk_e3")) + """, + + """ + -- searchers 里放一个返回非函数非字符串的东西:那个搜索器被跳过, + -- 消息里也不会多出一行 + package.path = "/tmp/?.lua" + package.cpath = "/tmp/?.so" + package.searchers = { + function(n) return 42 end, + function(n) return nil, "extra" end, + } + print(pcall(require, "fkpk_e4")) + """, + + """ + -- 搜索器自己报错会直接冒出去(require 不用 pcall 包它) + package.searchers = { function(n) error("searcher died") end } + print(pcall(require, "fkpk_e5")) + """, }; + + /** + * {@code require} / {@code package.searchpath} 的深矩阵(57 行): + * 真在磁盘上造一批模块,然后走完整条搜索器链 —— 而不只是在内存里摆 + * {@code package.preload}。之前这一块只覆盖了「纯 Lua 那条路」的一半。 + *

+ * 覆盖到的形状:模块返回 nil / false / true / 数字 / 无返回 / 多值(只取第一)、 + * 模块抛错、模块语法错、模块里自己设 {@code package.loaded}、 + * 点号模块名换目录分隔符、重复 {@code require} 的缓存、循环 {@code require} + * (参考给 {@code C stack overflow},不是哨兵)、 + * {@code package.loaded} 预置成值 / false / nil 的三种后果、 + * {@code package.preload} 给函数 / 返回 nil / 抛错 / 非函数、 + * {@code searchpath} 的 {@code sep} / {@code rep} 与缺参、 + * {@code package.searchers} 的个数与逐个调用、整条链被换掉。 + *

+ * 顺带钉住一件容易悄悄坏掉的事:{@code package.path} 的默认值必须与参考 + * 二进制编译时那套 {@code LUA_PATH_DEFAULT} 一致 —— 矩阵里每条 + * {@code module 'X' not found} 都把 {@code /usr/local/share/lua/5.4/...} 那 + * 一串完整打印出来,改一个字符就红。 + *

+ * 敏感性量过:把 {@code searchpath} 的默认 {@code sep} 从 {@code "."} 改成 + * {@code "-"},立刻红在 {@code sp_dots}。 + *

+ * 库目录用 {@code /tmp/fkreq} 并在开头 {@code rm -rf} 重建:两边跑在同一台 + * 机器上,绝对路径对双方都可见,也不依赖 cwd。 + */ + private static final String[] DEEP = {""" + -- require / package.searchpath 深矩阵:真在磁盘上造模块,走完整条搜索器链 + local out = {} + local function d(v) + local t = type(v) + if t == "nil" then return "nil" end + if t == "boolean" then return tostring(v) end + if t == "number" then return math.type(v) .. ":" .. tostring(v) end + if t == "string" then return string.format("q(%d)[%s]", #v, (v:gsub("\\n", "\\\\n"))) end + return t + end + local function probe(name, f, ...) + local r = table.pack(pcall(f, ...)) + local s = name .. " [" .. r.n .. "]" + for i = 2, r.n do s = s .. " " .. d(r[i]) end + if not r[1] then s = s .. " ERR " .. d(r[2]) end + out[#out + 1] = s + end + local function say(s) out[#out + 1] = s end + + local D = "/tmp/fkreq" + local R = D .. "/repo" + os.execute("rm -rf " .. D .. " && mkdir -p " .. R .. "/sub") + local function put(path, body) + local f = assert(io.open(path, "w")) + f:write(body) + f:close() + end + + put(R .. "/basic.lua", "return 'BASIC'\\n") + put(R .. "/nonil.lua", "-- 什么都不返回\\n") + put(R .. "/retnil.lua", "return nil\\n") + put(R .. "/retfalse.lua", "return false\\n") + put(R .. "/rettrue.lua", "return true\\n") + put(R .. "/retnum.lua", "return 42\\n") + put(R .. "/retmulti.lua", "return 1, 2, 3\\n") + put(R .. "/err.lua", "error('boom')\\n") + put(R .. "/syntax.lua", "this is not lua\\n") + put(R .. "/prints.lua", "print('SIDE'); return 'P'\\n") + put(R .. "/setsloaded.lua", "package.loaded['setsloaded'] = 'PRESET'; return 'FROMCHUNK'\\n") + put(R .. "/sub/inner.lua", "return 'INNER'\\n") + put(R .. "/counter.lua", "local n = (package.loaded['counter_n'] or 0) + 1\\n" + .. "package.loaded['counter_n'] = n\\nreturn n\\n") + put(R .. "/circ.lua", "return require('circ')\\n") + + -- 把仓库路径前置到 package.path,避免依赖 cwd + local oldpath = package.path + package.path = R .. "/?.lua;" .. R .. "/?/init.lua;" .. oldpath + + say("path_head " .. tostring(package.path:sub(1, #R + 6))) + + probe("r_basic", require, "basic") + probe("r_basic_twice", require, "basic") -- 缓存:第二次不再跑 + probe("r_prints", require, "prints") -- 副作用只发生一次 + probe("r_prints2", require, "prints") + probe("r_nonil", require, "nonil") -- 无返回 → true + probe("r_retnil", require, "retnil") -- 返回 nil → true + probe("r_retfalse", require, "retfalse") -- 返回 false → false + probe("r_rettrue", require, "rettrue") + probe("r_retnum", require, "retnum") + probe("r_retmulti", require, "retmulti") -- 只取第一个 + probe("r_err", require, "err") + probe("r_syntax", require, "syntax") + probe("r_setsloaded", require, "setsloaded") -- chunk 里先设了 loaded + probe("r_setsloaded2", require, "setsloaded") + probe("r_sub", require, "sub.inner") -- 点号换成目录分隔符 + probe("r_counter1", require, "counter") + probe("r_counter2", require, "counter") + probe("r_missing", require, "no_such_mod_xyz") + probe("r_missing_dots", require, "a.b.c.nope") + probe("r_circ", require, "circ") -- 循环 require 给的是哨兵 + probe("r_noname", require) + probe("r_nilname", require, nil) + probe("r_numname", require, 42) + probe("r_emptyname", require, "") + probe("r_boolname", require, true) + + -- package.loaded 预置 + package.loaded["preset"] = "PRE" + probe("l_preset", require, "preset") + package.loaded["prefalse"] = false + probe("l_prefalse", require, "prefalse") + package.loaded["prenil"] = nil + probe("l_prenil", require, "prenil") + + -- package.preload + package.preload["frompreload"] = function() return "FROM_PRELOAD" end + probe("p_preload", require, "frompreload") + package.preload["preloadnil"] = function() return nil end + probe("p_preloadnil", require, "preloadnil") + package.preload["preloaderr"] = function() error("pe") end + probe("p_preloaderr", require, "preloaderr") + probe("p_preload_extra", function() + package.preload["multi"] = function(n) return "N=" .. tostring(n) end + return require("multi") + end) + probe("p_preload_badtype", function() + package.preload["notfn"] = 42 + return require("notfn") + end) + + -- searchpath + probe("sp_basic", package.searchpath, "basic", R .. "/?.lua") + probe("sp_missing", package.searchpath, "nope_xyz", R .. "/?.lua") + probe("sp_multi", package.searchpath, "nope_xyz", R .. "/?.lua;" .. R .. "/?/init.lua") + probe("sp_dots", package.searchpath, "sub.inner", R .. "/?.lua") + probe("sp_sep", package.searchpath, "sub.inner", R .. "/?.lua", ".", "/") + probe("sp_rep", package.searchpath, "subXinner", R .. "/?.lua", "X", "/") + probe("sp_rep2", package.searchpath, "sub--inner", R .. "/?.lua", "--", "/") + probe("sp_emptysep", package.searchpath, "basic", R .. "/?.lua", "", "") + probe("sp_nopath", package.searchpath, "basic", "") + probe("sp_nosep", package.searchpath, "basic", R .. "/?.lua") + probe("sp_badname", package.searchpath, 42, R .. "/?.lua") + probe("sp_badpath", package.searchpath, "basic", 42) + probe("sp_noargs", package.searchpath) + probe("sp_abs", package.searchpath, "basic", R .. "/?.lua;" .. R .. "/?.lua") + + -- searchers 链本身 + probe("s_count", function() return #package.searchers end) + probe("s_types", function() + local r = {} + for i, v in ipairs(package.searchers) do r[#r + 1] = type(v) end + return table.concat(r, ",") + end) + probe("s_call1", function() return package.searchers[1]("basic") end) + probe("s_call4", function() return package.searchers[4]("basic") end) + probe("s_call_missing1", function() return package.searchers[1]("nope_xyz_1") end) + probe("s_call_bad", function() return package.searchers[1](42) end) + probe("s_replace", function() + local old = package.searchers + package.searchers = { function(n) return function() return "REPLACED" end end } + local v = require("anything_xyz") + package.searchers = old + return v + end) + probe("s_empty", function() + local old = package.searchers + package.searchers = {} + local ok, e = pcall(require, "anything_xyz") + package.searchers = old + return ok, e + end) + + package.path = oldpath + os.execute("rm -rf " .. D) + print(table.concat(out, "\\n")) + """ }; + + + /** + * C 搜索器那条路(23 行)—— 也是照着未覆盖行点名打的。 + *

+ * 本实现没有 {@code dlopen},{@code loadfunc} 一律失败,但「失败成什么样」 + * 是有形状的:{@code searcher_C} 找到文件 → {@code loadfunc} 失败 → + * {@code checkLoad} 装裱出 {@code error loading module 'X' from file 'Y'}; + * {@code searcher_Croot} 还会先把模块名切成根名({@code a.b} → {@code a}); + * {@code package.loadlib} 的三元组第三个值是 {@code "open"}(不是 + * {@code "absent"} —— 那是 {@code ERRLIB} 才给的)。 + * 这些分支从来没人走过,因为默认 {@code cpath} 下永远找不到真文件; + * 把 {@code package.cpath} 指到一个真存在的文件上就走到了。 + *

+ * 一处不比:{@code dlopen} 的失败文本是平台自己的(macOS 是 + * {@code dlopen(...): tried: '...' (slice is not valid mach-o file)}, + * 本实现给 {@code dynamic libraries not enabled; check your Lua installation})。 + * 两边是同一件事、只是措辞不同,归一成 {@code } 再比,其余照比。 + *

+ * 顺带这一片还咬出了「目录当文件用」那处 bug({@code m_dir}): + * {@code require} 一个名字撞上目录的模块时,参考报 + * {@code error loading module ... cannot read ...: Is a directory}, + * 我们报 {@code module not found}。修法见 + * {@code RuntimeTest#directoryAsFileMatchesReference}。 + */ + private static final String[] CFALLBACK = {""" + -- 照着 PackageLib 未覆盖行点名打:C 搜索器那条路(本实现没有 dlopen, + -- 一律失败,但「失败成什么样」是有形状的)、loadlib、searchpath 的模板边界 + local out = {} + local function d(v) + local t = type(v) + if t == "nil" then return "nil" end + if t == "boolean" then return tostring(v) end + if t == "number" then return math.type(v) .. ":" .. tostring(v) end + if t == "string" then return string.format("q(%d)[%s]", #v, (v:gsub("\\n", "\\\\n"))) end + return t + end + -- C 搜索器 / loadlib 的失败文本是平台自己的:macOS 是 + -- "dlopen(...): tried: '...' (slice is not valid mach-o file)",glibc 是 + -- "...: cannot open shared object file" / "...: file too short" / + -- "undefined symbol: ...",本实现没有 dlopen,给 + -- "dynamic libraries not enabled...",而 require 那层会把它们再包成 + -- "error loading module 'x' from file 'y': ..."。两边都是「没有动态库」 + -- 这一件事,只是措辞不同 —— 归一成 再比,其余部分照比。 + local NODL_PATS = { "dlopen", "dynamic libraries not enabled", + "cannot open shared object", "file too short", + "undefined symbol", "error loading module" } + local function nodl(v) + if type(v) ~= "string" then return v end + for i = 1, #NODL_PATS do + if v:find(NODL_PATS[i], 1, true) then return "" end + end + return v + end + local function d0(v) + return d(nodl(v)) + end + local function probe(name, f, ...) + local r = table.pack(pcall(f, ...)) + local s = name .. " [" .. r.n .. "]" + for i = 2, r.n do s = s .. " " .. d0(r[i]) end + if not r[1] then s = s .. " ERR " .. d0(r[2]) end + out[#out + 1] = s + end + + local D = "/tmp/fkreq2" + os.execute("rm -rf " .. D .. " && mkdir -p " .. D) + local function put(p, body) + local f = assert(io.open(p, "w")); f:write(body or "x"); f:close() + end + -- 这些文件真的存在:C 搜索器于是能「找到」它们,才有后面的 loadfunc + put(D .. "/basic.so") + put(D .. "/a.so") + put(D .. "/bad.lua", "syntax error here\\n") + + -- 1. cpath 指到真实存在的文件上:searcher_C 找得到 → loadfunc → 无 dlopen → 失败 + probe("c_hit", function() + local old = package.cpath + package.cpath = D .. "/?.so" + local ok, e = pcall(require, "basic") + package.cpath = old + return ok, e + end) + probe("c_hit_dotted", function() + local old = package.cpath + package.cpath = D .. "/?.so" + local ok, e = pcall(require, "a.b") -- searcher_Croot:先切出根名 "a" + package.cpath = old + return ok, e + end) + probe("c_searcher_direct", function() + local old = package.cpath + package.cpath = D .. "/?.so" + local r = table.pack(pcall(package.searchers[3], "basic")) + package.cpath = old + local s = "" + for i = 2, r.n do s = s .. " " .. d0(r[i]) end + return "n=" .. r.n .. s + end) + probe("c_searcher4_direct", function() + local old = package.cpath + package.cpath = D .. "/?.so" + local r = table.pack(pcall(package.searchers[4], "a.b")) + package.cpath = old + local s = "" + for i = 2, r.n do s = s .. " " .. d0(r[i]) end + return "n=" .. r.n .. s + end) + probe("c_searcher4_root", function() + local old = package.cpath + package.cpath = D .. "/?.so" + local r = table.pack(pcall(package.searchers[4], "a")) -- 没有点号 → 直接返回 0 + package.cpath = old + return "n=" .. r.n + end) + + -- 2. package.loadlib:永远失败,但三元组有形状(第三个是 "absent") + probe("ll_basic", package.loadlib, "/tmp/nope.so", "luaopen_x") + probe("ll_exists", package.loadlib, D .. "/basic.so", "luaopen_basic") + probe("ll_noargs", package.loadlib) + probe("ll_onearg", package.loadlib, "/tmp/nope.so") + probe("ll_nil", package.loadlib, nil, "x") + probe("ll_num", package.loadlib, 42, "x") + -- ll_empty 是唯一要单独走的:三元组第三个值(「在哪一步失败」)落在 + -- ERRLIB 上时给 LIB_FAIL(POSIX 有 dlopen 是 "open",没有的是 + -- "absent"),落在 ERRFUNC 上时给 "init"。而空路径是分水岭: + -- glibc 的 dlopen("") 是「打开主程序」—— 它成功,于是往下走到 + -- dlsym("") 失败,给 "init";macOS 的 dlopen("") 直接失败,给 "open"; + -- 本实现没有 dlopen,也给 "open"。三个答案分属三个 loader,不是 Lua + -- 的语义差 —— 只比「这个三元组在不在、第三个是不是字符串」。 + -- 其余 ll_* 两边都停在 ERRLIB,"open" 照比(它同时钉住 LIB_FAIL 不是 + -- "absent")。 + local function probe_llnowhere(name, ...) + local r = table.pack(pcall(package.loadlib, ...)) + local s = name .. " [" .. r.n .. "]" + for i = 2, r.n do + local v = r[i] + if i == 4 and (v == "open" or v == "init" or v == "absent") then + v = "" + end + s = s .. " " .. d0(v) + end + if not r[1] then s = s .. " ERR " .. d0(r[2]) end + out[#out + 1] = s + end + probe_llnowhere("ll_empty", "", "") + + -- 3. searchpath 的模板边界:以分隔符结尾、只有分隔符、连续分隔符 + local P = D .. "/hit.lua" + put(P) + probe("sp_trailsep", package.searchpath, "hit", D .. "/?.lua;") + probe("sp_onlysep", package.searchpath, "hit", ";") + probe("sp_2sep", package.searchpath, "hit", ";;") + probe("sp_leadsep", package.searchpath, "hit", ";" .. D .. "/?.lua") + probe("sp_midsep", package.searchpath, "hit", ";" .. D .. "/?.lua;") + probe("sp_noq", package.searchpath, "hit", D .. "/hit.lua") + probe("sp_twoq", package.searchpath, "hit", D .. "/?.lua;" .. D .. "/?.lua") + + -- 4. 模块文件本身加载失败:语法错 / 是目录 + probe("m_syntax", function() + local old = package.path + package.path = D .. "/?.lua" + local ok, e = pcall(require, "bad") + package.path = old + return ok, e + end) + probe("m_dir", function() + local old = package.path + package.path = D .. "/?.lua" + os.execute("mkdir -p " .. D .. "/adir.lua") + local ok, e = pcall(require, "adir") + package.path = old + return ok, e + end) + probe("m_loadfile", function() + local ok, e = pcall(loadfile, D .. "/bad.lua") + return ok, e + end) + probe("m_loadfile_missing", function() + return loadfile(D .. "/nope_xyz.lua") + end) + + os.execute("rm -rf " .. D) + print(table.concat(out, "\\n")) + """ }; + +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/ParserTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/ParserTest.java new file mode 100644 index 0000000..6d2d82d --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/ParserTest.java @@ -0,0 +1,403 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * 语法错误信息的差分测试。 + *

+ * 阶段 1(词法)已经把词法层的报错逐字节对齐过了;这里接着对齐语法层: + * 「'end' expected」「label 'x' already defined on line N」 + * 「 at line N jumps into the scope of local 'y'」…… + *

+ * 为什么要这么较真:这些字符串是用户直接看到的。而且它们里的行号与名字 + * 是逐条手拼的({@code luaO_pushfstring}),照手册推必错——只能跑参考实现。 + */ +class ParserTest { + + @BeforeAll + static void requireReference() { + Assumptions.assumeTrue(ReferenceLua.available(), + "找不到 lua5.4 参考二进制,跳过语法错误差分:" + ReferenceLua.describe()); + } + + private static String escape(String s) { + return s.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t"); + } + + @Test + void syntaxErrorMessagesMatchReference() { + String[] cases = { + "x =", // '' expected + "if x then", // 'end' expected (to close 'if' at line 1) + "local 1 = 2", // expected + "f(1 2)", // ')' expected (to close '(' at line 1) + "::a::\n::a::", // label 'a' already defined + "goto nowhere", // no visible label + "break", // break outside loop + "goto l\nlocal x = 1\n::l::", // jumps into the scope of local + "local a = 1", // unknown attribute + "local a , b = nil, nil", // multiple to-be-closed + "local x = 1\nx = 2", // assign to const + "function 1() end", // expected + "x[1 = 2", // ']' expected + "a = 1 +", // unexpected symbol + "t = {1 2}", // '}' expected + "while x y end", // 'do' expected + "repeat x = 1", // 'until' expected + "#", // unexpected symbol + "(1", // ')' expected + "a.b.1 = 2", // expected + "for i = 1 do end", // ',' or 'in'... 实际是 fornum 缺上限 + "for i 1, 10 do end", // '=' or 'in' expected + "local x = function() return 1", // 'end' expected + "x = 1 = 2", // syntax error + "x() = 1", // syntax error + "return return", // unexpected symbol + "end", // unexpected symbol near 'end' + "do do end", // 'end' expected (to close 'do' at line 1) + "for i = 1, 2, 3, 4 do end", // 'do' expected + "local function", // expected + "a::b", // '=' expected? 实际是 syntax error + "x = ...", // 合法(主函数是 vararg) + }; + // 路径必须短:参考实现会把过长的源名截成 '...'(LUA_IDSIZE) + String path = "target/fkparse.lua"; + int checked = 0; + for (String src : cases) { + byte[] bytes = src.getBytes(StandardCharsets.ISO_8859_1); + ReferenceLua.FileResult ref = ReferenceLua.runBytes(bytes, path); + String label = "用例:" + escape(src); + if (ref.result.exitCode == 0) { + // 参考实现认为合法:我们也不能报错 + Parser.parseFile(bytes, ("@" + path).getBytes(StandardCharsets.UTF_8), new StringPool()); + checked++; + continue; + } + assertEquals(1, ref.result.exitCode, label); + if (ref.result.output.contains("stack traceback")) { + // 参考实现是运行时才失败的(比如给 变量赋了数字), + // 这类用例比不了语法消息——本测试只关心加载阶段的报错 + checked++; + continue; + } + int at = ref.result.output.indexOf(ref.path + ":"); + assertTrue(at >= 0, "参考输出里应当带脚本路径,实际:" + ref.result.output); + String expected = ref.result.output.substring(at).trim(); + + String actual; + try { + Parser.parseFile(bytes, ("@" + path).getBytes(StandardCharsets.UTF_8), new StringPool()); + fail("期望语法错误但没有报错," + label); + return; + } catch (LuaError e) { + assertNotNull(e.value, label); + actual = e.getMessage().trim(); + } + assertEquals(expected, actual, label); + checked++; + } + assertEquals(cases.length, checked); + } + + /** + * 编译期常量({@code })不占寄存器。 + *

+ * 这条规则从源码上看不出来({@code nvarstack} 要跳过 RDKCTC 的变量去数), + * 写错了现象是「后面的局部变量莫名其妙挪了一个寄存器」。 + */ + @Test + void compileTimeConstantDoesNotTakeARegister() { + Proto p = Parser.parse("local a = 1\nlocal b = 2\nreturn a + b\n" + .getBytes(StandardCharsets.UTF_8), + "t".getBytes(StandardCharsets.UTF_8), new StringPool()); + assertEquals(0, p.numparams); + // a 不占寄存器,b 在 r0;否则 b 会在 r1 而 maxstacksize 也更大 + assertEquals(2, p.maxstacksize, "maxstacksize 应当只用到 r0/r1"); + } + + /** {@code lauxlib.c: skipcomment}:文件首行的 {@code #!...} 要跳过,且行号不变 */ + @Test + void shebangLineIsSkippedAndKeepsLineNumbers() { + byte[] src = "#!/usr/bin/env lua\nlocal x = 1\nx()\n".getBytes(StandardCharsets.UTF_8); + byte[] stripped = Parser.skipFilePreamble(src); + assertEquals('\n', stripped[0], "被跳过的行要补一个换行"); + assertEquals("\nlocal x = 1\nx()\n", new String(stripped, StandardCharsets.UTF_8)); + + Proto p = Parser.parseFile(src, "@t".getBytes(StandardCharsets.UTF_8), new StringPool()); + assertEquals(3, p.getFuncline(p.sizecode - 1), "x() 依然在第 3 行"); + } + + /** + * 语法 / 词法错误矩阵(172 行):把 {@code lparser.c} 与 {@code llex.c} 里 + * 每一条报错字符串都走到一遍。 + *

+ * 枚举办法不是凭印象挑用例,而是扫 C 源码的报错站点: + * {@code error_expected} / {@code check_match} 的两种形态、 + * {@code errorlimit}(局部变量、upvalue、构造器项)、 + * {@code attempt to assign to const variable}、 + * {@code jumpscopeerror} / {@code undefgoto} / 重复标签、 + * {@code or '...' expected}、{@code function arguments expected}、 + * {@code unexpected symbol}、{@code unknown attribute}、 + * {@code multiple to-be-closed variables}、 + * {@code cannot use '...' outside a vararg function}、 + * {@code '=' or 'in' expected}、{@code control structure too long}, + * 以及词法层的 unfinished / malformed / hexadecimal digit / missing '{}' / + * UTF-8 value too large / decimal escape too large / invalid escape / + * invalid long string delimiter。 + *

    + *
  • {@code } 与 {@code } 是不带引号的。 + * {@code luaX_token2str} 对 {@code token >= TK_EOS} 直接返回裸串, + * 对保留字与单字符才加 {@code '…'}。所以消息里既有 + * {@code expected} 也有 {@code 'end' expected}, + * 而 {@code near} 后面那个 token 走的是 {@code txtToken} + * (字符串与数字取原文,于是有 {@code near ''\x''} 这种嵌套引号)。
  • + *
  • C 栈溢出是「返回」不是「抛」。深度嵌套的 + * {@code luaE_incCstack} 报错会被 {@code luaD_protectedparser} 兜住, + * 所以 {@code load} 给 {@code nil + "C stack overflow"}; + * 而 {@code load} 自己的参数校验({@code bad argument #2})才是抛。 + * 矩阵里一律走 {@code pcall} 才能把这两种分开。
  • + *
  • {@code control structure too long} 要循环体超过 + * {@code MAXARG_Bx}(262143)条指令才触发,所以那条用例是现生成 + * 的 1MB 源码 —— 参考实现 0.02 秒、本实现 0.6 秒,可以进套件。
  • + *
+ * 敏感性是量过的:把 {@code tokenName} 改成一律加引号红 11 行;把 + * {@code checkMatch} 的同行捷径去掉红 13 行;把 {@code errorLimit} 的 + * {@code "main function"} 改成 {@code "main chunk"} 红 1 行。 + */ + @Test + void syntaxErrorMatrixMatchesReference() { + Diff.assertMatches(""" + -- 语法错误矩阵:C 栈溢出是「抛」出来的(load 不返回 nil+msg),所以一律走 pcall + local function p(tag, src, name) + local ok, f, e = pcall(load, src, name or "=c") + if not ok then print(tag, "R", f) + elseif f ~= nil then print(tag, "OK") + else print(tag, e) end + end + local function q(tag, src) + local ok, f, e = pcall(load, src) + if not ok then print(tag, "R", f) + elseif f ~= nil then print(tag, "OK") + else print(tag, e) end + end + -- A/B/D/E/F/O:统一用 =c 做 chunkname + p("a01", "if x then") + p("a02", "while x do") + p("a03", "for i=1,2 do") + p("a04", "for k,v in t do") + p("a05", "repeat") + p("a06", "do") + p("a07", "function f()") + p("a08", "local function f()") + p("a09", "f(1") + p("a10", "t[1") + p("a11", "t = {1") + p("a12", "f{1") + p("a13", "if x") + p("a14", "while x") + p("a15", "function f") + p("a16", "function f(1)") + p("a17", "function f(a") + p("a18", "function f(a,") + p("a19", "local = 1") + p("a20", "local ,") + p("a21", "local x,") + p("a22", "for i 1,2 do end") + p("a23", "goto") + p("a24", "::") + p("a25", "::1::") + p("a26", "local function") + p("a27", "local x =") + p("a28", "x = 1 +") + p("a29", "t.x") + p("a30", "f()") + p("a31", "local x ") + p("a32", "local x <") + p("a33", "local x = 1\\nx = 2") + p("d02", "local x = 1\\nx, y = 2, 3") + p("d03", "local x = 1\\nlocal function f() x = 2 end") + p("d04", "local x = 1\\nx.y = 2") + p("d05", "local x = 1\\nprint(x)") + p("d06", "local a, b = 1, 2\\nb = 3") + p("d07", "local x = nil") + p("d08", "local x = nil") + p("d09", "local x , y = nil, nil") + p("d10", "local x = nil\\nlocal y = nil") + p("d11", "local x = nil") + p("d12", "local x = nil") + p("d13", "local x = 1\\nlocal y = x\\nlocal x = 2") + p("e01", "goto L") + p("e02", "break") + p("e03", "do break end") + p("e04", "while true do break end\\nbreak") + p("e05", "goto L\\n::L::") + p("e06", "::L::\\ngoto L") + p("e07", "do ::L:: end\\ngoto L") + p("e08", "goto L\\ndo\\nlocal x = 1\\n::L::\\nend") + p("e09", "::L::\\n::L::") + p("e10", "for i=1,2 do goto cont end\\n::cont::") + p("e11", "while true do goto x end") + p("e12", "do goto L ::L:: end") + p("e13", "for i=1,2 do\\ngoto L\\ndo local y = 1 ::L:: end\\nend") + p("e14", "do\\ngoto L\\nlocal x = 1\\n::L::\\nend") + p("e15", "for i=1,2 do\\ngoto cont\\nlocal y = 1\\n::cont::\\nend") + p("e16", "repeat\\ngoto L\\nlocal z = 1\\n::L::\\nuntil true") + p("e17", "::break::") + p("f01", "function f(1) end") + p("f02", "function f(a, 1) end") + p("f03", "function g(x) return ... end") + p("f04", "local function f() return ... end") + p("f05", "1 = 2") + p("f06", "f() = 1") + p("f07", "x + 1 = 2") + p("f08", "return 1, 2 = 3") + p("f09", "local x = 1\\nx() = 2") + p("f10", "print(...)") + p("f11", "return 1 return 2") + p("f12", "do return 1 end return 2") + p("f13", "break\\n") + p("f14", "::L:: goto L") + p("o01", "local x = 1e") + p("o02", "local x = 0x") + p("o03", "local x = 0xg") + p("o04", "local x = 'abc") + p("o05", "local x = \\"abc\\n") + p("o06", "local x = [[abc") + p("o07", "local x = --[[abc") + p("o08", "local x = '\\\\x'") + p("o09", "local x = '\\\\u'") + p("o10", "local x = '\\\\u{110000}'") + p("o11", "local x = '\\\\u{41'") + p("o12", "local x = '\\\\300'") + p("o13", "local x = '\\\\q'") + p("o14", "local x = [==[abc]=]") + p("o15", "local x = @") + p("o16", "local x = $") + p("o17", "local x = 1e999e5") + p("o18", "local x = .") + p("o19", "local x = 0x1p") + p("o20", "local x = 0777e") + p("o21", "local x = `") + p("o22", "local x = '\\\\u{}'") + p("o23", "local x = 1 ..") + p("o24", "local x = '\\\\u{7FFFFFFF}'") + p("o25", "local x = '\\\\x1'") + p("o26", "local x = '\\\\xZZ'") + p("o27", "local x = \\\\1") + p("o28", "local x = \\\\127") + p("o29", "local x = '\\\\u{41}'") + p("o30", "local x = [[]]") + + -- C:errorlimit(超限) + do + local s = "local " + for i = 1, 201 do s = s .. "v" .. i .. "," end + s = s .. "x" + p("c01", s) + end + do + local s = "local function f()\\nlocal " + for i = 1, 201 do s = s .. "v" .. i .. "," end + s = s .. "x\\nend" + p("c02", s) + end + do + -- 外层 200 个局部(a1..a199 + ax),中层 57 个(b1..b56 + bx) + -- 最内层引用全部 = 257 个 upvalue > 255 + local s = "local function f()\\nlocal " + for i = 1, 199 do s = s .. "a" .. i .. "," end + s = s .. "ax\\nreturn function()\\nlocal " + for i = 1, 56 do s = s .. "b" .. i .. "," end + s = s .. "bx\\nreturn function() return " + for i = 1, 199 do s = s .. "a" .. i .. "+" end + s = s .. "ax+" + for i = 1, 56 do s = s .. "b" .. i .. "+" end + s = s .. "bx end\\nend\\nend" + p("c03", s) + end + do + local n = 262150 + p("c04", "for i=1,1 do " .. string.rep("x=1;", n) .. " end") + end + + -- N:嵌套深度(enterlevel / C 栈) + do + local d = 201 + p("n01", string.rep("(", d) .. "1" .. string.rep(")", d)) + p("n02", string.rep("{", d) .. string.rep("}", d)) + p("n03", "return " .. string.rep("-", d) .. "1") + p("n04", "return " .. string.rep("not ", d) .. "x") + p("n05", "return " .. string.rep("(", 50) .. "1" .. string.rep(")", 50)) + p("n06", string.rep("(", 3000) .. "1" .. string.rep(")", 3000)) + p("n07", "return " .. string.rep("{", 201) .. string.rep("}", 201)) + end + + -- P:chunkname 的形状 + q("p01", "x=") + q("p02", "x=\\n") + q("p03", "x= " .. string.rep("1 ", 40)) + q("p04", "local x = 1\\nx =") + q("p05", "\\239\\187\\191x=") + q("p06", "#!/usr/bin/lua\\nx=") + q("p07", "#!/usr/bin/lua\\n") + p("p08", "x=", "=c") + p("p09", "x=", "@f.lua") + p("p10", "x=", "f.lua") + p("p11", "x=", "") + p("p12", "x=", "=a\\nb") + p("p13", "x=", string.rep("z", 100)) + p("p14", "x=", true) + p("p15", "x=", 42) + p("p16", "x=", {}) + do local ok, f = pcall(load, 1) print("p17", ok, type(f)) end + do local ok, f = pcall(load, nil) print("p18", ok, type(f)) end + do local ok, f = pcall(load, "return 1", "=c", "b") print("p19", ok, type(f)) end + do local ok, f = pcall(load, "return 1", "=c", "t", {}) print("p20", ok, type(f)) end + do local ok, f = pcall(load, "return 1", "=c", "x") print("p21", ok, type(f)) end + + -- Q:源码里的 \\0 + q("q01", "x\\0= 1") + q("q02", "x = 1 --\\0y") + p("q03", "x\\0= 1", "=c") + """); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/PatternGridTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/PatternGridTest.java new file mode 100644 index 0000000..5b1a1d2 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/PatternGridTest.java @@ -0,0 +1,327 @@ +package com.github.esrrhs.fakescript.lua; + +import org.junit.jupiter.api.Test; + +import org.junit.jupiter.api.BeforeAll; + +import java.io.File; + +/** + * 模式匹配的全网格差分 —— 阶段 12。 + *

+ * 为什么单开一类:{@code StringLibTest} 里那批模式用例是手挑的,量级只有 + * 几十条,而 {@code lstrlib.c} 的匹配器是本项目里唯一一个靠递归下降 + + * 回溯的模块({@code match} / {@code max_expand} / {@code min_expand} / + * {@code classend} / {@code singlematch}),边界全在字符类与量词的组合上。 + * 手挑用例盖不住的组合,机械铺开一次就能盖住(约定 #39)。 + *

+ * 两片各自成阵: + *

    + *
  • 片一:字符类 × 量词 × 构造 × 五个出口。20 个字符类(含 + * {@code %g}/{@code %G} 这种 5.4 才有的)乘 5 种量词,再叠上集合、 + * {@code %b}、{@code %f}、捕获、位置捕获、锚,共 188 个模式; + * 18 个目标串(含 NUL、{@code 0xFF}、空串、{@code %}、{@code ]}); + * 出口是 {@code find}/{@code match}/{@code gmatch}/{@code gsub} + * 四种({@code gsub} 又分「替换串」与「替换函数」两路)。约 16900 行。
  • + *
  • 片二:{@code init} / {@code plain} / 替换串 / 畸形模式 / 参数类型。 + * {@code init} 从 {@code -5} 扫到 {@code 8} 外加浮点与数字字符串, + * {@code plain} 给八种值(含 {@code 0}、{@code ''}、{@code {}}), + * 畸形模式 30 种(含 33 层捕获、{@code %f} 缺 {@code [}), + * 参数类型 8 种 × 四个函数。
  • + *
+ *

+ * 两片都带一遍协程第二遍:同一批语料塞进 {@code coroutine.create} + * 再跑一遍;片二末尾还试了「在 {@code gsub} 的回调里 yield」——那是从 + * 没有续体的 C 帧里 yield,参考实现报 + * {@code attempt to yield across a C-call boundary}(约定 #81)。 + *

+ * 这一片一条分歧都没咬出来——铺之前担心、铺完确认干净,这本身就是结论: + * 匹配器这一块可以放心。 + */ +class PatternGridTest { + + @BeforeAll + static void prepare() { + new File("target/rt").mkdirs(); + } + + /** 片一:字符类 x 量词 x 构造 x 五个出口(约 16900 行)。 */ + @Test + void classQuantifierGridMatchesReference() { + Diff.assertMatches(""" + +-- 模式匹配全网格:字符类 x 量词 x 构造 x 五个出口 +-- 语料在 Lua 里自己拼(不是把 16000 行展开写死),但覆盖面与展开版一致。 +local HEX = "0123456789ABCDEF" +local function hb(b) return HEX:sub((b // 16) + 1, (b // 16) + 1) .. HEX:sub((b % 16) + 1, (b % 16) + 1) end +local function esc(s) + return (s:gsub(".", function(c) + local b = c:byte() + if b < 32 or b > 126 then return "<" .. hb(b) .. ">" end + return c + end)) +end +local function show(v) + local t = type(v) + if t == "string" then return "#" .. #v .. "[" .. esc(v) .. "]" + elseif t == "number" then + if math.type(v) == "integer" then return "i" .. tostring(v) end + return "f" .. tostring(v) + end + return t .. "(" .. tostring(v):gsub("0x%x+", "PTR") .. ")" +end +local function P(tag, fn) + local r = table.pack(pcall(fn)) + local out = tag + for i = 1, r.n do out = out .. " | " .. show(r[i]) end + print(out .. " | n=" .. r.n) +end +local function gmatchN(s, p, n) + local out = {} + local it = string.gmatch(s, p) + for _ = 1, n do + local r = table.pack(it()) + if r.n == 0 then break end + local one = "" + for j = 1, r.n do one = one .. ((j > 1) and "," or "") .. show(r[j]) end + out[#out + 1] = one + end + return table.concat(out, ";") +end + +local SUBJ = { "", "a", "abc", "a1b2", " x ", "A_z9", "\\0ab", "\\255\\254", "()", + "(a(b)c)", "a.b", "%", "-", "]", "aBc123 \\t", "\\255", "a\\0", "THE (quick) fox" } + +local PATS = {} +-- 1) 每个字符类 x 每个量词 +for _, c in ipairs({ "a", "A", "c", "C", "d", "D", "g", "G", "l", "L", + "p", "P", "s", "S", "u", "U", "w", "W", "x", "X" }) do + for _, q in ipairs({ "", "*", "+", "-", "?" }) do PATS[#PATS + 1] = "%" .. c .. q end +end +-- 2) 裸字符与 magic 的转义 +for _, ch in ipairs({ ".", "a", "1", " " }) do + for _, q in ipairs({ "", "*", "+", "-", "?" }) do PATS[#PATS + 1] = ch .. q end +end +for _, e in ipairs({ "%%", "%.", "%(", "%)", "%[", "%]", "%*", "%+", "%-", "%?", "%^", "%$" }) do + PATS[#PATS + 1] = e +end +-- 3) 集合 +for _, e in ipairs({ "[abc]", "[^abc]", "[a-c]", "[^a-c]", "[]]", "[]a]", "[a-]", "[-a]", + "[%a]", "[^%a]", "[%a%d]", "[a-c0-9]", "[[", "[]", "[^]", + "[a-\\255]", "[%]]", "[%%]", "[-]", "[^-]" }) do + PATS[#PATS + 1] = e +end +-- 4) %b / %f +for _, e in ipairs({ "%b()", "%b<>", "%b[]", "%bab", "%bx", "%b", "%f[%a]", "%f[%w]", + "%f[a]", "%f[^a]", "%f[%a]%a+", "%f[]", "%f", "%f[%a" }) do + PATS[#PATS + 1] = e +end +-- 5) 捕获与位置捕获 +for _, e in ipairs({ "(a)", "(a*)", "()", "(())", "(%a)", "()a()", "(a)(b)", "(a-)" }) do + PATS[#PATS + 1] = e +end +-- 6) 锚与组合 +for _, e in ipairs({ "^a", "a$", "^a$", "^", "$", "^$", ".*", ".-", ".+", "a*b", "a-b", + "%a+%d", "%a*%d*", "(.)%1" }) do + PATS[#PATS + 1] = e +end + +print("patterns " .. #PATS) +for si = 1, #SUBJ do + local s = SUBJ[si] + print("=== S" .. si .. " ===") + for pi = 1, #PATS do + local p = PATS[pi] + local tag = "P" .. pi + P(tag, function() return string.find(s, p) end) + P(tag, function() return string.match(s, p) end) + P(tag, function() return gmatchN(s, p, 12) end) + P(tag, function() return string.gsub(s, p, "<%0>") end) + P(tag, function() return string.gsub(s, p, function(...) return "|" .. select("#", ...) .. "|" end) end) + end +end + """); + } + + /** 片二:init / plain / 替换串 / 畸形模式 / 参数类型(约 2400 行)。 */ + @Test + void initPlainReplacementAndMalformedGridMatchesReference() { + Diff.assertMatches(""" + +-- 模式匹配网格 II:init / plain、替换串、畸形模式、协程第二遍 +local HEX = "0123456789ABCDEF" +local function hb(b) return HEX:sub((b // 16) + 1, (b // 16) + 1) .. HEX:sub((b % 16) + 1, (b % 16) + 1) end +local function esc(s) + return (s:gsub(".", function(c) + local b = c:byte() + if b == 91 or b == 93 then return "<" .. hb(b) .. ">" end + if b < 32 or b > 126 then return "<" .. hb(b) .. ">" end + return c + end)) +end +local function show(v) + local t = type(v) + if t == "string" then return "#" .. #v .. "[" .. esc(v) .. "]" + elseif t == "number" then + if math.type(v) == "integer" then return "i" .. tostring(v) end + return "f" .. tostring(v) + end + -- 约定 #42:地址一律归一成类型 + return t .. "(" .. tostring(v):gsub("0x%x+", "PTR") .. ")" +end +local function P(tag, fn) + local r = table.pack(pcall(fn)) + local out = tag + for i = 1, r.n do out = out .. " | " .. show(r[i]) end + print(out .. " | n=" .. r.n) +end + +print("=== A find 的 init 网格 ===") +local SUBJ = { "", "a", "abc", "a1b2", "\\0ab", "\\255\\254", "aaa" } +local PATS = { "a", "%a", "%a+", "", "^a", "a$", ".", "%d", "[a-c]" } +for i = 1, #SUBJ do + local s = SUBJ[i] + for j = 1, #PATS do + local p = PATS[j] + for init = -5, 8 do + P("A " .. i .. " " .. j .. " init=" .. init, function() + return string.find(s, p, init) + end) + end + -- plain 三态:false / true / nil,以及非布尔 + for _, plain in ipairs({ "false", "true", "nil", "0", "1", "''", "'x'", "{}" }) do + P("A plain " .. i .. " " .. j .. " " .. plain, function() + return string.find(s, p, 1, load("return " .. plain)()) + end) + end + -- init 是浮点 / 数字字符串 + for _, iv in ipairs({ "1.0", "2.5", "-1.0", "'2'", "'0'", "0x7fffffff", "math.maxinteger", "math.mininteger" }) do + P("A initf " .. i .. " " .. j .. " " .. iv, function() + return string.find(s, p, load("return " .. iv)()) + end) + end + end +end + +print("=== B gsub 的替换串 ===") +local REPL = { "%0", "%1", "%2", "%9", "%%", "%", "%x", "%-", "% ", "a%0b", + "%0%0", "[%1]", "%1x", "1", "true", "false" } +for i = 1, #SUBJ do + local s = SUBJ[i] + for j = 1, #REPL do + local r = REPL[j] + P("B " .. i .. " " .. j .. " " .. esc(r), function() + return string.gsub(s, "(a)", r) + end) + P("B nocap " .. i .. " " .. j .. " " .. esc(r), function() + return string.gsub(s, "a", r) + end) + end +end + +print("=== C gsub 的第三个参数:表 / 函数 / 布尔 / nil / 数字 ===") +for i = 1, #SUBJ do + local s = SUBJ[i] + P("C nil " .. i, function() return string.gsub(s, "%a", nil) end) + P("C false " .. i, function() return string.gsub(s, "%a", false) end) + P("C true " .. i, function() return string.gsub(s, "%a", true) end) + P("C 0 " .. i, function() return string.gsub(s, "%a", 0) end) + P("C 42 " .. i, function() return string.gsub(s, "%a", 42) end) + P("C tbl " .. i, function() + return string.gsub(s, "%a", setmetatable({}, {__index = function(_, k) return "K" .. k end})) + end) + P("C tblnil " .. i, function() + return string.gsub(s, "%a", setmetatable({}, {__index = function(_, k) return nil end})) + end) + P("C fnnil " .. i, function() return string.gsub(s, "%a", function() return nil end) end) + P("C fnfalse " .. i, function() return string.gsub(s, "%a", function() return false end) end) + P("C fnnum " .. i, function() return string.gsub(s, "%a", function() return 42 end) end) + P("C fnerr " .. i, function() return pcall(string.gsub, s, "%a", function() error("boom") end) end) + P("C fnerr2 " .. i, function() return pcall(string.gsub, s, "%a", function() error({}) end) end) +end + +print("=== D gsub 的 max n ===") +for _, n in ipairs({ "-1", "0", "1", "2", "100", "1.0", "2.5", "'2'", "math.maxinteger", "math.mininteger" }) do + P("D n=" .. n, function() return string.gsub("aaa", "a", "b", load("return " .. n)()) end) + P("D n2=" .. n, function() return string.gsub("abcabc", "%a", function(c) return c:upper() end, load("return " .. n)()) end) +end + +print("=== E 畸形模式 ===") +local BAD = { + "%", "a%", "%f", "%f[", "%f[%a", "%f[]", "%fa", "(", ")", "(a", "a)", + "[", "[a", "[^", "[a-", "%1", "(a)%2", "%2(a)", "%0", + "()%1", "(())%2", "%b", "%bx", "%b()%b", + "(((((((((((((((((((((((((((((((((((", -- 32 层 + string.rep("(", 33), + "[" .. string.rep("a", 200) .. "]", + "%\\255", "%\\0", "[\\255]", "(%255)", +} +for i = 1, #BAD do + local p = BAD[i] + P("E find " .. i, function() return string.find("abc", p) end) + P("E match " .. i, function() return string.match("abc", p) end) + P("E gsub " .. i, function() return string.gsub("abc", p, "x") end) + P("E gmatch " .. i, function() + local it = string.gmatch("abc", p) + return it() + end) +end + +print("=== F 参数类型错误 ===") +local ARGS = { "nil", "true", "42", "1.5", "'x'", "{}", "print", "coroutine.create(function() end)" } +for i = 1, #ARGS do + local a = ARGS[i] + P("F find1 " .. a, function() return pcall(string.find, load("return " .. a)()) end) + P("F find2 " .. a, function() return pcall(string.find, "abc", load("return " .. a)()) end) + P("F match1 " .. a, function() return pcall(string.match, load("return " .. a)()) end) + P("F gmatch1 " .. a, function() return pcall(string.gmatch, load("return " .. a)()) end) + P("F gsub1 " .. a, function() return pcall(string.gsub, load("return " .. a)()) end) + P("F gsub2 " .. a, function() return pcall(string.gsub, "abc", load("return " .. a)()) end) +end +P("F find0", function() return pcall(string.find) end) +P("F match0", function() return pcall(string.match) end) +P("F gmatch0", function() return pcall(string.gmatch) end) +P("F gsub0", function() return pcall(string.gsub) end) +P("F gsub3", function() return pcall(string.gsub, "abc") end) + +print("=== G 协程第二遍:整片重跑 ===") +-- 约定第四条:同一批语料塞进 coroutine.create 再跑一遍 +local function body() + local out = {} + for i = 1, #SUBJ do + local s = SUBJ[i] + for j = 1, #PATS do + out[#out + 1] = show(string.find(s, PATS[j])) + out[#out + 1] = show(select(2, string.gsub(s, PATS[j], "<%0>"))) + end + end + for i = 1, #BAD do + out[#out + 1] = esc(select(2, pcall(string.match, "abc", BAD[i]))) + end + return table.concat(out, ";") +end +P("G main", function() return body() end) +P("G co", function() + local c = coroutine.create(function() coroutine.yield(body()) end) + return select(2, coroutine.resume(c)) +end) +P("G wrap", function() + local w = coroutine.wrap(function() coroutine.yield(body()) end) + return w() +end) +-- 从 C 函数(gsub 的回调)里 yield:C 帧没有续体,这正是约定 #81 +P("G yieldInGsub", function() + return pcall(string.gsub, "abc", "%a", function(c) coroutine.yield(c) return c end) +end) +P("G yieldInGsubCo", function() + local co = coroutine.create(function() + return string.gsub("abc", "%a", function(c) coroutine.yield(c) return c end) + end) + local r = table.pack(coroutine.resume(co)) + local out = tostring(r[1]) + for i = 2, r.n do out = out .. " | " .. show(r[i]) end + return out +end) + """); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/ProtoDump.java b/src/test/java/com/github/esrrhs/fakescript/lua/ProtoDump.java new file mode 100644 index 0000000..dfc81c0 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/ProtoDump.java @@ -0,0 +1,95 @@ +package com.github.esrrhs.fakescript.lua; + +import java.util.Locale; + +/** + * 把 {@link Proto} 转成与 {@code fkdump}(参考实现侧那个小转储器) + * 逐字节相同的文本。 + *

+ * 为什么自造格式而不用 {@code luac -l}:后者是给人看的,每个 opcode 打的字段 + * 不一样、还夹着 {@code ; _ENV "print"} 这类注释,要反解回原始字段就得把 + * 那个大 switch 抄一遍——抄错一处,比对结论就废了。这里对所有 opcode + * 统一打印全部字段,两边各写一遍也不会写歪。 + *

+ * 浮点常量打位模式的十六进制:十进制格式化在 C 与 Java 之间差异极大 + * ({@code %g} 去不去尾零、{@code NaN} 怎么打),位模式没有这些分歧。 + */ +final class ProtoDump { + + private ProtoDump() { + } + + static String dump(Proto f) { + StringBuilder sb = new StringBuilder(); + dumpInto(f, 0, 0, sb); + return sb.toString(); + } + + private static void dumpInto(Proto f, int level, int index, StringBuilder sb) { + sb.append(String.format(Locale.ROOT, + "PROTO level=%d index=%d linedefined=%d lastlinedefined=%d\n", + level, index, f.linedefined, f.lastlinedefined)); + sb.append(String.format(Locale.ROOT, + " numparams=%d isvararg=%d maxstacksize=%d nups=%d nconsts=%d nsubs=%d\n", + f.numparams, f.isVararg ? 1 : 0, f.maxstacksize, + f.sizeupvalues, f.sizek, f.sizep)); + for (int i = 0; i < f.sizecode; i++) { + int inst = f.code[i]; + sb.append(String.format(Locale.ROOT, + " I %d %s a=%d b=%d c=%d k=%d bx=%d sbx=%d ax=%d sb=%d sc=%d sj=%d line=%d\n", + i, OpCode.NAMES[OpCode.getOpCode(inst)], + OpCode.getArgA(inst), OpCode.getArgB(inst), OpCode.getArgC(inst), + OpCode.getArgK(inst), OpCode.getArgBx(inst), OpCode.getArgSBx(inst), + OpCode.getArgAx(inst), OpCode.getArgSB(inst), OpCode.getArgSC(inst), + OpCode.getArgSJ(inst), f.getFuncline(i))); + } + for (int i = 0; i < f.sizek; i++) { + sb.append(" K ").append(i).append(' ').append(constant(f.k[i])).append('\n'); + } + for (int i = 0; i < f.sizeupvalues; i++) { + Proto.Upvaldesc up = f.upvalues[i]; + sb.append(String.format(Locale.ROOT, " UP %d instack=%d idx=%d kind=%d name=", + i, up.instack ? 1 : 0, up.idx, up.kind)); + sb.append(up.name == null ? "?" : escape(up.name)); + sb.append('\n'); + } + for (int i = 0; i < f.sizep; i++) { + dumpInto(f.p[i], level + 1, i, sb); + } + } + + private static String constant(LuaValue v) { + if (v.isNil()) { + return "nil"; + } + if (v.isBoolean()) { + return "bool:" + (v.asBoolean() ? 1 : 0); + } + if (v.isInteger()) { + return "int:" + v.i; + } + if (v.isFloat()) { + return String.format(Locale.ROOT, "flt:%016x", Double.doubleToRawLongBits(v.n)); + } + if (v.isString()) { + return "str:" + escape(v.asString()); + } + return "other"; + } + + /** 与 {@code fkdump.c} 的 {@code printString} 同一套转义 */ + private static String escape(LuaString s) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + int c = s.byteAt(i) & 0xFF; + if (c == '\\' || c == '"') { + sb.append(String.format(Locale.ROOT, "\\%02x", c)); + } else if (c >= 0x20 && c < 0x7f) { + sb.append((char) c); + } else { + sb.append(String.format(Locale.ROOT, "\\%02x", c)); + } + } + return sb.toString(); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/ReferenceLua.java b/src/test/java/com/github/esrrhs/fakescript/lua/ReferenceLua.java new file mode 100644 index 0000000..6376d07 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/ReferenceLua.java @@ -0,0 +1,429 @@ +package com.github.esrrhs.fakescript.lua; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +/** + * 参考实现的驱动:调用一个真实的 {@code lua5.4} 二进制跑脚本。 + *

+ * 为什么需要它:这是本项目的主闸门。「看起来像 Lua」没有价值, + * 唯一能证明「真的是 Lua」的办法是同一份脚本在两边跑出逐字节相同的 + * stdout。上游自带的测试套件({@code lua-5.4.x/tests})再刁钻,也覆盖不到 + * 我们自己实现里的那些「碰巧对了」的地方。 + *

+ * 二进制的查找顺序: + *

    + *
  1. 环境变量 {@code LUA54_BIN}(CI 与本地开发都用它指向确切的路径)
  2. + *
  3. 仓库内 {@code target/lua54/src/lua}(CI 构建脚本的默认落点)
  4. + *
  5. PATH 上的 {@code lua5.4} / {@code lua}
  6. + *
+ * 找到之后还会跑一次 {@code -v} 校验版本确实是 5.4:本机上那个 {@code lua} + * 很可能是 5.5 或其他版本,拿错版本当参考会得出错误的「不一致」结论, + * 那比没有参考更糟。 + */ +final class ReferenceLua { + + private static final String ENV_BIN = "LUA54_BIN"; + + private static String cached; + private static boolean lookedUp; + + private ReferenceLua() { + } + + /** 找到可用的 lua5.4 二进制;找不到返回 null */ + static synchronized String find() { + if (lookedUp) { + return cached; + } + lookedUp = true; + + List candidates = new ArrayList<>(); + String env = System.getenv(ENV_BIN); + if (env != null && !env.isEmpty()) { + candidates.add(env); + } + candidates.add("target/lua54/src/lua"); + candidates.add("/usr/local/bin/lua5.4"); + candidates.add("/opt/homebrew/bin/lua5.4"); + candidates.add("lua5.4"); + candidates.add("lua"); + + for (String c : candidates) { + if (isLua54(c)) { + cached = c; + return cached; + } + } + return null; + } + + /** 校验二进制存在且版本是 5.4 */ + private static boolean isLua54(String bin) { + try { + ProcessBuilder pb; + if (bin.contains(File.separator) || bin.startsWith("target/")) { + File f = new File(bin); + if (!f.canExecute()) { + return false; + } + pb = new ProcessBuilder(f.getAbsolutePath(), "-v"); + } else { + pb = new ProcessBuilder(bin, "-v"); + } + Process p = pb.start(); + String out = readAll(p.getInputStream()) + readAll(p.getErrorStream()); + p.waitFor(); + return out.contains("Lua 5.4"); + } catch (Exception e) { + return false; + } + } + + /** + * 找到可用的 {@code luac5.4};找不到返回 {@code null}。 + *

+ * 它是「二进制 chunk 双向兼容」那个用例的编译器:本实现要跑参考实现 + * 编出来的 chunk,而不是自己编出来的——后者等于用自己的编译器给自己 + * 判卷。{@code make} 默认会把 {@code luac} 和 {@code lua} 一起编出来, + * 所以 CI 上 {@code target/lua54/src/luac} 一定在。 + */ + static String findLuac() { + return findSibling("luac", "LUAC54_BIN", "luac5.4", "luac"); + } + + /** + * 按同一套规则找「参考实现旁边的另一个工具」。 + * + * @param sibling 与 {@code lua} 同目录时它的文件名 + * @param env 指定它的环境变量 + * @param names PATH 上可能的名字 + */ + private static String findSibling(String sibling, String env, String... names) { + String e = System.getenv(env); + if (e != null && !e.isEmpty() && canRun(e)) { + return e; + } + String lua = find(); + if (lua != null) { + java.io.File f = new java.io.File(new java.io.File(lua).getParentFile(), sibling); + if (f.canExecute() && canRun(f.getAbsolutePath())) { + return f.getAbsolutePath(); + } + } + for (String n : names) { + if (canRun(n)) { + return n; + } + } + return null; + } + + private static boolean canRun(String bin) { + try { + java.io.File f = new java.io.File(bin); + ProcessBuilder pb = new ProcessBuilder( + f.isAbsolute() ? bin : (f.getPath().contains(java.io.File.separator) + ? f.getAbsolutePath() : bin), + "-v"); + pb.redirectErrorStream(true); + Process p = pb.start(); + String out = readAll(p.getInputStream()); + p.waitFor(); + return out.contains("Lua 5.4"); + } catch (Exception ex) { + return false; + } + } + + /** + * 用参考实现自己的编译器把源码编成二进制 chunk。 + * + * @return 产物路径;编译失败返回 {@code null} + */ + static java.io.File compile(byte[] source, String outPath) { + String luac = findLuac(); + if (luac == null) { + return null; + } + try { + java.nio.file.Path out = Paths.get(outPath); + java.nio.file.Path parent = out.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + java.nio.file.Path src = Paths.get(outPath + ".lua"); + Files.write(src, source); + ProcessBuilder pb = new ProcessBuilder(luac, "-o", outPath, src.toString()); + pb.redirectErrorStream(true); + Process p = pb.start(); + readAll(p.getInputStream()); + int code = p.waitFor(); + Files.deleteIfExists(src); + return (code == 0) ? out.toFile() : null; + } catch (Exception e) { + return null; + } + } + + /** 跑一段脚本,返回退出码与合并后的输出 */ + static Result run(String script) { + String bin = find(); + if (bin == null) { + throw new IllegalStateException("no lua5.4 reference binary; set " + ENV_BIN); + } + try { + File tmp = File.createTempFile("fkconf", ".lua"); + Files.write(tmp.toPath(), script.getBytes(StandardCharsets.UTF_8)); + ProcessBuilder pb = new ProcessBuilder(new File(bin).getAbsolutePath(), + tmp.getAbsolutePath()); + pb.redirectErrorStream(true); + Process p = pb.start(); + String out = readAll(p.getInputStream()); + int code = p.waitFor(); + tmp.delete(); + return new Result(code, out); + } catch (Exception e) { + throw new IllegalStateException("failed to run reference lua: " + e, e); + } + } + + /** + * 跑一段脚本,stdin 由调用方给。 + *

+ * 只有 {@code debug.debug}({@code ldblib.c: db_debug})会读 stdin,而它 + * 是一路读到 EOF 或 {@code cont} 的循环——不喂 stdin 就只能验到「打完 + * 提示符就退出」这一种形状,循环体、出错分支、{@code cont} 三条路全验不到。 + * + * @param stdin 喂给脚本的字节;{@code null} 表示 {@code /dev/null} + */ + static Result runWithStdin(byte[] script, byte[] stdin) { + String bin = find(); + if (bin == null) { + throw new IllegalStateException("no lua5.4 reference binary; set " + ENV_BIN); + } + try { + File tmp = File.createTempFile("fkstdin", ".lua"); + Files.write(tmp.toPath(), script); + ProcessBuilder pb = new ProcessBuilder(bin, tmp.getAbsolutePath()); + pb.redirectErrorStream(true); + File in = null; + if (stdin == null) { + pb.redirectInput(new File("/dev/null")); + } else { + in = File.createTempFile("fkstdin", ".in"); + Files.write(in.toPath(), stdin); + pb.redirectInput(in); + } + Process p = pb.start(); + String out = readAll(p.getInputStream()); + int code = p.waitFor(); + Files.deleteIfExists(tmp.toPath()); + if (in != null) { + Files.deleteIfExists(in.toPath()); + } + return new Result(code, out); + } catch (Exception e) { + throw new IllegalStateException("failed to run reference lua: " + e, e); + } + } + + /** 跑一个已存在的脚本文件 */ + static Result runFile(String path) { + return runFile(path, null); + } + + /** + * 跑一个已存在的脚本文件。 + * + * @param cwd 工作目录;上游测试套件靠它把 {@code ./?.lua} 落在用例目录上, + * 从而 {@code require} 到同目录里的同伴文件 + */ + static Result runFile(String path, File cwd) { + String bin = find(); + if (bin == null) { + throw new IllegalStateException("no lua5.4 reference binary; set " + ENV_BIN); + } + try { + ProcessBuilder pb = new ProcessBuilder(new File(bin).getAbsolutePath(), path); + if (cwd != null) { + pb.directory(cwd); + } + pb.redirectErrorStream(true); + /* + * stdin 定向到 /dev/null:本实现的 LuaState#in 默认是空流, + * 两边必须一致,否则 io.read() 一边给 nil 另一边却阻塞在 + * 继承来的终端上。 + */ + pb.redirectInput(new File("/dev/null")); + Process p = pb.start(); + String out = readAll(p.getInputStream()); + int code = p.waitFor(); + return new Result(code, out); + } catch (Exception e) { + throw new IllegalStateException("failed to run reference lua: " + e, e); + } + } + + /** + * 把原始字节写成脚本文件并跑它。 + *

+ * 不走 {@link #run(String)} 是因为后者按 UTF-8 编码写文件:词法用例里会有 + * 非法 UTF-8 字节({@code \x80} 之类),而参考实现读的是字节流, + * 用 Java String 中转会把用例改写成另一份输入。 + *

+ * 路径要短:参考实现的 {@code luaO_chunkid} 会把超过 {@code LUA_IDSIZE} + * 的源名截成 {@code ...xxx},{@code java.io.tmpdir} 下那串随机临时文件名 + * 必然超长,比对就没法做了。 + */ + static FileResult runBytes(byte[] content, String path) { + String bin = find(); + if (bin == null) { + throw new IllegalStateException("no lua5.4 reference binary; set " + ENV_BIN); + } + try { + java.nio.file.Path p = Paths.get(path); + java.nio.file.Path parent = p.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + Files.write(p, content); + Result r = runFile(path); + return new FileResult(r, path); + } catch (Exception e) { + throw new IllegalStateException("failed to run reference lua: " + e, e); + } + } + + /** + * 用参考实现的编译器把脚本编成字节码,再交给 {@code fkdump} 转储。 + *

+ * 二进制的来源(优先级从高到低):环境变量 {@code FKDUMP_BIN}、与 + * {@code lua5.4} 二进制同目录下的 {@code fkdump}、PATH 上的 {@code fkdump}。 + * 找不到就返回 {@code null},测试跳过——CI 的 conformance job 会把它编出来。 + */ + static String dumpProto(byte[] content, String path) { + String bin = findFkdump(); + if (bin == null) { + return null; + } + try { + java.nio.file.Path p = Paths.get(path); + java.nio.file.Path parent = p.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + Files.write(p, content); + ProcessBuilder pb = new ProcessBuilder(bin, path); + pb.redirectErrorStream(true); + Process pr = pb.start(); + String out = readAll(pr.getInputStream()); + pr.waitFor(); + return out; + } catch (Exception e) { + throw new IllegalStateException("failed to run fkdump: " + e, e); + } + } + + private static String fkdumpCached; + private static boolean fkdumpLookedUp; + + static synchronized String findFkdump() { + if (fkdumpLookedUp) { + return fkdumpCached; + } + fkdumpLookedUp = true; + List candidates = new ArrayList<>(); + String env = System.getenv("FKDUMP_BIN"); + if (env != null && !env.isEmpty()) { + candidates.add(env); + } + String lua = find(); + if (lua != null && lua.contains(File.separator)) { + candidates.add(new File(lua).getParent() + File.separator + "fkdump"); + } + candidates.add("fkdump"); + for (String c : candidates) { + File f = new File(c.contains(File.separator) ? c : c); + if (f.canExecute()) { + fkdumpCached = f.getAbsolutePath(); + return fkdumpCached; + } + } + return null; + } + + static final class FileResult { + final Result result; + final String path; + + FileResult(Result result, String path) { + this.result = result; + this.path = path; + } + } + + private static String readAll(InputStream in) { + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] buf = new byte[8192]; + int n; + while ((n = in.read(buf)) > 0) { + bos.write(buf, 0, n); + } + /* + * 用 Latin-1 而不是 UTF-8 解码:Latin-1 是字节↔字符的一一映射, + * 比对因此是逐字节的。按 UTF-8 解码的话,任何非法字节都会变成 + * U+FFFD —— 两边同时变成 U+FFFD,于是「一边输出 0x80、另一边输出 + * 0xFF」这种真分歧被判成一致。差分闸门对整片非 UTF-8 输出是瞎的, + * 这件事本身就是一类假绿。 + */ + return new String(bos.toByteArray(), StandardCharsets.ISO_8859_1); + } catch (Exception e) { + return ""; + } + } + + static final class Result { + final int exitCode; + final String output; + + Result(int exitCode, String output) { + this.exitCode = exitCode; + this.output = output; + } + + @Override + public String toString() { + return "exit=" + exitCode + "\n" + output; + } + } + + /** 让测试能跳过而不是失败(找不到参考二进制时) */ + static boolean available() { + return find() != null; + } + + /** 供诊断:把查找路径打出来 */ + static String describe() { + String bin = find(); + return bin == null ? ("") : bin; + } + + /** 未使用,仅为让 Files/Paths 的 import 保持有效(实际用于 run 的临时文件) */ + static boolean exists(String p) { + return Files.exists(Paths.get(p)); + } + + /** 未使用,占位以保持与 OutputStream 相关的签名清晰 */ + @SuppressWarnings("unused") + private static void unused(OutputStream os) { + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/RuntimeTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/RuntimeTest.java new file mode 100644 index 0000000..d260aa7 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/RuntimeTest.java @@ -0,0 +1,12952 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * 运行期差分 —— 阶段 3 的主闸门。 + *

+ * 与阶段 1/2 的字节码闸门互补:那边证明「编译出来的东西和参考实现一样」, + * 这边证明「跑起来的结果和参考实现一样」。同一份脚本分别交给两边, + * 逐字节比对 stdout;出错时比对第一行错误信息(回溯依赖调试信息的层数, + * 且程序名前缀是二进制路径,两边必然不同)。 + *

+ * 脚本路径固定为 {@code target/rt/case.lua}:参考实现的 {@code luaO_chunkid} + * 会把过长的源名从左边截成 {@code ...xxx},用随机临时文件名就没法比对了。 + */ +class RuntimeTest { + + @BeforeAll + static void requireReference() { + assumeTrue(ReferenceLua.available(), + "no lua5.4 reference binary; set LUA54_BIN. found: " + ReferenceLua.describe()); + } + + /** 差分用例。每条都会在两边各跑一遍 */ + static List scripts() { + List s = new ArrayList<>(); + + // 输出与类型 + s.add("print(1, 2.0, \"x\", true, nil, false)"); + s.add("print(1.0, 0.0, -0.0, 100.0, 1e15, 1e16, 1e300, 0.1, 1/3)"); + s.add("print(0/0, 1/0, -1/0)"); + s.add("print(type(nil), type(true), type(1), type(1.0), type('a'), type({}), type(print))"); + s.add("print(math.type(1), math.type(1.0), math.type('1'), math.type(nil))"); + + // 算术与整数/浮点子类型 + s.add("print(1+2, 3-1, 2*3, 7/2, 7//2, 7%3, 2^10)"); + s.add("print(-7//2, -7%3, 7//-2, 7%-3)"); + s.add("print(1 == 1.0, math.type(1), tostring(1), tostring(1.0))"); + s.add("print(1//0)"); + + // 位运算 + s.add("print(5 & 3, 5 | 3, 5 ~ 3, ~0, 1 << 3, -8 >> 2)"); + + // 字符串 + s.add("print('a' .. 'b' .. 1 .. 2.0)"); + s.add("print(#('abc'), #(''))"); + s.add("print('abc' < 'abd', 'Z' < 'a', 'a' < 'ab')"); + s.add("local s = '10' print(s + 1, s + 1.0, -s)"); + + // 表 + s.add("local t = {} t[1] = 'a' t[2] = 'b' print(#t, t[1], t[2])"); + s.add("local t = {1, 2, 3, x = 'y'} print(#t, t.x, t[3])"); + s.add("local t = {} t[1.0] = 'v' print(t[1], #t)"); + s.add("local t = {} for i = 1, 10 do t[i] = i * i end print(#t, t[10])"); + + // 控制流 + s.add("local s = 0 for i = 1, 10 do s = s + i end print(s)"); + s.add("local s = 0 for i = 10, 1, -2 do s = s + i end print(s)"); + s.add("local s = 0 for i = 1.0, 2.0, 0.5 do s = s + 1 end print(s)"); + s.add("for i = 1, 0 do print('never') end print('done')"); + s.add("local i = 0 while i < 5 do i = i + 1 end print(i)"); + s.add("local i = 0 repeat i = i + 1 until i >= 3 print(i)"); + s.add("local t = {'a','b','c'} local r = '' for _, v in ipairs(t) do r = r .. v end print(r)"); + s.add("local t = {a=1, b=2} local n = 0 for k, v in pairs(t) do n = n + v end print(n)"); + s.add("local x = 5 if x > 3 then print('big') elseif x > 1 then print('mid') else print('small') end"); + s.add("for i = 1, 5 do if i == 3 then goto cont end print(i) ::cont:: end"); + + // 函数与闭包 + s.add("local function f(a, b) return a + b end print(f(1, 2), f(1))"); + s.add("local function counter() local n = 0 return function() n = n + 1 return n end end" + + " local c = counter() print(c(), c(), c())"); + s.add("local function outer() local x = 10 return function() return x end," + + " function(v) x = v end end local g, s = outer() print(g()) s(99) print(g())"); + s.add("local function fmt(...) local a, b = ... return a, b end print(fmt(1, 2, 3))"); + s.add("local function va(...) return select('#', ...) end print(va(), va(1), va(1,2,3))"); + s.add("print((function() return 1, 2, 3 end)())"); + s.add("local function rec(n) if n <= 0 then return 0 end return n + rec(n - 1) end print(rec(100))"); + s.add("local function tail(n, acc) if n == 0 then return acc end return tail(n - 1, acc + n) end" + + " print(tail(1000, 0))"); + + // 元表 + s.add("local mt = {__index = function(t, k) return 'def:' .. k end}" + + " local t = setmetatable({}, mt) print(t.x, t.y)"); + s.add("local mt = {__add = function(a, b) return 'added' end}" + + " local t = setmetatable({}, mt) print(t + 1, 1 + t)"); + s.add("local log = {} local mt = {__newindex = function(t, k, v) log[#log+1] = k end}" + + " local t = setmetatable({}, mt) t.a = 1 t.b = 2 print(#log, log[1], log[2])"); + s.add("local mt = {__index = {greet = 'hi'}} local t = setmetatable({}, mt) print(t.greet)"); + s.add("local mt = {__eq = function(a, b) return true end}" + + " local a, b = setmetatable({}, mt), setmetatable({}, mt) print(a == b, a == {})"); + s.add("local mt = {__lt = function(a, b) return true end}" + + " local a, b = setmetatable({}, mt), setmetatable({}, mt) print(a < b, a <= b)"); + s.add("local mt = {__le = function(a, b) return false end}" + + " local a, b = setmetatable({}, mt), setmetatable({}, mt) print(a <= b)"); + s.add("local mt = {__len = function() return 42 end} print(#setmetatable({}, mt))"); + s.add("local mt = {__call = function(self, x) return x * 2 end}" + + " print(setmetatable({}, mt)(21))"); + s.add("local mt = {__tostring = function() return 'CUSTOM' end} print(setmetatable({}, mt))"); + s.add("local t = setmetatable({}, {__metatable = 'locked'})" + + " print(getmetatable(t)) print(pcall(setmetatable, t, {}))"); + s.add("local a = {} local b = {} print(rawequal(a, b), rawequal(a, a))"); + s.add("local t = {} rawset(t, 1, 'x') print(rawget(t, 1), rawlen(t))"); + + // 错误处理 + s.add("print(pcall(function() error('boom') end))"); + s.add("print(pcall(function() error('boom', 0) end))"); + s.add("print(pcall(function() error({code = 7}) end))"); + s.add("print(pcall(function() return 1 + nil end))"); + s.add("print(pcall(function() local t = nil return t.x end))"); + s.add("print(pcall(function() local t = nil t.x = 1 end))"); + s.add("print(pcall(function() return #5 end))"); + s.add("print(pcall(function() return nil .. 'x' end))"); + s.add("print(xpcall(function() error('e') end, function(m) return 'H:' .. m end))"); + s.add("print(pcall(error))"); + s.add("local ok, err = pcall(function() assert(false, 'nope') end) print(ok, err)"); + s.add("print(select('#', 1, 2, 3), select(2, 'a', 'b', 'c'))"); + + // 库函数 + s.add("print(tonumber('10'), tonumber('0x10'), tonumber('1.5'), tonumber('abc'))"); + s.add("print(tonumber('ff', 16), tonumber('101', 2), tonumber('z', 36))"); + /* + * 内嵌 \0:C 版靠「解析指针是否走到 s + l」判失败,Java 的 + * String.trim() 却会把 \0 当成空白先去掉,于是这两组曾经全算成数字。 + * 上游 math.lua 有六条断言对着它们。 + */ + s.add("print(tonumber('1\\0'), tonumber('1 \\0'), tonumber('1\\0 '))"); + s.add("print(tonumber('1\\0', 2), tonumber('\\0ff', 16), tonumber('1\\0', 10))"); + s.add("print(tonumber(' '), tonumber(''), tonumber('1 a'), tonumber('1 a', 2))"); + s.add("print(tonumber('inf'), tonumber('Nan'), tonumber(' 3.4.5 '), tonumber('e1'))"); + s.add("print(tostring(1), tostring(nil), tostring(true), tostring(1.5))"); + s.add("local t = {1, 2, 3} print(table.unpack(t))"); + s.add("local t = {'a'} local k, v = next(t) print(k, v, next(t, k))"); + + // math + s.add("print(math.abs(-3), math.abs(-3.5), math.abs(0))"); + s.add("print(math.ceil(2.3), math.ceil(-2.3), math.ceil(3), math.ceil(3.0))"); + s.add("print(math.floor(2.7), math.floor(-2.7), math.floor(3), math.floor(3.0))"); + s.add("print(math.sqrt(16), math.sqrt(2))"); + s.add("print(math.fmod(7, 3), math.fmod(-7, 3), math.fmod(7, -3), math.fmod(7.5, 2))"); + s.add("print(math.max(1, 5, 3), math.min(1, 5, 3), math.max(1.5, 1), math.min(1.5, 1))"); + s.add("print(math.tointeger(3.0), math.tointeger(3.5), math.tointeger('7')," + + " math.tointeger(3))"); + s.add("print(math.pi, math.huge, -math.huge, math.huge > 1e300)"); + s.add("print(math.maxinteger + 1 == math.mininteger," + + " math.mininteger - 1 == math.maxinteger)"); + + // 一元运算与布尔 + s.add("print(not nil, not false, not 0, not '')"); + s.add("print(-'5', -'5.0', #'abcd')"); + s.add("print(1 and 2, nil and 2, false or 3, nil or false)"); + s.add("print('a' and 'b' or 'c')"); + + // 表构造与多返回值的各种落点 + s.add("local function three() return 1, 2, 3 end" + + " local t = {three()} print(#t, t[1], t[2], t[3])"); + s.add("local function three() return 1, 2, 3 end local t = {three(), three()}" + + " print(#t, t[1], t[2], t[3], t[4])"); + s.add("local function three() return 1, 2, 3 end print(three(), three())"); + s.add("local function three() return 1, 2, 3 end print((three()))"); + s.add("local t = {[1] = 'a', [2] = 'b', 3, 4} print(#t, t[1], t[2], t[3], t[4])"); + + // 方法与 self + s.add("local o = {} function o:m(x) return x * 2 end print(o:m(21))"); + s.add("local o = {v = 7} function o:get() return self.v end print(o:get())"); + + // upvalue 共享 + s.add("local x = 0 local function inc() x = x + 1 end" + + " local function get() return x end inc() inc() print(get(), x)"); + s.add("local t = {} for i = 1, 3 do t[#t+1] = function() return i end end" + + " print(t[1](), t[2](), t[3]())"); + + // 元表的其它入口 + s.add("local mt = {__concat = function(a, b) return 'J' end}" + + " print(setmetatable({}, mt) .. 'x', 'x' .. setmetatable({}, mt))"); + s.add("local mt = {__unm = function(a) return 'NEG' end} print(-setmetatable({}, mt))"); + s.add("local mt = {__index = function(_, k) error('no key ' .. k) end}" + + " print(pcall(function() local t = setmetatable({}, mt) return t.zz end))"); + s.add("local base = {a = 1} local mid = setmetatable({}, {__index = base})" + + " local t = setmetatable({}, {__index = mid}) print(t.a)"); + s.add("local t = setmetatable({}, {__name = 'MyType'}) print(tostring(t))"); + s.add("print(getmetatable(''), getmetatable(1), getmetatable(nil), getmetatable(true))"); + s.add("local mt = {__eq = function() return 'maybe' end}" + + " local a = setmetatable({}, mt) local b = setmetatable({}, mt) print(a == b)"); + s.add("print(pcall(function() local t = {} t[nil] = 1 end))"); + s.add("print(pcall(function() local t = {} t[0/0] = 1 end))"); + s.add("print(#{1, 2, 3, nil}, #{n = 1})"); + + // 错误处理进阶 + s.add("print(pcall(function() error(setmetatable({}, {__tostring = function()" + + " return 'OBJ' end})) end))"); + s.add("local function f() error('deep', 2) end local function g() f() end" + + " print(pcall(g))"); + s.add("print(pcall(pcall, error, 'x'))"); + s.add("print(select('#'), select('#', nil))"); + s.add("print(select(-1, 'a', 'b', 'c'), select(-3, 'a', 'b', 'c'))"); + s.add("print(pcall(function() return select(0, 'a') end))"); + + // _ENV + s.add("local _ENV = {print = print, x = 42} print(x)"); + s.add("local function f() return _G == _ENV end print(f())"); + + // 边界 + s.add("print(math.maxinteger, math.mininteger)"); + s.add("local n = 0 for i = 1, 3 do n = n + i end print(n)"); + s.add("do local x = 1 do local x = 2 print(x) end print(x) end"); + s.add("local t = {} for i = 1, 100 do t[i] = i end local s = 0" + + " for i = 1, #t do s = s + t[i] end print(s)"); + s.add("local s = 0 for i = 1, 1000 do s = s + i end print(s)"); + s.add("print(({'x','y','z'})[2])"); + s.add("local a, b, c = 1 print(a, b, c)"); + + // ---- 批 2:把 Vm 里还没被点亮的指令补上 ---- + + // 立即数比较走元方法(OP_LTI/LEI/GTI/GEI 的慢路径) + s.add("local mt = {__lt = function(a, b) return true end," + + " __le = function(a, b) return false end}" + + " local t = setmetatable({}, mt) print(t < 1, t <= 1, t > 1, t >= 1)"); + s.add("local mt = {__lt = function(a, b) return b == 1 end}" + + " print(setmetatable({}, mt) < 1, 1 < setmetatable({}, mt))"); + s.add("local mt = {} print(pcall(function() return setmetatable({}, mt) < 1 end))"); + s.add("print(2.5 < 3, 2.5 <= 2.5, 3 > 2.5, 3 >= 3.0)"); + + // 立即数算术走元方法(OP_ADDI / SHRI / BNOT 等) + s.add("local mt = {__add = function(a, b) return 'A' .. b end}" + + " print(setmetatable({}, mt) + 7)"); + s.add("local mt = {__shl = function(a, b) return 'SHL' end}" + + " print(setmetatable({}, mt) << 3)"); + s.add("local mt = {__bnot = function(a) return 'BNOT' end}" + + " print(~setmetatable({}, mt))"); + s.add("print(pcall(function() return '10' | 1 end))"); + s.add("print(pcall(function() return ~'a' end))"); + s.add("print(pcall(function() return 2 ^ 'x' end))"); + + // 位运算 / 除法的整数与浮点两路 + s.add("print(1 << 62, -1 >> 63, 7 & -1, 0 ~ 0 ~ 5)"); + s.add("print(5.0 | 0, 5.0 & 3.0, ~5.0, 1.0 << 2.0)"); + // 整数除零/取模零报错,浮点不报(1.0//0 就是 inf)——两者别混成一条 + s.add("print(pcall(function() return 1 // 0 end))"); + s.add("print(pcall(function() return 1 % 0 end))"); + s.add("print(1.0 // 0, 1.0 % 0, -1.0 // 0)"); + s.add("print(2 ^ 0.5, (-8) ^ (1/3), 0 ^ 0)"); + s.add("print(-2.5, - -3, -(0/0))"); + + // CONCAT:多个操作数、数字、元方法 + s.add("print('a' .. 1 .. 2.0 .. true .. 'b')"); + s.add("local s = '' for i = 1, 60 do s = s .. 'x' end print(#s)"); + s.add("print(pcall(function() return {} .. 'x' end))"); + + // 变长参数 + s.add("local function f(...) return ... end print(f(1, 2, 3))"); + s.add("local function f(...) local t = {...} return #t, t[2] end print(f('a', 'b', nil))"); + s.add("local function f(a, ...) return select('#', ...), (...) end print(f(1, 2, 3))"); + s.add("local function id(...) return ... end print(id(id(1, 2), 3))"); + + // 大表构造:多段 SETLIST 与哈希部分预分配 + s.add("local t = {} for i = 1, 200 do t[i] = i end print(#t, t[200])"); + s.add("local t = {1,2,3,4,5,6,7,8,9,10, a=1, b=2, c=3, d=4, e=5} print(#t, t.e)"); + s.add("local n = 0 local t = {10,20,30,40,50,60} for k in pairs(t) do n = n + k end" + + " print(n)"); + + // 与 OP_CLOSE / OP_TBC + s.add("do local f = setmetatable({}, {__close = function()" + + " print('closing') end}) print('body') end print('after')"); + s.add("local order = {} do local a = setmetatable({}, {__close = function()" + + " order[#order+1] = 'a' end}) local b = setmetatable({}," + + " {__close = function() order[#order+1] = 'b' end}) end" + + " print(order[1], order[2])"); + s.add("print(pcall(function() local f = setmetatable({}," + + " {__close = function(_, err) print('on', err) end}) error('x') end))"); + + // 短路与真假值(OP_TEST / OP_TESTSET / OP_LFALSESKIP) + s.add("local t = {} print(t.a and t.a.b, t.a or 'fallback')"); + s.add("local x = nil local y = x and 'has' or 'none' print(y)"); + s.add("local x = 1 local y = x and 'has' or 'none' print(y)"); + s.add("local n = 0 local t = {1, 2, 3} while t[n+1] do n = n + 1 end print(n)"); + + // 方法调用 OP_SELF + s.add("local obj = {v = 1, inc = function(self) self.v = self.v + 1 end}" + + " obj:inc() obj:inc() print(obj.v)"); + s.add("print(pcall(function() local t = {} t:nope() end))"); + + // _ENV 与 upvalue + s.add("local _ENV = {print = print} a = 1 print(a)"); + s.add("print(pcall(function() local _ENV = 5 return x end))"); + s.add("local f do local up = 'u' f = function() return up end end print(f())"); + + // 返回值截断 + s.add("local function three() return 1, 2, 3 end local a = three() print(a)"); + s.add("local function three() return 1, 2, 3 end" + + " local t = {three(), 9} print(#t, t[1], t[2])"); + s.add("local function three() return 1, 2, 3 end return print(#{three()})"); + + // 数值格式化 + s.add("print(1e15, 1e16, 1e-5, 123456789012345, 1/3, 2/3)"); + s.add("print(0.1 + 0.2, 1e308 * 10, -1e308 * 10)"); + s.add("print(3 == 3.0, '3' == 3, 0/0 == 0/0, 1/0 == 1/0)"); + + return s; + } + + @ParameterizedTest(name = "script #{index}") + @MethodSource("scripts") + void scriptMatchesReference(String src) { + Diff.assertMatches(src); + } + + /** + * {@code lauxlib.c: interror} 的两种报错文本。 + *

+ * {@code luaL_checkinteger} 解释不出整数时,判据是 + * {@code lua_isnumber(L, arg)}(是数字,或字符串能解释成数字), + * 不是 {@code lua_type(L, arg) == LUA_TNUMBER}。于是: + *

    + *
  • {@code string.format('%d', '3.5')} —— {@code '3.5'} 解释得成 + * 3.5,只是 3.5 不是整数 → {@code number has no integer + * representation};
  • + *
  • {@code string.format('%d', 'abc')} —— 压根不是数字 → + * {@code number expected, got string}。
  • + *
+ * 早年这里按 {@code v.isNumber()} 判,两条都成了后者。 + */ + @Test + void integerCoercionErrorTexts() { + Diff.assertMatches("" + + "print(pcall(string.format, '%d', '3.5'))\n" + + "print(pcall(string.format, '%d', 'abc'))\n" + + "print(pcall(string.format, '%d', '10'))\n" + + "print(pcall(string.format, '%d', '3.0'))\n" + + "print(pcall(string.format, '%d', ' 7 '))\n" + + "print(pcall(string.format, '%d', true))\n" + + "print(pcall(string.format, '%d', nil))\n" + + "print(pcall(string.format, '%d', {}))\n" + + "print(pcall(string.format, '%d', 1e100))\n" + + "print(pcall(string.format, '%5.2f', '3.5'))\n" + + "print(pcall(string.format, '%5.2f', 'abc'))\n" + + "print(pcall(string.rep, 'x', '3.5'))\n" + + "print(pcall(string.rep, 'x', 'abc'))\n" + + "print(pcall(string.sub, 'abcdef', '2', '4'))\n" + + "print(pcall(string.sub, 'abcdef', '2.5', '4'))\n" + + "print(pcall(string.sub, 'abcdef', 'x', '4'))\n" + + "print(pcall(table.concat, {'a','b'}, '2'))\n" + + "print(pcall(table.move, {1,2,3}, '1', '2', '1'))\n"); + } + + /** + * {@code %.14g} 的舍入:一次舍到位,不是「最短表示再舍一次」。 + *

+ * Java 的 {@code String.format("%.Nf")} / {@code ("%.Ne")} 不是拿真值舍的, + * 而是拿 {@code Double.toString} 的最短表示再舍一次 —— 连舍两次。 + * {@code math.sqrt(math.maxinteger)} 的真值是 + * {@code 3037000499.976049900054931640625},最短表示 {@code 3037000499.97605} + * 的第 15 位是 5,于是第二次舍入把它顶成 {@code 3037000499.9761}; + * 参考实现按真值舍,给 {@code 3037000499.976}。 + *

+ * 这一类错很隐蔽:4602 个值里只错 12 个(0.26%),手写的用例几乎撞不到, + * 只有 fuzz 撞得到。所以这里用脚本内的 LCG 铺开几千个量级—— + * 不用 {@code math.random},免得随机数发生器本身也进入被比较的范围。 + */ + @Test + void floatFormattingMatchesReferenceAcrossMagnitudes() { + Diff.assertMatches("" + // 实测撞出来的那几个 + + "print(math.sqrt(math.maxinteger))\n" + + "print(math.sqrt(56.5), math.sqrt(96), math.sqrt(385.5), math.sqrt(399.5))\n" + + "print(2^89)\n" + + "print(24807139121570.0, 6409511108729.2, 6.8674997827243e-05)\n" + // 循环小数与整除 + + "for i = 1, 400 do print(i/7) print(i/3) print(i/9) end\n" + // 铺满量级:次正规到 1e300 + + "for i = 0, 300 do print(2^(i - 150)) end\n" + + "for i = 1, 300 do print(i * 1e-5) print(i * 1e5) print(i / 3) end\n" + // 开方:结果往往正好落在 14 位有效数字的边界上 + + "for i = 1, 400 do print(math.sqrt(i)) print(math.sqrt(i + 0.5)) end\n" + // 脚本内 LCG,铺随机尾数 + + "local s = 12345\n" + + "local function rnd() s = (s * 1103515245 + 12345) % 2147483648 return s end\n" + + "for i = 1, 600 do\n" + + " local a, b = rnd(), rnd()\n" + + " print(a / b)\n" + + " print(a * b * 1e-10)\n" + + " print(a + b / 10000)\n" + + "end\n" + + "print(0.0, -0.0, 1/0, -1/0, 0/0)\n"); + } + + /** + * {@code #t} 的具体取值,而不只是「某个合法边界」。 + *

+ * 手册只要求 {@code #t} 返回一个边界,但真实程序看得到具体是哪一个,所以要 + * 和参考实现一模一样。差在 {@code ltable.c: luaH_getn} 的分支 (1a): + * 数组末格为空、但倒数第二格还在时,C 直接答 {@code limit-1},不去二分 + * ——注释写明了动机,{@code t[#t] = nil} 之后立刻再取长度是最常见的一串操作。 + *

+ * 少了 (1a):数组 {@code [1, nil, 3, nil]} 二分给 {@code 1},参考实现给 + * {@code 3}。系统性地铺开各种空洞之后,4968 条结果里错 212 条(4.3%)。 + * 这里用循环铺一个子集;每条都把 {@code #t} 取两次 —— C 会顺手把 {@code alimit} + * 改成一个「提示」,两次结果必须一致。 + */ + @Test + void tableLengthBoundaryMatchesReference() { + Diff.assertMatches("" + // 实测撞出来的那几条 + + "local p = table.pack(1, nil, 3, nil) print(p.n, #p)\n" + + "local q = {1, 2, 3, 4} q[2] = nil q[4] = nil print(#q)\n" + // 单个空洞:n 从 1 到 32,空洞扫过每个位置 + + "local function build(n, skip)\n" + + " local t = {}\n" + + " for i = 1, n do if not skip(i) then t[i] = i end end\n" + + " return t\n" + + "end\n" + + "for n = 1, 32 do\n" + + " for h = 1, n do\n" + + " local t = build(n, function(i) return i == h end)\n" + + " print(n, h, #t, #t)\n" + + " end\n" + + "end\n" + // 两个空洞 + + "for n = 2, 14 do\n" + + " for a = 1, n do\n" + + " for b = a, n do\n" + + " local t = build(n, function(i) return i == a or i == b end)\n" + + " print(n, a, b, #t, #t)\n" + + " end\n" + + " end\n" + + "end\n" + // 全空 / 只留头 / 只留尾 / 隔一个空一个 + + "for n = 1, 24 do\n" + + " local t = build(n, function(i) return true end) print(n, #t, #t)\n" + + " t = build(n, function(i) return i > 1 end) print(n, #t, #t)\n" + + " t = build(n, function(i) return i < n end) print(n, #t, #t)\n" + + " t = build(n, function(i) return i % 2 == 0 end) print(n, #t, #t)\n" + + " t = build(n, function(i) return i % 2 == 1 end) print(n, #t, #t)\n" + + "end\n" + // table.pack 的 32 种 nil 组合:n 是精确的,#p 与 unpack 用 #p + + "for m = 0, 31 do\n" + + " local args = {}\n" + + " for k = 1, 5 do args[k] = ((m >> (k - 1)) & 1) == 1 and k or nil end\n" + + " local r = table.pack(table.unpack(args, 1, 5))\n" + + " print(r.n, #r, select('#', table.unpack(r)))\n" + + "end\n"); + } + + /** + * 库函数报错里的全名在协程里也得反查得到。 + *

+ * {@code lauxlib.c: pushglobalfuncname} 从 {@code _LOADED}(也就是 + * {@code package.loaded})里按值反查两层,才把 {@code '?'} 换成 + * {@code 'math.random'}。而 {@code _LOADED} 在 C 里挂在注册表上, + * 注册表是 {@code global_State} 级别的 —— 所有 {@code lua_State} 共用一份。 + * 这里早先每个 state 一份,协程拿到的是空表,于是: + * {@code bad argument #1 to '?'} vs 参考实现的 {@code 'math.random'}。 + *

+ * 不打印 {@code coroutine.running()}:那是线程地址,两边必然不同。 + */ + @Test + void libraryFunctionNamesResolveInsideCoroutines() { + Diff.assertMatches("" + + "local co = coroutine.create(function()\n" + + " print(pcall(math.random, 5, 1))\n" + + " print(pcall(string.rep, 'x', 'y'))\n" + + " print(pcall(string.format, '%d', 'zz'))\n" + + " print(pcall(table.concat, 5))\n" + + " print(pcall(math.random, 'x'))\n" + + "end)\n" + + "print(coroutine.resume(co))\n" + // 主线程上也要对:两条路径共用同一份 _LOADED + + "print(pcall(math.random, 5, 1))\n" + + "print(pcall(string.rep, 'x', 'y'))\n" + + "local function f() return math.random(5, 1) end\n" + + "print(pcall(f))\n" + + "local g = math.random\n" + + "print(pcall(g, 5, 1))\n"); + } + + @Test + void errorMessagesMatchReference() { + List bad = new ArrayList<>(); + bad.add("print(nil + 1)"); + bad.add("local t = nil print(t.x)"); + bad.add("local t = nil t.x = 1"); + bad.add("print(#5)"); + bad.add("print(nil .. 'x')"); + bad.add("print((nil)())"); + bad.add("error('boom')"); + bad.add("assert(false)"); + bad.add("for i = 1, 'x' do end"); + bad.add("for i = 1, 10, 0 do end"); + // 名字后缀:常量 / 局部变量 / upvalue / 全局 / 字段 / 方法 / 元方法 + bad.add("return '10' | 1"); + bad.add("return ~'a'"); + bad.add("local x = nil return x + 1"); + bad.add("local x = nil return x .. 'a'"); + bad.add("local x = nil return #x"); + bad.add("local up = nil local function f() return up + 1 end f()"); + bad.add("return undefinedglobal + 1"); + bad.add("local t = {} return t.x + 1"); + bad.add("local t = {} return t[1] + 1"); + bad.add("local t = {} t:nope()"); + bad.add("local t = {} return t ^ 1"); + bad.add("local x = 5 print(x)"); + bad.add("return 1 // 0"); + bad.add("return 1 % 0"); + bad.add("return 1 & 'x'"); + bad.add("local t = setmetatable({}, {}) return t < 1"); + bad.add("return 2 < 'a'"); + for (String src : bad) { + Diff.assertFirstLineMatches(src); + } + } + + /** + * {@code and} / {@code or} 的返回值,而不只是真假。 + *

+ * 这两个算符的字节码一直与参考实现逐条相同,所以字节码闸门({@code ConformanceTest}) + * 全绿;错的是 {@code OP_TESTSET} 的判条件写成了「真值 == k」, + * 而上游判的是 {@code l_isfalse(rb) == k}——两者在 {@code k} 的两个取值上 + * 恰好相反。更阴的是 {@code TESTSET} 只在「结果寄存器 ≠ B」时才留下来 + * (否则 {@code patchTestReg} 把它改写成 {@code OP_TEST}),于是 + * {@code if x then} 一直是对的、{@code x and y} 一直是错的—— + * 症状 {@code print(t and "yes")} 打印出那张表, + * 而 {@code cond and a or b} 这个 Lua 里最常用的写法整体取错分支。 + *

+ * 所以这里专门钉住「返回的是哪个值」,以及短路有没有真的短路。 + */ + @Test + void logicalOperatorsReturnValues() { + Diff.assertMatches("" + + "print(1 and 2, true and 'x', nil and 2, false and 2)\n" + + "print(1 or 2, true or 'x', nil or 2, false or 2)\n" + + "local a, t, z = 1, {}, nil\n" + + "print(a and 2, t and 'yes', z and 2, z and 'no')\n" + + "print(a or 9, t or 9, z or 9)\n" + // 短路:右操作数根本不该被求值 + + "print(z and z.x)\n" + + "print(t or z.x)\n" + + "local n = 0\n" + + "local function bump() n = n + 1 return true end\n" + + "local r = z and bump()\n" + + "print(r, n)\n" + + "r = a and bump()\n" + + "print(r, n)\n" + // 最常用的惯用法 + + "print(z and 1 or 2)\n" + + "print(a and 1 or 2)\n" + + "print(false and 1 or 2)\n" + // 比较结果是 VRELOC,走的是另一条(OP_NOT 折叠) + + "print((1 < 2) and 'lt', (1 > 2) and 'gt', (1 > 2) or 'ge')\n" + + "print(not (1 < 2) and 'x' or 'y')\n" + // 嵌套与副作用顺序 + + "print(a and t and 'deep')\n" + + "print(z or false or 'fall')\n" + + "print((a and nil) or 'after')\n"); + } + + /** + * {@code string.format('%q')} 的系统性探针:{@code addquoted} 的每条分支、 + * 以及 0..255 每一个字节原样输出。 + *

+ * 扫全部 256 个字节不只是为了 {@code %q}:字节 0x80–0xFF 是非法 UTF-8, + * 它们被原样写进 stdout。这一段因此同时是「输出逐字节一致」的钉子 + * ——而逐字节这件事本身曾经是假的:闸门两边都用 UTF-8 解码捕获到的输出, + * 非法字节在两边都变成 {@code U+FFFD},于是「一边写 0x80、另一边写 0xFF」 + * 会被判成一致。实测:{@code b'"\x80"'} 与 {@code b'"\xef\xbf\xbd"'} + * 按 UTF-8 解码后相等(都是 {@code U+FFFD}),按 Latin-1 解码才不等。 + * 闸门现在用 Latin-1(字节↔字符一一对应)。 + *

+ * 不含次正规浮点:{@code %q} 走 C 的 {@code %a},而 C99 对次正规数 + * 的前导数字写的是 unspecified —— glibc 与 Apple libc 实测相反 + * (详见 {@code StringLibTest.subnormalQuoteFollowsGlibc})。 + */ + @Test + void quoteFormatMatchesReferenceForEveryByte() { + Diff.assertMatches("" + + "local function emit(s) print(string.format('%q', s)) end\n" + + "for b = 0, 255 do emit(string.char(b)) end\n" + + "for b = 0, 255 do emit(string.char(b) .. '5') end\n" + + "for b = 0, 255 do emit(string.char(b) .. 'z') end\n" + + "for b = 0, 255 do emit('a' .. string.char(b) .. '9b') end\n" + // 需要补成 \ddd 的那些:后一位是数字时 \d 会被读成 \dd + + "emit('\\1\\12\\123')\n" + + "emit(string.char(1, 50) .. string.char(2, 51) .. string.char(0, 48))\n" + + "emit('\"')\n" + + "emit('\\\\')\n" + + "emit('\\n')\n" + + "emit('\\r')\n" + + "emit('\\t')\n" + + "emit('\\0')\n" + + "emit('a\\0b')\n" + + "emit('\\\\\"\\\\')\n" + // 浮点:quotefloat 的十六进制分支(次正规除外,见上) + + "local fs = {0.0, -0.0, 1.0, -1.0, 0.5, -0.5, 1/3, 1/7, 0.1, 0.2, 0.3,\n" + + " 1e300, 1e-300, 1e308, math.pi, math.sqrt(2), 2^53, 2^63, 2^64, -2^63,\n" + + " math.maxinteger, math.mininteger, 1e15, 1e16, 1e17, 0.000001, 0.0000001}\n" + + "for i = 1, #fs do print(string.format('%q', fs[i])) end\n" + + "print(string.format('%q', 1/0), string.format('%q', -1/0),\n" + + " string.format('%q', 0/0))\n" + // 整数:MIN_INTEGER 走十六进制 + + "local is = {0, 1, -1, 7, 255, 65535, 2147483647, -2147483648,\n" + + " math.maxinteger, math.mininteger, 1000000000000, -1000000000000}\n" + + "for i = 1, #is do print(string.format('%q', is[i])) end\n" + + "print(string.format('%q', nil), string.format('%q', true),\n" + + " string.format('%q', false), string.format('%q', '10'),\n" + + " string.format('%q', ' x '))\n" + // 往返:%q 出来的要能被 load 原样读回 + + "local rt = {'a', '\\n', '\"', '\\\\', '\\0', '\\1', '\\12', '\\123',\n" + + " '\\x7f', '\\xff', 'a\\0b', '\\\\1', '\\0012', 'he said \\\"hi\\\"\\n'}\n" + + "for i = 1, #rt do\n" + + " local q = string.format('%q', rt[i])\n" + + " local ok, v = pcall(load('return ' .. q))\n" + + " print(q, ok, (ok and v == rt[i]) or '-')\n" + + "end\n" + // 报错路径 + + "print(pcall(string.format, '%q', {}))\n" + + "print(pcall(string.format, '%q', print))\n" + + "print(pcall(string.format, '%5q', 'x'))\n" + + "print(pcall(string.format, '%.3q', 'x'))\n" + + "print(pcall(string.format, '%-q', 'x'))\n" + + "print(pcall(string.format, '%q'))\n"); + } + + /** + * 运算符 × 类型的全矩阵:没有元方法时,每个组合的报错内容。 + *

+ * 这一批是「运算符 × 类型」的系统性扫描,不是挑几个组合点一下。它撞出过一处 + * 真分歧:{@code 1.5 & 3} 参考实现报左边的 {@code 1.5} + * ({@code number (local 'a') has no integer representation}), + * 这里按 {@code F2I_FLOOR} 判,{@code 1.5} 被当成「能转成整数」于是去报了 + * 右边的 {@code 3}。{@code lvm.h} 里 {@code LUA_FLOORN2I == F2Ieq}—— + * 名字带 floor,语义是「只接受整数值,不取整」。手写的用例只有 + * inf/nan 那两个极端值,两种模式在它们上面结论一样,所以一直没露出来。 + *

+ * 值里带 {@code local}/{@code field} 后缀是刻意的:{@code varinfo} 会反推 + * 值是怎么来的,报的不是类型而是变量名,这条也一起钉住。 + */ + @Test + void operatorTypeErrorsMatchReference() { + Diff.assertMatches(""" + -- 元方法与类型错误矩阵:运算符 × 类型对,无元方法时的报错 + local VALUES = { + { "nil", function() return nil end }, + { "boolean", function() return true end }, + { "integer", function() return 3 end }, + { "float", function() return 1.5 end }, + { "string", function() return "x" end }, + { "numstr", function() return "3" end }, + { "numstrf", function() return "1.5" end }, + { "table", function() return {} end }, + { "function", function() return print end }, + { "thread", function() return coroutine.create(function() end) end }, + } + + local OPS = { + { "add", function(a, b) return a + b end }, + { "sub", function(a, b) return a - b end }, + { "mul", function(a, b) return a * b end }, + { "div", function(a, b) return a / b end }, + { "idiv", function(a, b) return a // b end }, + { "mod", function(a, b) return a % b end }, + { "pow", function(a, b) return a ^ b end }, + { "unm", function(a) return -a end }, + { "band", function(a, b) return a & b end }, + { "bor", function(a, b) return a | b end }, + { "bxor", function(a, b) return a ~ b end }, + { "shl", function(a, b) return a << b end }, + { "shr", function(a, b) return a >> b end }, + { "bnot", function(a) return ~a end }, + { "concat", function(a, b) return a .. b end }, + { "len", function(a) return #a end }, + { "lt", function(a, b) return a < b end }, + { "le", function(a, b) return a <= b end }, + { "eq", function(a, b) return a == b end }, + { "index", function(a) return a.k end }, + { "call", function(a) return a() end }, + } + + for oi = 1, #OPS do + local op = OPS[oi] + local nargs = (op[1] == "unm" or op[1] == "bnot" or op[1] == "len" + or op[1] == "index" or op[1] == "call") and 1 or 2 + for i = 1, #VALUES do + local jto = (nargs == 1) and i or #VALUES + for j = (nargs == 1) and i or 1, jto do + local a = VALUES[i][2]() + local b = (nargs == 2) and VALUES[j][2]() or nil + local ok, r + if nargs == 1 then + ok, r = pcall(op[2], a) + else + ok, r = pcall(op[2], a, b) + end + -- 表/函数/线程的地址每次都不同,只留类型 + if not ok and type(r) ~= "string" then r = type(r) end + print(op[1], VALUES[i][1], (nargs == 2) and VALUES[j][1] or "-", + ok, tostring(r)) + end + end + end + """); + } + + /** + * 有元方法时:分派顺序、{@code __le} 的回退、{@code __index}/{@code __newindex} + * 的表与函数两种形态、{@code __concat} 的右结合、{@code __close} 的调用时机 + * 与错误传递、以及元方法本身出错或不合法时的报错。 + *

+ * 二元运算的四种组合(只有左有 / 只有右有 / 两边都有 / 都没有)逐个运算符铺开, + * 因为「先找谁」是 {@code luaT_trybinTM} 最容易写反的地方。 + */ + @Test + void metamethodDispatchMatchesReference() { + Diff.assertMatches(""" + -- 有元方法时:谁的分派先走、结果是什么、链式/回退 + local function mt(t, name) + return setmetatable({}, {__index = function(_, k) + return function(a, b) return name .. "(" .. tostring(a) .. "," .. tostring(b) .. ")" end + end}) + end + + -- 1) 二元运算:左右各自定义/不定义 + local BINOPS = { "__add", "__sub", "__mul", "__div", "__mod", "__pow", + "__idiv", "__band", "__bor", "__bxor", "__shl", "__shr" } + local SYMBOL = { __add = "+", __sub = "-", __mul = "*", __div = "/", + __mod = "%", __pow = "^", __idiv = "//", __band = "&", + __bor = "|", __bxor = "~", __shl = "<<", __shr = ">>" } + for i = 1, #BINOPS do + local ev = BINOPS[i] + for l = 0, 1 do + for r = 0, 1 do + local a, b = {}, {} + if l == 1 then + setmetatable(a, {[ev] = function(x, y) return "L" end}) + end + if r == 1 then + setmetatable(b, {[ev] = function(x, y) return "R" end}) + end + -- 用 load 拼运算符,避免手写 12 份 + local f = load("local a, b = ... return a " .. SYMBOL[ev] .. " b") + local ok, res = pcall(f, a, b) + print(ev, "l=" .. l, "r=" .. r, ok, tostring(res)) + end + end + end + + -- 2) 一元:只看第一个操作数的元表 + for _, ev in ipairs({ "__unm", "__bnot", "__len" }) do + for l = 0, 1 do + local a = {} + if l == 1 then + setmetatable(a, {[ev] = function(x) return "L" end}) + end + local f + if ev == "__unm" then f = load("local a = ... return -a") + elseif ev == "__bnot" then f = load("local a = ... return ~a") + else f = load("local a = ... return #a") end + print(ev, "l=" .. l, pcall(f, a)) + end + end + + -- 3) 比较:__lt / __le 的回退(没有 __le 时用 not (b < a)) + local CMP = { "<", "<=", ">", ">=" } + for i = 1, #CMP do + local sym = CMP[i] + for l = 0, 1 do + for r = 0, 1 do + local a, b = {}, {} + if l == 1 then setmetatable(a, {__lt = function() return "LT" end, + __le = function() return "LE" end}) end + if r == 1 then setmetatable(b, {__lt = function() return "LT" end, + __le = function() return "LE" end}) end + local f = load("local a, b = ... return a " .. sym .. " b") + print("cmp", sym, "l=" .. l, "r=" .. r, pcall(f, a, b)) + end + end + end + + -- 4) 只有 __lt、没 __le:a <= b 要走 not (b < a) + local onlylt = setmetatable({}, {__lt = function() return true end}) + print("onlylt <=", pcall(function() return onlylt <= onlylt end)) + print("onlylt >=", pcall(function() return onlylt >= onlylt end)) + + -- 5) 比较报错:不同类型 + local cv = { {"nil", nil}, {"bool", true}, {"int", 1}, {"flt", 1.5}, + {"str", "x"}, {"tbl", {}}, {"fn", print} } + for i = 1, #cv do + for j = 1, #cv do + print("lt", cv[i][1], cv[j][1], pcall(function() return cv[i][2] < cv[j][2] end)) + print("le", cv[i][1], cv[j][1], pcall(function() return cv[i][2] <= cv[j][2] end)) + print("eq", cv[i][1], cv[j][1], pcall(function() return cv[i][2] == cv[j][2] end)) + end + end + + -- 6) __concat:右结合、多操作数、元方法在任一侧 + local ca = setmetatable({}, {__concat = function(a, b) return "C" end}) + print(#(ca .. "x"), ca .. "x" .. 1, 1 .. "x" .. ca, "x" .. ca .. 2) + print(pcall(function() return {} .. 1 end)) + print(pcall(function() return 1 .. {} end)) + + -- 7) __index / __newindex:表形态与函数形态 + local proto = {k = "fromtable"} + local t1 = setmetatable({}, {__index = proto}) + print(t1.k, t1.missing, rawget(t1, "k")) + local t2 = setmetatable({}, {__index = function(t, k) return "fn:" .. tostring(k) end}) + print(t2.k, t2[1], rawget(t2, "k")) + local log = {} + local t3 = setmetatable({}, {__newindex = function(t, k, v) log[#log+1] = k .. "=" .. tostring(v) end}) + t3.x = 1 + t3[1] = 2 + print(t3.x, rawget(t3, "x"), table.concat(log, ",")) + local store = {} + local t4 = setmetatable({}, {__index = store, __newindex = store}) + t4.a = 5 + print(t4.a, rawget(t4, "a"), store.a) + -- 没有 __newindex 但 __index 是表:写入落自己身上 + local t5 = setmetatable({}, {__index = store}) + t5.b = 7 + print(t5.b, rawget(t5, "b"), store.b) + + -- 8) __eq:两侧都有时是否调用(类型相同才走) + local eqa = setmetatable({}, {__eq = function() return "EQ" end}) + local eqb = setmetatable({}, {__eq = function() return "EQ" end}) + print(eqa == eqb, eqa == setmetatable({}, {})) + -- 原始相等:数字的不同子类型 + print(1 == 1.0, "1" == 1, rawequal(1, 1.0), rawequal({}, {})) + + -- 9) __call / __tostring / __metatable / __name / __pairs + local c = setmetatable({}, {__call = function(self, ...) return "called", ... end}) + print(c(1, 2)) + print(tostring(setmetatable({}, {__tostring = function() return "TS" end}))) + print(pcall(function() + return setmetatable({}, {__tostring = function() return 42 end}) + end)) + local guarded = setmetatable({}, {__metatable = "locked"}) + print(getmetatable(guarded), pcall(setmetatable, guarded, {})) + print(pcall(function() return #setmetatable({}, {__len = function() return "x" end}) end)) + local nm = setmetatable({}, {__name = "MyType"}) + print(tostring(nm):sub(1, 6), pcall(function() return nm + 1 end)) + print(pcall(function() return setmetatable({}, {__name = 5}) end)) + local pv = setmetatable({}, {__pairs = function(t) return function() end, t, nil end}) + local n = 0 + for _ in pairs(pv) do n = n + 1 end + print("pairs", n) + + -- 10) __close 与 to-be-closed 变量 + do + local order = {} + local function closef(_, err) + order[#order + 1] = "closed:" .. tostring(err) + return nil + end + local obj = setmetatable({}, {__close = closef}) + local ok, e = pcall(function() + local x = obj + error("boom") + end) + print("close", ok, e, table.concat(order, "|")) + print(pcall(function() + local y = setmetatable({}, {__close = function() return 1, 2 end}) + end)) + print(pcall(function() + local z = 42 + end)) + print(pcall(function() + local w = nil + end)) + end + + -- 11) 元方法自己出错 / 不是函数 + print(pcall(function() + return setmetatable({}, {__add = 42}) + 1 + end)) + print(pcall(function() + return setmetatable({}, {__add = function() error("in mm") end}) + 1 + end)) + print(pcall(function() + return setmetatable({}, {__index = 42}).x + end)) + + -- 12) 元方法链:__index 是表,那张表又有元表 + local deep = setmetatable({}, {__index = setmetatable({}, {__index = {v = "deep"}})}) + print(deep.v) + """); + } + + /** + * {@code string.format} 矩阵:动词 × 修饰符 × 宽度 × 精度 × 值类型, + * 20777 行。{@code lstrlib.c: str_format} 是 C 里分支最密的函数之一, + * 这一批是照着它的每个分支铺的,不是挑几个格式串点一下。它咬出两处: + *

    + *
  • 校验顺序:{@code d i u o x X} 与 {@code f e E g G} 在 C 里是 + * 先 {@code luaL_checkinteger} / {@code luaL_checknumber}、 + * 后 {@code checkformat}({@code intcase} 那一段的顺序); + * 而 {@code c a s} 是先 {@code checkformat}。这里一律把格式串校验 + * 放在前面,于是 {@code ("% .100d"):format("x")} 报的是 + * {@code invalid conversion specification} 而不是 + * {@code number expected, got string}——矩阵里错 6620 行。
  • + *
  • 整数动词给了精度时 {@code 0} 标志被忽略:C99 写的是 + * 「for d, i, o, u, x, and X conversions, if a precision is + * specified, the 0 flag is ignored」。 + * {@code ("%05.3d"):format(42)} 是 {@code " 042"}(精度先补成 + * {@code "042"},宽度再用空格补),不是 {@code "00042"}。 + * 错 220 行。浮点动词没有这条规则,别一并改。
  • + *
+ */ + @Test + void formatSpecifierMatrixMatchesReference() { + Diff.assertMatches(""" + -- string.format 矩阵:动词 × 修饰符 × 宽度 × 精度 × 值类型 + local VALS = { + { "int", function() return 42 end }, + { "negint", function() return -42 end }, + { "maxint", function() return math.maxinteger end }, + { "minint", function() return math.mininteger end }, + { "zero", function() return 0 end }, + { "flt", function() return 1.5 end }, + { "negflt", function() return -1.5 end }, + { "bigflt", function() return 1e300 end }, + { "smallflt", function() return 1e-300 end }, + { "nan", function() return 0 / 0 end }, + { "inf", function() return 1 / 0 end }, + { "neginf", function() return -1 / 0 end }, + { "zeroflt", function() return 0.0 end }, + { "negzero", function() return -0.0 end }, + { "str", function() return "hi" end }, + { "longstr", function() return string.rep("z", 120) end }, + { "emptystr", function() return "" end }, + { "nulstr", function() return "a" .. string.char(0) .. "b" end }, + { "numstr", function() return "42" end }, + { "nilv", function() return nil end }, + { "bool", function() return true end }, + { "tbl", function() return {} end }, + } + + -- A) 动词 × 值(无修饰符) + local VERBS = { "d", "i", "x", "X", "o", "c", "e", "E", "f", "g", "G", "q", + "s", "%", "a", "A", "u", "F", "b", "n", "1", "" } + for vi = 1, #VERBS do + local v = VERBS[vi] + for i = 1, #VALS do + local ok, r = pcall(string.format, "%" .. v, VALS[i][2]()) + print("A", v, VALS[i][1], ok, tostring(r)) + end + end + + -- B) 修饰符 × 宽度 × 精度:挑几个动词与几个值 + local FLAGS = { "", "-", "+", " ", "#", "0", "-0", "+0", "#0", "-+ " } + local WIDTH = { "", "0", "1", "5", "12" } + local PREC = { "", ".", ".0", ".1", ".3", ".10", ".20", ".99", ".100" } + local BVERBS = { "d", "x", "f", "e", "g", "s", "c", "o", "q" } + local BVALS = { 1, 6, 8, 15, 20 } -- int / flt / nan / str / nulstr + + for bv = 1, #BVERBS do + local verb = BVERBS[bv] + for fi = 1, #FLAGS do + for wi = 1, #WIDTH do + for pi = 1, #PREC do + local spec = "%" .. FLAGS[fi] .. WIDTH[wi] .. PREC[pi] .. verb + for k = 1, #BVALS do + local idx = BVALS[k] + local ok, r = pcall(string.format, spec, VALS[idx][2]()) + print("B", spec, VALS[idx][1], ok, tostring(r)) + end + end + end + end + end + + -- C) 多参数 / 参数不够 / 参数过多 + print(pcall(string.format, "%d %d", 1)) + print(pcall(string.format, "%d %d", 1, 2, 3)) + print(pcall(string.format, "%d", 1, 2)) + print(pcall(string.format, "%%")) + print(pcall(string.format, "%%d", 1)) + print(pcall(string.format, "%5%", 1)) + print(pcall(string.format, "abc")) + print(pcall(string.format, "")) + print(pcall(string.format, "%", 1)) + print(pcall(string.format, "a%", 1)) + print(pcall(string.format, "%-", 1)) + print(pcall(string.format, "%.", 1)) + print(pcall(string.format, "%.d", 1)) + print(pcall(string.format)) + print(pcall(string.format, nil)) + print(pcall(string.format, 1)) + print(pcall(string.format, true)) + print(pcall(string.format, {})) + + -- D) %s 的 100 字节阈值(>100 且不截断时不进 buff) + for n = 95, 105 do + local s = string.rep("y", n) + print("D", n, pcall(string.format, "%s", s) == false, #string.format("%s", s), + pcall(string.format, "%10s", s) == false, + pcall(string.format, "%.3s", s) == false) + end + + -- E) 数字字符串参与数值动词(字符串可转数字) + for _, v in ipairs({ "42", "42.0", "42.5", "0x2a", " 42 ", "1e3", "", " " }) do + print("E", string.format("[%s]", v), + pcall(string.format, "%d", v), + pcall(string.format, "%5.2f", v), + pcall(string.format, "%x", v)) + end + + -- F) 超大精度 / 超大宽度(C 里有上限检查) + print(pcall(string.format, "%200d", 1)) + print(pcall(string.format, "%.200f", 1.5)) + print(pcall(string.format, "%200s", "x")) + print(pcall(string.format, "%1000d", 1)) + print(pcall(function() return #(string.format("%200d", 1)) end)) + print(pcall(function() return #(string.format("%200s", "x")) end)) + """); + } + + /** + * {@code string.pack} / {@code unpack} / {@code packsize} 矩阵:全部格式动词 + * (含 {@code i/I} 的 1–16 字节宽度、{@code c/s} 的计数、{@code X} 对齐、 + * {@code !n} 对齐、四种字节序)× 各类型的值,外加非法格式串与偏移越界, + * 706 行。这一批是干净的,收进来是为了把「已经一致」这件事也钉住—— + * 否则下次动 {@code lstrlib.c} 的 pack 分支时,没有一条用例会说话。 + */ + @Test + void packUnpackMatrixMatchesReference() { + Diff.assertMatches(""" + -- string.pack / unpack / packsize 矩阵 + local function show(tag, fmt, ...) + local pok, p = pcall(string.pack, fmt, ...) + if not pok then + print(tag, fmt, "pack-err", tostring(p)) + return + end + local sok, s = pcall(string.packsize, fmt) + local uok, a, b, c = pcall(string.unpack, fmt, p) + print(tag, fmt, "ok", #p, sok and s or ("err:" .. tostring(s)), + uok and tostring(a) or ("err:" .. tostring(a)), + uok and tostring(b) or "-", uok and tostring(c) or "-") + end + + local FORMATS = { + "b", "B", "h", "H", "l", "L", "j", "J", "T", "n", "d", "f", "z", "x", "Xi", "Xo", + "i", "i1", "i2", "i3", "i4", "i8", "i16", "i17", "i0", + "I", "I1", "I2", "I3", "I4", "I8", "I16", + "c0", "c1", "c3", "c8", "s", "s1", "s2", "s4", "s8", + "i4", "=i4", "!i4", "I4", "!4i4", "!8i8", "!1i1", "!2i2", "!16i8", + "!4b", "!8d", "d", "=d", "!d", "f", + "j", "!8j", "n", + "i4i4", "i4", "!8i8i8", "c3bBhH", "zs1", "i4z", "xXi4", "Xi4", + "!8Xi4", "Xi4", "bXb", "i4Xi4", + "s1i4", "i4s1", "c0c0", "zz", + } + local INTS = { 0, 1, -1, 127, 128, 255, -128, -129, 32767, 32768, -32768, -32769, + 2147483647, 4294967296, math.maxinteger, math.mininteger } + local STRS = { "", "a", "abc", "hello world" } + + -- A) 整数动词 × 整数值 + local IFMT = { "b", "B", "h", "H", "l", "L", "j", "J", "T", "i", "i1", "i2", + "i3", "i4", "i8", "I", "I1", "I3", "I8", "i4", "!8i8" } + for i = 1, #IFMT do + for k = 1, #INTS do + show("A", IFMT[i], INTS[k]) + end + end + + -- B) 浮点动词 × 浮点值 + local FFMT = { "f", "d", "n", "d", "=f", "!8d" } + local FLTS = { 0.0, -0.0, 1.0, -1.0, 1.5, -1.5, 1 / 3, 1e300, 1e-300, 1 / 0, + -1 / 0, 0 / 0, 3.4028235e38, 1e39 } + for i = 1, #FFMT do + for k = 1, #FLTS do + show("B", FFMT[i], FLTS[k]) + end + end + + -- C) 字符串动词 × 字符串 + local SFMT = { "z", "s", "s1", "s2", "s4", "s8", "c0", "c1", "c3", "c8" } + for i = 1, #SFMT do + for k = 1, #STRS do + show("C", SFMT[i], STRS[k]) + end + end + + -- D) 全部格式串 × 一个通用值 + for i = 1, #FORMATS do + show("D", FORMATS[i], 7) + end + + -- E) 参数类型错误 + for _, f in ipairs({ "b", "h", "l", "j", "i4", "I4", "d", "f", "n", "z", "s", "c3", "x" }) do + print("E", f, pcall(string.pack, f, "x")) + print("E", f, pcall(string.pack, f, nil)) + print("E", f, pcall(string.pack, f, {})) + print("E", f, pcall(string.pack, f, true)) + end + + -- F) 非法格式串 + for _, f in ipairs({ "", "y", "i17", "i0", "i-1", "!0i4", "!7i4", "!3", "!!", + "i4i", "c", "c-1", "X", "Xy", "s0", "s17", "z1", "b1", + "i4 ", " i4", "I17", "I0", string.rep("i4", 100) }) do + print("F", string.format("[%s]", f), pcall(string.packsize, f), + pcall(string.pack, f, 1)) + end + + -- G) unpack 的输入长度/偏移 + for _, f in ipairs({ "i4", "c3", "z", "s1", "b" }) do + print("G", f, pcall(string.unpack, f, "")) + print("G", f, pcall(string.unpack, f, string.rep("\\1", 40))) + print("G", f, pcall(string.unpack, f, string.rep("\\1", 40), 1)) + print("G", f, pcall(string.unpack, f, string.rep("\\1", 40), 2)) + print("G", f, pcall(string.unpack, f, string.rep("\\1", 40), 41)) + print("G", f, pcall(string.unpack, f, string.rep("\\1", 40), 42)) + print("G", f, pcall(string.unpack, f, string.rep("\\1", 40), 0)) + print("G", f, pcall(string.unpack, f, string.rep("\\1", 40), -1)) + print("G", f, pcall(string.unpack, f, string.rep("\\1", 40), -41)) + end + + -- H) 多值往返 + print("H", pcall(string.unpack, "i4i4", string.pack("i4i4", 1, 2))) + print("H", pcall(string.unpack, "i4", string.pack("i4", 1, 2))) + print("H", pcall(string.unpack, "zsi4", string.pack("zsi4", "ab", "cd", 9))) + print("H", pcall(string.unpack, "!8i8d", string.pack("!8i8d", 3, 1.25))) + print("H", pcall(string.unpack, "c3b", string.pack("c3b", "xyz", 5))) + print("H", pcall(string.pack, "i4i4", 1)) + print("H", pcall(string.pack, "i4", 1, 2)) + print("H", pcall(string.packsize)) + print("H", pcall(string.unpack)) + print("H", pcall(string.unpack, "i4")) + print("H", pcall(string.pack, 1, 2)) + print("H", pcall(string.pack, true, 2)) + """); + } + + /** + * {@code os.date} 的每个转换说明符、{@code os.time} 的表参数校验、 + * {@code utf8.char/codepoint/offset/len/codes} 的边界、{@code tonumber} 的 + * 2–36 全进制,854 行。它咬出过一处真分歧与一处平台相关的假分歧: + *
    + *
  • {@code luaL_checkinteger} 有两份实现:{@code BaseLib} 里那份 + * 既没带函数名前缀,也把「无整数表示」报成了「类型不对」。 + * {@code tonumber} 的进制、{@code select} 的下标、{@code error} 的层号、 + * {@code ipairs} 迭代器的下标——四个调用点全错。已改成转调 + * {@link LuaAux#checkInteger}。
  • + *
  • {@code os.date('!%z', t)} 是平台相关的:C 的 + * {@code strftime} 读的是进程全局时区,不是 Lua 传进去的那个 + * {@code tm};带 {@code !} 时 macOS 给本地偏移 {@code +0800}、 + * glibc 给 {@code +0000}。所以这里只测不带 {@code !} 的 + * {@code %z} 与 {@code %Z} —— 两边同一台机器,结果确定。 + * 这是继次正规 {@code %a} 之后第二个「不是我们错、是 libc 不一致」的地方。
  • + *
+ */ + @Test + void osDateAndUtf8AndBaseConversionMatchesReference() { + Diff.assertMatches(""" + -- os.date / os.time / utf8 / tonumber 的边界 + -- A) os.date 的每个转换说明符(UTC,固定时间戳,避免本地时区与当前时间) + local T = 1700000000 + -- 注意:%z / %Z 整体不进这张表。带 "!" 时两个 libc 给的答案相反 + -- (macOS 的 strftime 用全局 timezone 给本地偏移 +0800,glibc 用 + -- tm_gmtoff 给 +0000 / GMT);不带 "!" 时 %Z 是「时区名」,glibc 给 + -- GMT、Apple libc 给 UTC —— 都差,都不是错(约定 #53)。 + local SPECS = { "a", "A", "b", "B", "c", "C", "d", "D", "e", "F", "g", "G", + "h", "H", "I", "j", "m", "M", "n", "p", "r", "R", "S", "t", + "T", "u", "U", "V", "w", "W", "x", "X", "y", "Y", "%" } + for i = 1, #SPECS do + print("A", SPECS[i], pcall(os.date, "!" .. SPECS[i], T)) + end + for i = 1, #SPECS do + print("A2", SPECS[i], pcall(os.date, "!%" .. SPECS[i], T)) + end + print("A3", pcall(os.date, "%z", T)) + + -- B) *t / !*t 整表 + local ok, d = pcall(os.date, "!*t", T) + if ok then + print("B", d.year, d.month, d.day, d.hour, d.min, d.sec, d.wday, d.yday, d.isdst) + else + print("B", "err", d) + end + print("B2", pcall(os.date, "!*t")) + print("B3", pcall(os.date, "*t", T)) + print("B4", pcall(os.date, "!*t", 0)) + print("B5", pcall(os.date, "!*t", -1)) + print("B6", pcall(os.date, "!*t", 2 ^ 40)) + print("B7", pcall(os.date, "!*t", -2 ^ 40)) + print("B8", pcall(os.date, "!*t", 1.5)) + print("B9", pcall(os.date, "!*t", "x")) + print("B10", pcall(os.date, "!*t", {})) + + -- C) os.date 的非法格式 + for _, f in ipairs({ "%", "%Q", "%E", "%O", "%-", "%_", "%1", "%%%", "abc%", + "%q", "%Ez", "%Oy", "%Ec", "%EC", "%Ex", "%EY", "%Od", + "%Oe", "%OH", "%OI", "%Om", "%OM", "%OS", "%Ou", "%OU", + "%OV", "%Ow", "%OW", "%Oy" }) do + print("C", string.format("[%s]", f), pcall(os.date, "!" .. f, T)) + end + + -- D) os.time + print("D", pcall(os.time, { year = 2023, month = 11, day = 14, hour = 22, + min = 13, sec = 20 })) + print("D2", pcall(os.time, { year = 1970, month = 1, day = 1, hour = 0, + min = 0, sec = 0 })) + print("D3", pcall(os.time, { year = 1970, month = 1, day = 2 })) + print("D4", pcall(os.time, {})) + print("D5", pcall(os.time, { year = 2023 })) + print("D6", pcall(os.time, { year = 2023, month = 13, day = 1 })) + print("D7", pcall(os.time, { year = 2023, month = 0, day = 1 })) + print("D8", pcall(os.time, { year = 2023, month = 1, day = 0 })) + print("D9", pcall(os.time, { year = 2023, month = 1, day = 32 })) + print("D10", pcall(os.time, { year = 2023, month = 1, day = 1, hour = 25 })) + print("D11", pcall(os.time, { year = 2023, month = 1, day = 1, isdst = true })) + print("D12", pcall(os.time, { year = 1900, month = 1, day = 1 })) + print("D13", pcall(os.time, { year = 3000, month = 1, day = 1 })) + print("D14", pcall(os.time, "x")) + print("D15", pcall(os.time, 5)) + print("D16", pcall(os.difftime, 100, 50)) + print("D17", pcall(os.difftime, 50, 100)) + print("D18", pcall(os.difftime)) + print("D19", pcall(os.difftime, 1)) + print("D20", pcall(os.difftime, "x", "y")) + + -- E) utf8.char + for _, n in ipairs({ 0, 1, 0x7f, 0x80, 0x7ff, 0x800, 0xffff, 0x10000, 0x10ffff, + 0x110000, 0x7fffffff, 0x80000000, -1, -0x110000 }) do + local ok, r = pcall(utf8.char, n) + print("E", n, ok, ok and #r or tostring(r)) + end + print("E2", pcall(utf8.char)) + print("E3", pcall(utf8.char, 1, 2, 3)) + print("E4", pcall(utf8.char, 1, "x")) + print("E5", pcall(utf8.char, 1.0)) + print("E6", pcall(utf8.char, 1.5)) + print("E7", pcall(utf8.char, 0x110000, 1)) + + -- F) utf8.codepoint / offset / len + local S = "hello" .. utf8.char(0x4e2d) .. utf8.char(0x6587) .. "ab" + for i = -12, 12 do + print("F", i, pcall(utf8.codepoint, S, i)) + end + for i = -12, 12 do + for j = i, i + 2 do + print("F2", i, j, pcall(utf8.codepoint, S, i, j)) + end + end + for i = -12, 12 do + print("F3", i, pcall(utf8.offset, S, i)) + end + for i = -12, 12 do + print("F4", i, pcall(utf8.offset, S, i, 1)) + end + print("F5", pcall(utf8.codepoint, "")) + print("F6", pcall(utf8.codepoint, S, 1, 100)) + print("F7", pcall(utf8.codepoint, S, 0)) + print("F8", pcall(utf8.codepoint, 5)) + print("F9", pcall(utf8.offset, S, 1, 100)) + print("F10", pcall(utf8.len, S)) + print("F11", utf8.charpattern) + print("F12", pcall(utf8.len, "\\xff")) + print("F13", pcall(utf8.len, "\\xc3\\xa9")) + print("F14", pcall(utf8.len, "abc", 2)) + print("F15", pcall(utf8.len, "abc", 2, 3)) + print("F16", pcall(utf8.len, "abc", -1)) + print("F17", pcall(utf8.len, "abc", 0)) + print("F18", pcall(utf8.len, "abc", 5)) + print("F19", pcall(utf8.len, 5)) + local n = 0 + for p, c in utf8.codes(S) do n = n + 1 end + print("F20", n, pcall(utf8.codes, "\\xff"), pcall(utf8.codes, 5)) + + -- G) tonumber 全进制 + for base = 2, 36 do + for _, s in ipairs({ "0", "1", "z", "Z", "-1", "-z", "10", "ff", "FF", "", + " 1 ", "1.5", "0x10", "+1", "1e2" }) do + print("G", base, string.format("[%s]", s), pcall(tonumber, s, base)) + end + end + print("G2", pcall(tonumber, "10", 1)) + print("G3", pcall(tonumber, "10", 37)) + print("G4", pcall(tonumber, "10", 1.5)) + print("G5", pcall(tonumber, "10", "10")) + print("G6", pcall(tonumber, 10, 10)) + print("G7", pcall(tonumber, nil)) + print("G8", pcall(tonumber, nil, 10)) + print("G9", tonumber(" 12 "), tonumber("0x1p4"), tonumber("0X10"), + tonumber(".5"), tonumber("5."), tonumber("1e"), tonumber("inf"), + tonumber("nan"), tonumber("-0"), tonumber("0xfffffffffffff"), + tonumber("0xffffffffffffffff")) + print("G10", math.tointeger(1), math.tointeger(1.0), math.tointeger(1.5), + math.tointeger("3"), math.tointeger(1 / 0), math.tointeger(0 / 0), + math.tointeger(nil), math.tointeger(2 ^ 63)) + """); + } + + /** + * {@code table} 库矩阵:{@code insert/remove/move/concat/unpack/pack/sort} + * 的位置 × 表大小 × 偏移 × 值类型,含同表重叠 {@code move} 与跨表 + * {@code move},6277 行。它咬出过一处真分歧: + *

+ * {@code tmove} 的正反向分支漏了一个条件。 C 是 + * {@code t > e || t <= f || (tt != 1 && !lua_compare(1, tt, OPEQ))}, + * 我们只写了前两项。第三项的含义是:源和目标是两张不同的表时一律正向 + * 拷贝,与 {@code t}、{@code f} 的相对位置无关。漏掉它,本该正向的 + * 情形会走成逆向。 + *

+ * 难缠的地方在于内容完全一致,只有 {@code #t} 不一样: + * {@code table.move({1}, -2, 1, -1, {1})} 之后目标表同样只剩 + * {@code [2]=1},参考实现 {@code #t == 0},我们给 {@code 2}。原因是插入 + * 次序反了 —— 正向先把 {@code [1]} 挖空,{@code alimit} 仍是 1; + * 逆向却先塞 {@code [2]},把 {@code alimit} 顶到 2, + * {@code luaH_getn} 的第一条分支(「{@code array[alimit-1]} 为 nil 就只在 + * 数组部分二分」)随之不再成立。所以「写入序列相同、走 {@code rawset} 就对、 + * 走 {@code table.move} 就错」—— 差别只在内部布局。 + *

+ * 反过来也说明一件事:光比对表内容是不够的,得连 {@code #t} + * 一起比,否则这类「布局」分歧永远不会被发现。 + */ + @Test + void tableLibraryMatrixMatchesReference() { + Diff.assertMatches(""" + -- table 库矩阵:insert / remove / move / concat / unpack / pack / sort 的边界 + local function dump(tag, t) + local parts = {} + for i = -2, 8 do + parts[#parts + 1] = tostring(i) .. "=" .. tostring(rawget(t, i)) + end + print(tag, "#" .. tostring(#t), table.concat(parts, ",")) + end + + local function fresh(n) + local t = {} + for i = 1, n do t[i] = i end + return t + end + + -- 1) insert:位置取值 × 表大小 + for n = 0, 4 do + for pos = -6, 8 do + local t = fresh(n) + print("I", n, pos, pcall(table.insert, t, pos, 99)) + dump("I-dump", t) + local t2 = fresh(n) + print("I2", n, pos, pcall(table.insert, t2, pos, 99, 98)) + dump("I2-dump", t2) + end + end + + -- 2) insert:参数与非数组表 + print("IA", pcall(table.insert, {}, 1)) + print("IB", pcall(table.insert, {}, 1, 2, 3, 4)) + print("IC", pcall(table.insert)) + print("ID", pcall(table.insert, 5, 1)) + print("IE", pcall(table.insert, "x", 1)) + print("IF", pcall(table.insert, {}, "x", 1)) + print("IG", pcall(table.insert, {}, 1.5, 1)) + print("IH", pcall(table.insert, {}, 1.0, 1)) + print("II", pcall(table.insert, {}, 2, 1)) + print("IJ", pcall(table.insert, { 1, 2, 3 }, 4, 9)) + local big = {} + for i = 1, 100 do big[i] = i end + print("IK", pcall(table.insert, big, 101, 1)) + print("IL", pcall(table.insert, big, 102, 1)) + print("IM", pcall(table.insert, big, 0, 1)) + print("IN", pcall(table.insert, big, -101, 1)) + print("IO", pcall(table.insert, big, math.maxinteger, 1)) + print("IP", pcall(table.insert, big, math.mininteger, 1)) + + -- 3) remove:位置取值 × 表大小 + for n = 0, 4 do + for pos = -6, 8 do + local t = fresh(n) + local ok, v = pcall(table.remove, t, pos) + print("R", n, pos, ok, tostring(v)) + dump("R-dump", t) + end + end + print("RA", pcall(table.remove, {})) + print("RB", pcall(table.remove, 5)) + print("RC", pcall(table.remove, {}, 1, 2)) + print("RD", pcall(table.remove, {}, "x")) + print("RE", pcall(table.remove, {}, 1.5)) + print("RF", pcall(table.remove, big)) + print("RG", pcall(table.remove, big, 1)) + print("RH", pcall(table.remove, big, 101)) + print("RI", pcall(table.remove, big, 0)) + + -- 4) move:同源重叠 / 跨表 / 各种越界 + -- table.move 返回的是目标表,地址每次都不一样,只留状态与错误文本 + local function mv(a1, f, e, t) + local ok, err = pcall(table.move, a1, f, e, t) + return ok and "ok" or ("err:" .. tostring(err)) + end + for n = 0, 3 do + for f = -2, 5 do + for e = f - 1, 5 do + for t = -2, 5 do + local a = fresh(n) + print("MV", n, f, e, t, mv(a, f, e, t)) + dump("MV-dump", a) + end + end + end + end + for n = 0, 3 do + for f = -2, 5 do + for e = f - 1, 5 do + for t = -2, 5 do + local a, b = fresh(n), fresh(n) + local mok, merr = pcall(table.move, a, f, e, t, b) + print("MV2", n, f, e, t, mok and "ok" or ("err:" .. tostring(merr))) + dump("MV2-dump", b) + end + end + end + end + local ma, mb = pcall(table.move, {}, 1, 2, 3) + print("MVA", ma, type(mb)) + print("MVB", pcall(table.move, {}, 1, 2)) + print("MVC", pcall(table.move, {}, 1)) + print("MVD", pcall(table.move, {})) + print("MVE", pcall(table.move, 5, 1, 2, 3, {})) + print("MVF", pcall(table.move, {}, "x", 2, 3, {})) + print("MVG", pcall(table.move, {}, 1, "x", 3, {})) + print("MVH", pcall(table.move, {}, 1, 2, "x", {})) + print("MVI", pcall(table.move, {}, 1.5, 2, 3, {})) + print("MVJ", pcall(table.move, {}, 1, 2, 3, 1, 2)) + local ka, kb = pcall(table.move, {}, 1, 2, 3, {}, 5) + print("MVK", ka, type(kb)) + print("MVL", pcall(table.move, {}, 1, 1.5, 3, {})) + + -- 5) concat:值类型 × 空洞 × 分隔符 × 区间 + local VALS = { nil, true, 1, 1.5, "s", {}, print } + local SEPS = { nil, "", ",", "xy" } + for i = 1, #VALS do + local t = { VALS[i] } + for s = 1, #SEPS do + print("C", i, s, pcall(table.concat, t, SEPS[s])) + end + print("C2", i, pcall(table.concat, t, ",", 1, 1)) + print("C3", i, pcall(table.concat, t, ",", 2, 1)) + print("C4", i, pcall(table.concat, t, ",", 0, 1)) + print("C5", i, pcall(table.concat, t, ",", -1, 1)) + end + print("CA", table.concat({ 1, 2, 3 })) + print("CB", table.concat({ 1, 2, 3 }, "-")) + print("CC", table.concat({ 1, 2, 3 }, "-", 2)) + print("CD", table.concat({ 1, 2, 3 }, "-", 2, 3)) + print("CE", table.concat({}, "-")) + print("CF", pcall(table.concat, { 1, nil, 3 })) + print("CG", pcall(table.concat, 5)) + print("CH", pcall(table.concat, {}, 5)) + print("CI", pcall(table.concat, { 1, 2 }, ",", "x")) + print("CJ", pcall(table.concat, { 1, 2 }, ",", 1, "y")) + print("CK", pcall(table.concat)) + print("CL", pcall(table.concat, { 1, 2 }, ",", 1, 3)) + print("CM", pcall(table.concat, { 1, 2 }, ",", 1, 100)) + + -- 6) unpack:区间与上限 + for i = -4, 4 do + for j = i - 1, 4 do + print("U", i, j, pcall(table.unpack, { 1, 2, 3 }, i, j)) + end + end + print("UA", pcall(table.unpack, {})) + print("UB", pcall(table.unpack, 5)) + print("UC", pcall(table.unpack, { 1, 2 }, "x")) + print("UD", pcall(table.unpack, { 1, 2 }, 1, "y")) + print("UE", pcall(table.unpack, { 1, 2 }, 1.5)) + print("UF", pcall(table.unpack, { 1, 2 }, 1, 2, 3)) + print("UG", pcall(table.unpack, {}, 1, 0)) + print("UH", pcall(table.unpack, {}, 2, 1)) + print("UI", pcall(table.unpack, {}, -1, 0)) + local many = {} + for i = 1, 2000 do many[i] = i end + print("UJ", pcall(function() return #table.pack(table.unpack(many)) end)) + print("UK", pcall(function() return table.unpack(many, 1, 1000000) end)) + print("UL", pcall(function() return table.unpack(many, -1000000, -1) end)) + + -- 7) pack + local pa, pb = pcall(table.pack) + print("P", pa, type(pb), pb == nil and "nil" or pb.n) + local p2a, p2b = pcall(table.pack, nil) + print("P2", p2a, type(p2b), p2b == nil and "nil" or p2b.n) + local p3a, p3b = pcall(table.pack, nil, nil) + print("P3", p3a, type(p3b), p3b == nil and "nil" or p3b.n) + local pk = table.pack(1, nil, 3, nil) + print("P4", pk.n, pk[1], pk[2], pk[3], pk[4], #pk) + print("P5", table.pack("a", "b").n) + + -- 8) sort:比较函数异常 / 非法次序 / 相等元素 / 非数组 + local function cmpOk(a, b) return a < b end + print("SA", pcall(table.sort, { 3, 1, 2 }, cmpOk)) + local s1 = { 3, 1, 2 } + table.sort(s1) + print("SB", s1[1], s1[2], s1[3]) + local s2 = { 3, 1, 2 } + table.sort(s2, function(a, b) return a > b end) + print("SC", s2[1], s2[2], s2[3]) + print("SD", pcall(table.sort, { 1, 2 }, function(a, b) return true end)) + print("SE", pcall(table.sort, { 1, 2, 3 }, function(a, b) return true end)) + print("SF", pcall(table.sort, { 1, 2 }, function(a, b) error("boom") end)) + print("SG", pcall(table.sort, { 1, 2 }, function(a, b) return nil end)) + print("SH", pcall(table.sort, { 1, 2 }, 5)) + print("SI", pcall(table.sort, { 1, 2 }, "x")) + print("SJ", pcall(table.sort, 5)) + print("SK", pcall(table.sort, {})) + print("SL", pcall(table.sort, { 1 }, function(a, b) return a < b end)) + print("SM", pcall(table.sort, { 1, 2 }, function(a, b) return a < b end, 1)) + local s3 = { 5, 5, 5 } + table.sort(s3) + print("SN", s3[1], s3[2], s3[3]) + print("SO", pcall(table.sort, { 1, 2, 3 }, function(a, b) return a <= b end)) + local s4 = { "b", "a" } + table.sort(s4) + print("SP", s4[1], s4[2]) + print("SQ", pcall(table.sort, { 1, "a" }, cmpOk)) + print("SR", pcall(table.sort, { 0 / 0, 1 }, cmpOk)) + """); + } + + /** + * {@code string} 库矩阵:模式匹配(各类 {@code %} 类、集合、锚点、捕获、 + * {@code %b}、{@code %f}、贪婪/非贪婪)× 主题串(含空串、制表、换行、 + * 多字节 UTF-8)× {@code init} 越界 × 三种替换形态(串/表/函数),24819 行。 + *

+ * 这一片是干净的:铺开前心里没底,铺开后一处分歧都没有。照第 25 条 + * 经验(已经一致的也钉住),照样固化成常驻测试 —— 哪天有人动 + * {@code lstrlib.c} 的对应分支,这里会替他说话。 + */ + @Test + void patternMatchingMatrixMatchesReference() { + Diff.assertMatches(""" + -- string 库矩阵:模式匹配 × 主题 × init × 替换形态,外加下标类函数 + local TAB = string.char(9) + local NL = string.char(10) + local E9 = string.char(0xC3, 0xA9) -- 单个 UTF-8 字符,占两个字节 + + local PATTERNS = { + "", "a", "abc", "a*", "a+", "a-", "a?", ".", ".*", ".+", ".-", + "%a", "%a+", "%a*", "%d", "%d+", "%s", "%s+", "%w", "%w+", "%p", "%c", "%x+", + "%u", "%l", "%A", "%D", "%S", "%W", + "[abc]", "[^abc]", "[a-c]", "[a-c]+", "[%a]", "[%a%d]", "[^%d]+", + "(%a)", "(%a)(%d)", "(%a+)", "()%a", "()%a+", "((%a)(%d))", + "^a", "^a+", "a$", "^ab$", "^", "$", "^$", + "%bxy", "%b()", "%f[%a]%a+", "%f[%w]", + "a*b", "a-b", "a+b", "a?b", "[a-c]*d", "%a%s*%d", "(%w+)%s*=%s*(%w+)", + } + + local SUBJECTS = { + "", "a", "abc", "aaa", "a1b2c3", " x ", "hello world", "x,y,z", + "aXbXc", "123", "abc123", E9 .. "ab", "()", "x(y)z", "a=b", "k = v", + "AaBb", " ", TAB .. NL, "xy", + } + + -- 1) find:模式 × 主题 × init × plain + for p = 1, #PATTERNS do + for s = 1, #SUBJECTS do + for _, init in ipairs({ nil, 1, 2, -1, -3, 0, 100, -100 }) do + for _, plain in ipairs({ nil, false, true }) do + local ok, a, b, c, d = pcall(string.find, SUBJECTS[s], PATTERNS[p], + init, plain) + print("F", p, s, tostring(init), tostring(plain), ok, + tostring(a), tostring(b), tostring(c), tostring(d)) + end + end + end + end + + -- 2) match:模式 × 主题 × init + for p = 1, #PATTERNS do + for s = 1, #SUBJECTS do + for _, init in ipairs({ nil, 1, 3, -2, 0, 100 }) do + local ok, a, b, c = pcall(string.match, SUBJECTS[s], PATTERNS[p], init) + print("M", p, s, tostring(init), ok, tostring(a), tostring(b), tostring(c)) + end + end + end + + -- 3) gmatch:模式 × 主题,收集全部(含位置捕获) + for p = 1, #PATTERNS do + for s = 1, #SUBJECTS do + local ok, it = pcall(string.gmatch, SUBJECTS[s], PATTERNS[p]) + if not ok then + print("G", p, s, "err", tostring(it)) + else + local n, acc = 0, {} + local gok, a, b, c = pcall(it) + while gok and a ~= nil and n < 40 do + n = n + 1 + acc[#acc + 1] = tostring(a) .. "," .. tostring(b) .. "," .. tostring(c) + gok, a, b, c = pcall(it) + end + print("G", p, s, "ok", n, table.concat(acc, "|"), tostring(gok)) + end + end + end + + -- 4) gsub:模式 × 替换形态 × 主题 × n + local REPLS_S = { "", "x", "%0", "%1", "%2", "%1%1", "%0%1", "%%", "%9", + "a%1b", "%", "%x", "%z" } + for p = 1, #PATTERNS do + for r = 1, #REPLS_S do + for s = 1, #SUBJECTS do + local ok, a, b = pcall(string.gsub, SUBJECTS[s], PATTERNS[p], REPLS_S[r]) + print("S", p, r, s, ok, tostring(a), tostring(b)) + end + end + end + + -- 5) gsub 的替换是表 / 函数 / 数字 / 布尔 + local function repfn(k) return "[" .. tostring(k) .. "]" end + for p = 1, #PATTERNS do + for s = 1, #SUBJECTS do + print("T", p, s, pcall(string.gsub, SUBJECTS[s], PATTERNS[p], + { a = "A", b = "B", ["1"] = "ONE" })) + print("U", p, s, pcall(string.gsub, SUBJECTS[s], PATTERNS[p], repfn)) + print("V", p, s, pcall(string.gsub, SUBJECTS[s], PATTERNS[p], 5)) + print("W", p, s, pcall(string.gsub, SUBJECTS[s], PATTERNS[p], true)) + print("X", p, s, pcall(string.gsub, SUBJECTS[s], PATTERNS[p], nil)) + end + end + + -- 6) gsub 的 n 上限(含 0 与负) + for _, n in ipairs({ 0, 1, 2, 3, -1, 1.0, 1.5, "2" }) do + print("N", tostring(n), pcall(string.gsub, "aaaa", "a", "b", n)) + end + print("N2", pcall(string.gsub, "aaaa", "a", "b", "x")) + print("N3", pcall(string.gsub, "aaaa", "a", "b", {})) + + -- 7) 下标类:sub / byte / len / rep / reverse / upper / lower + local SS = { "", "a", "abc", "hello", E9 .. "ab" } + for s = 1, #SS do + local str = SS[s] + for i = -6, 6 do + print("sub", s, i, pcall(string.sub, str, i)) + for j = -6, 6 do + print("sub2", s, i, j, pcall(string.sub, str, i, j)) + end + print("byte", s, i, pcall(string.byte, str, i)) + print("byte2", s, i, i + 2, pcall(string.byte, str, i, i + 2)) + end + print("len", s, #str, pcall(string.len, str)) + print("rev", s, pcall(string.reverse, str)) + print("up", s, pcall(string.upper, str)) + print("low", s, pcall(string.lower, str)) + print("rep", s, pcall(string.rep, str, 0)) + print("rep2", s, pcall(string.rep, str, 3)) + print("rep3", s, pcall(string.rep, str, 3, "-")) + print("rep4", s, pcall(string.rep, str, 3, "")) + print("rep5", s, pcall(string.rep, str, -1)) + print("rep6", s, pcall(string.rep, str, 2, "xy")) + end + + -- 8) 参数类型错误 + for _, fn in ipairs({ string.find, string.match, string.gmatch, string.gsub, + string.sub, string.byte, string.rep, string.reverse, + string.upper, string.lower, string.len }) do + print("E1", pcall(fn)) + print("E2", pcall(fn, nil)) + print("E3", pcall(fn, 5)) + print("E4", pcall(fn, true)) + print("E5", pcall(fn, {})) + end + print("E6", pcall(string.find, "abc", 5)) + print("E7", pcall(string.find, "abc", "a", "x")) + print("E8", pcall(string.gsub, "abc", "a", "b", "c")) + print("E9", pcall(string.char, -1)) + print("E10", pcall(string.char, 256)) + print("E11", pcall(string.char, 1.5)) + print("E12", pcall(string.char, "x")) + print("E13", pcall(string.char)) + print("E14", pcall(string.rep, "a", 1e10)) + print("E15", pcall(string.sub, "abc", 1, 1, 1)) + """); + } + + /** + * {@code #t} 单独铺开扫,3736 行。起因就是上面那处 {@code tmove} 分歧: + * 它表现出来的样子是「表内容一模一样,只有 {@code #t} 不一样」。既然 + * {@code luaH_getn} 的答案取决于内部布局、布局又取决于插入历史,那就把 + * 「同一份内容、不同构造路径」这件事本身当成扫描维度: + *

    + *
  • {@code A}:{@code 1..n} 的全部子集 × 三种插入次序(正序、 + * 逆序、先奇后偶)—— 内容相同,只有次序不同;
  • + *
  • {@code B}:先填满再按子集挖空,走 rehash 的收缩路径;
  • + *
  • {@code C}:挖空后往边界外补,看 {@code alimit} 会不会被顶回去;
  • + *
  • {@code D}:{@code table.move} 造布局,跨表(一律正向)与同表 + * (可能逆向)分开跑 —— 就是出事的那条路径;
  • + *
  • {@code E}:{@code insert/remove} 交替把长度来回推;
  • + *
  • {@code F}:负键、0 键混进来,只撑哈希不撑数组;
  • + *
  • {@code G}/{@code H}:大到 40 的二分收敛点、尾部连续挖空。
  • + *
+ * 这一片干净,但仍固化:{@code luaH_getn} 的每一条分支都有可能被 + * 后来的改动碰歪,而这类歪斜只有把 {@code #t} 和内容一起比才看得见。 + */ + @Test + void tableLengthAcrossInsertionHistoriesMatchesReference() { + Diff.assertMatches(""" + -- 长度矩阵:#t 只跟「边界」有关,但边界由内部布局决定,布局由插入历史决定。 + -- 所以同一份内容用不同次序/不同函数构造,#t 可以不同 —— 这片就是扫这个。 + local function dump(tag, t) + local p = {} + for i = -3, 14 do p[#p + 1] = tostring(i) .. "=" .. tostring(rawget(t, i)) end + print(tag, "#" .. tostring(#t), table.concat(p, ",")) + end + + local ORDERS = { + { "fwd", function(n) local o = {} for i = 1, n do o[i] = i end return o end }, + { "rev", function(n) local o = {} for i = 1, n do o[i] = n - i + 1 end return o end }, + { "oe", function(n) + local o = {} + for i = 1, n, 2 do o[#o + 1] = i end + for i = 2, n, 2 do o[#o + 1] = i end + return o + end }, + } + + -- A) 子集 × 插入顺序:内容相同、次序不同,看 #t 是否跟着变 + for n = 0, 8 do + for mask = 0, 2 ^ n - 1 do + for oi = 1, #ORDERS do + local name, mk = ORDERS[oi][1], ORDERS[oi][2] + local t, order = {}, mk(n) + for k = 1, #order do + if mask & (1 << (order[k] - 1)) ~= 0 then t[order[k]] = order[k] end + end + dump(string.format("A n=%d m=%d %s", n, mask, name), t) + end + end + end + + -- B) 先填满再挖空:rehash 的收缩路径 + for n = 0, 8 do + for mask = 0, 2 ^ n - 1 do + local t = {} + for i = 1, n do t[i] = i end + for i = 1, n do + if mask & (1 << (i - 1)) ~= 0 then t[i] = nil end + end + dump(string.format("B n=%d m=%d", n, mask), t) + end + end + + -- C) 挖空后再往边界外补 + for n = 0, 6 do + for hole = 1, n do + local t = {} + for i = 1, n do t[i] = i end + t[hole] = nil + t[n + 1] = 99 + dump(string.format("C n=%d h=%d", n, hole), t) + local t2 = {} + for i = 1, n do t2[i] = i end + t2[hole] = nil + t2[n + 1] = 99 + t2[n + 2] = 98 + dump(string.format("C2 n=%d h=%d", n, hole), t2) + end + end + + -- D) table.move 造出的布局:跨表(一律正向)与同表(可能逆向) + for n = 0, 4 do + for f = -2, 4 do + for t = -2, 4 do + local a, b = {}, {} + for i = 1, n do a[i] = i; b[i] = i end + table.move(a, f, n, t, b) + dump(string.format("D n=%d f=%d t=%d", n, f, t), b) + local c = {} + for i = 1, n do c[i] = i end + table.move(c, f, n, t) + dump(string.format("D2 n=%d f=%d t=%d", n, f, t), c) + end + end + end + + -- E) insert / remove 交替,把长度来回推 + for n = 0, 4 do + local t = {} + for i = 1, n do t[i] = i end + for k = 1, 6 do + table.insert(t, 90 + k) + dump(string.format("E n=%d k=%d ins", n, k), t) + table.remove(t) + dump(string.format("E n=%d k=%d rem", n, k), t) + end + end + + -- F) 负键 / 0 键混进来:它们进哈希,不影响数组边界,但会撑大哈希部分 + for n = 0, 4 do + for extra = -3, 1 do + local t = {} + for i = 1, n do t[i] = i end + t[extra] = 7 + dump(string.format("F n=%d e=%d", n, extra), t) + local t2 = {} + for i = 1, n do t2[i] = i end + t2[extra] = nil + dump(string.format("F2 n=%d e=%d", n, extra), t2) + end + end + + -- G) 大表边界:二分收敛的地方 + for n = 1, 40 do + for hole = 1, n do + local u = {} + for i = 1, n do u[i] = i end + u[hole] = nil + print("G", n, hole, #u) + end + end + + -- H) 尾部连续挖空:alimit 与真实边界分离 + for n = 1, 20 do + for cut = 0, n do + local t = {} + for i = 1, n do t[i] = i end + for i = cut + 1, n do t[i] = nil end + print("H", n, cut, #t) + end + end + """); + } + + /** + * {@code io} 库矩阵:32 种打开模式(含全部非法模式)、{@code read} 的全部格式 + * 与 0–24 字节计数、数字读法、写各类值、{@code seek} 的三 whence、 + * {@code lines}、{@code close} 后再用、{@code io.input/output/read/write/close}、 + * 追加模式的 {@code seek} 语义、{@code r+} 读写交错、5000 字节大块、 + * 同一文件开两次,167 行。干净。 + *

+ * 值得钉住的理由不是它找出了什么,而是它覆盖了errno 文案这一层: + * {@code io.open} 失败要返回 {@code "路径: No such file or directory"} 加 + * 错误号 {@code 2},那是 C 的 {@code strerror} 给的,我们得自己对齐。 + * 这部分一旦对不上,没有任何手写用例会说话。 + *

+ * 脚本里有一处自己坑自己的地方,留着当提醒:{@code io.output(5)} + * 会在当前目录建一个名叫 {@code 5} 的文件,而差分两边跑在同一个 cwd, + * 于是第二趟的 {@code io.open(5)} 就从「打不开」变成「打开了」——第一版 + * 跑出两行假分歧,查了半天。凡是脚本有文件系统副作用的,开头结尾都要清。 + */ + @Test + void ioLibraryMatrixMatchesReference() { + Diff.assertMatches(""" + -- io 库矩阵:打开模式 / 读格式 / 写 / seek / lines / close / 错误路径 + -- 注意:绝对路径与 errno 文案都可能出现,这里一律不打印路径本身。 + local function ok(tag, ...) + print(tag, ...) + end + + local function pc(tag, f, ...) + -- pcall,但把 userdata 和函数都换成稳定的字符串,只留类型名 + local r = { pcall(f, ...) } + for i = 2, #r do + local v = r[i] + local t = type(v) + if t == "userdata" then + r[i] = "" + elseif t == "function" then + r[i] = "" + elseif t == "string" then + r[i] = string.format("%q", v) + end + end + print(tag, table.unpack(r, 1, #r)) + end + + local name = os.tmpname() + local path = name + + -- 1) open:全部模式组合(含无效模式) + local MODES = { + "r", "w", "a", "r+", "w+", "a+", "rb", "wb", "ab", "r+b", "w+b", "a+b", + "rb+", "wb+", "ab+", "br", "bw", "rt", "wt", "rw", "wr", "ar", "ra", + "", "x", "rx", "r++", "+", "r+b+", "r b", "R", "W", "A", + } + for i = 1, #MODES do + local m = MODES[i] + local pok, f, e = pcall(io.open, path, m) + if not pok then + print("O", string.format("%q", m), "err", string.format("%q", tostring(f))) + elseif f == nil then + print("O", string.format("%q", m), "nil", string.format("%q", tostring(e))) + else + print("O", string.format("%q", m), "opened", io.type(f)) + f:close() + print("O2", string.format("%q", m), io.type(f)) + end + end + + -- 2) 参数错误 + -- G5 的 io.output(5) 会在当前目录建一个名叫 5 的文件;两边跑同一个 cwd, + -- 第二趟就会看见它。所以每处用到名字 "5" 的地方前后都要清掉。 + os.remove("5") + pc("E1", io.open) + pc("E2", io.open, nil) + pc("E3", io.open, 5) + pc("E4", io.open, true) + pc("E5", io.open, {}) + pc("E6", io.open, path, 5) + pc("E7", io.open, path, nil) + pc("E8", io.open, path, "r", 5) + pc("E9", io.open, "/nonexistent-dir-xyz/nonexistent-file") + pc("E10", io.open, "/nonexistent-dir-xyz/nonexistent-file", "w") + pc("E11", io.open, "/nonexistent-dir-xyz/", "r") + + -- 3) 写 + 读:各种 read 格式 + local function withcontent(body) + local f = assert(io.open(path, "w")) + f:write("line1\\nline2\\nline3") + f:close() + local g = assert(io.open(path, "r")) + local r = { pcall(body, g) } + g:close() + return table.unpack(r, 1, #r) + end + + local FMTS = { "l", "L", "a", "n", "line", "*l", "*L", "*a", "*n", "*line" } + for i = 1, #FMTS do + pc(string.format("R %q", FMTS[i]), function(g) + local h = assert(io.open(path, "r")) + local a, b, c = h:read(FMTS[i], FMTS[i], FMTS[i]) + h:close() + return a, b, c + end) + end + for n = 0, 24 do + pc(string.format("RN %d", n), function() + local h = assert(io.open(path, "r")) + local v = h:read(n) + h:close() + return v + end) + end + -- 数字格式:各种前缀 + for _, s in ipairs({ "42 rest", "0x10", " 3.5", "-7", "1e3", "abc", "", "\\n5", + "0xZZ", "3", " 12 ", "inf", "nan", ".5", "5.", "0X1p4" }) do + pc(string.format("RD %q", s), function() + local f = assert(io.open(path, "w")) + f:write(s) + f:close() + local h = assert(io.open(path, "r")) + local a, b = h:read("n", "n") + h:close() + return a, b + end) + end + -- 混合格式串 + pc("RM1", function() + local h = assert(io.open(path, "r")) + local a, b, c, d = h:read("l", 3, "a", "l") + h:close() + return a, b, c, d + end) + pc("RM2", function() + local h = assert(io.open(path, "r")) + local a = h:read("a", "a", "l") + h:close() + return a + end) + pc("RM3", function() + local h = assert(io.open(path, "r")) + local a = h:read(0) + h:close() + return a + end) + pc("RM4", function() + local h = assert(io.open(path, "r")) + local a = h:read("x") + h:close() + return a + end) + pc("RM5", function() + local h = assert(io.open(path, "r")) + local a = h:read(5, "x") + h:close() + return a + end) + pc("RM6", function() + local h = assert(io.open(path, "r")) + local a = h:read() + h:close() + return a + end) + pc("RM7", function() + local h = assert(io.open(path, "r")) + local a = h:read("a") + local b = h:read("a") + h:close() + return a, b + end) + + -- 4) write:类型与返回值 + pc("W1", function() + local h = assert(io.open(path, "w")) + local r = h:write("a", 1, 2.5, "b") + h:close() + return r + end) + pc("W2", function() + local h = assert(io.open(path, "w")) + local r = h:write() + h:close() + return r + end) + pc("W3", function() + local h = assert(io.open(path, "w")) + local r = h:write(nil) + h:close() + return r + end) + pc("W4", function() + local h = assert(io.open(path, "w")) + local r = h:write(true) + h:close() + return r + end) + pc("W5", function() + local h = assert(io.open(path, "w")) + local r = h:write({}) + h:close() + return r + end) + pc("W6", function() + local h = assert(io.open(path, "w")) + local r = h:write(7) + h:close() + local g = assert(io.open(path, "r")) + local s = g:read("a") + g:close() + return r, s + end) + pc("W7", function() + local h = assert(io.open(path, "w")) + local r = h:write(1.0) + h:close() + local g = assert(io.open(path, "r")) + local s = g:read("a") + g:close() + return r, s + end) + pc("W8", function() + local h = assert(io.open(path, "r")) + local r = h:write("x") + h:close() + return r + end) + + -- 5) seek + pc("S1", function() + local h = assert(io.open(path, "w")) + h:write("0123456789") + h:close() + local g = assert(io.open(path, "r")) + local a = g:seek() + local b = g:seek("set") + local c = g:seek("cur") + local d = g:seek("end") + local e2 = g:seek("set", 3) + local f2 = g:read(2) + local g2 = g:seek("cur", -1) + local h2 = g:read(2) + local i2 = g:seek("set", 100) + local j2 = g:read("l") + local k2 = g:seek("set", -1) + g:close() + return a, b, c, d, e2, f2, g2, h2, i2, j2, k2 + end) + pc("S2", function() + local g = assert(io.open(path, "r")) + local a = g:seek("bad") + g:close() + return a + end) + pc("S3", function() + local g = assert(io.open(path, "r")) + local a = g:seek("set", "x") + g:close() + return a + end) + pc("S4", function() + local g = assert(io.open(path, "r")) + local a = g:seek(nil, 1) + g:close() + return a + end) + pc("S5", function() + local g = assert(io.open(path, "r")) + local a = g:seek(1) + g:close() + return a + end) + + -- 6) lines + pc("L1", function() + local n = 0 + for l in io.lines(path) do n = n + 1 end + return n + end) + pc("L2", function() + local h = assert(io.open(path, "w")) + h:write("a\\nb\\nc\\n") + h:close() + local t = {} + for l in io.lines(path, "l") do t[#t + 1] = l end + return #t, t[1], t[2], t[3] + end) + pc("L3", function() + local f = assert(io.open(path, "r")) + local t = {} + for l in f:lines() do t[#t + 1] = l end + f:close() + return #t + end) + pc("L4", function() + local f = assert(io.open(path, "r")) + local n = 0 + for l in f:lines("l", "l") do n = n + 1 end + f:close() + return n + end) + pc("L5", io.lines, "/nonexistent-dir-xyz/nope") + + -- 7) close / type / 关闭后再用 + pc("C1", function() + local f = assert(io.open(path, "r")) + local a = io.type(f) + local b = f:close() + local c = io.type(f) + local d = f:close() + return a, b, c, d + end) + pc("C2", function() + local f = assert(io.open(path, "r")) + f:close() + return f:read("l") + end) + pc("C3", function() + local f = assert(io.open(path, "r")) + f:close() + return f:write("x") + end) + pc("C4", function() + local f = assert(io.open(path, "r")) + f:close() + return f:seek() + end) + pc("C5", io.type, 5) + pc("C6", io.type, "x") + pc("C7", io.type, nil) + pc("C8", io.type, {}) + + -- 8) io.read / io.write / io.input / io.output + pc("G1", function() + local old = io.output() + local w = io.output(path) + io.write("abc") + io.output(old) + io.output(w) + io.close(w) + local h = assert(io.open(path, "r")) + local s = h:read("a") + h:close() + return s + end) + pc("G2", function() + local old = io.input() + local f = assert(io.open(path, "w")) + f:write("z\\n") + f:close() + local r = io.input(path) + local s = io.read() + io.input(old) + io.close(r) + return s + end) + pc("G3", io.input, 5) + pc("G4", io.input, "/nonexistent-dir-xyz/nope") + pc("G5", io.output, 5) + os.remove("5") + io.output(io.stdout) + pc("G6", io.close, 5) + pc("G7", io.close, {}) + + -- 9) 标准句柄 + print("H1", io.type(io.stdout), io.type(io.stdin), io.type(io.stderr)) + + -- 10) 二进制与 \\0 / \\r\\n + pc("B1", function() + local f = assert(io.open(path, "wb")) + f:write("a\\0b\\r\\nc\\n") + f:close() + local g = assert(io.open(path, "rb")) + local s = g:read("a") + g:close() + return #s, (string.gsub(s, ".", function(c) + return string.format("[%d]", string.byte(c)) + end)) + end) + pc("B2", function() + local f = assert(io.open(path, "wb")) + f:write("a\\0b\\r\\nc\\n") + f:close() + local g = assert(io.open(path, "r")) + local a, b, c = g:read("l", "l", "l") + g:close() + return a, b, c + end) + + -- 11) flush / setvbuf / 长行 + pc("V1", function() + local f = assert(io.open(path, "w")) + local a = f:flush() + local b = f:setvbuf("no") + local c = f:setvbuf("full", 1024) + local d = f:setvbuf("line") + local e2 = f:setvbuf("bad") + f:close() + return a, b, c, d, e2 + end) + + -- 12) 追加模式的 seek 语义 / r+ 读写交错 / 大块 / 越界 + pc("A1", function() + local f = assert(io.open(path, "w")) + f:write("12345") + f:close() + local g = assert(io.open(path, "a")) + g:seek("set", 0) + g:write("X") + g:close() + local h = assert(io.open(path, "r")) + local s = h:read("a") + h:close() + return s + end) + pc("A2", function() + local f = assert(io.open(path, "w")) + f:write("abcdef") + f:close() + local g = assert(io.open(path, "r+")) + local a = g:read(2) + g:seek("set", 0) + g:write("Z") + g:seek("set", 0) + local b = g:read("a") + g:close() + return a, b + end) + pc("A3", function() + local big = string.rep("x", 5000) + local f = assert(io.open(path, "w")) + f:write(big) + f:close() + local g = assert(io.open(path, "r")) + local a = g:read(4096) + local b = g:read(4096) + local c = g:read(1) + g:close() + return #a, #b, c + end) + pc("A4", function() + local f = assert(io.open(path, "w")) + local a = f:read("l") + f:close() + return a + end) + pc("A5", function() + local f = assert(io.open(path, "w")) + f:write("1 2 3\\n4\\n") + f:close() + local t = {} + for a, b in io.lines(path, "n", "n") do t[#t + 1] = tostring(a) .. "/" .. tostring(b) end + return #t, table.concat(t, ",") + end) + pc("A6", function() + local f = assert(io.open(path, "w")) + f:write("a\\nb\\n") + f:close() + local t = {} + for l in io.lines(path, 1) do t[#t + 1] = l end + return #t, table.concat(t, "") + end) + pc("A7", function() + local f = assert(io.open(path, "w")) + f:write("a\\n\\nb\\n") + f:close() + local t = {} + for l in io.lines(path, "L") do t[#t + 1] = string.format("%q", l) end + return #t, table.concat(t, ",") + end) + pc("A8", function() + -- 同一文件开两次 + local f = assert(io.open(path, "w")) + f:write("abc") + f:flush() + local g = assert(io.open(path, "r")) + local s = g:read("a") + g:close() + f:write("def") + f:close() + return s + end) + pc("A9", function() + local f = assert(io.open(path, "w")) + f:write("x") + f:close() + local g = assert(io.open(path, "r")) + g:close() + g:close() + return "ok" + end) + + os.remove(path) + os.remove("5") + print("DONE") + """); + } + + /** + * {@code debug} 库 + {@code load} + {@code require} + 错误与栈回溯矩阵, + * 289 行。它咬出过一处真分歧: + *

+ * {@code xpcall} 没校验第二个参数是不是函数。 C 第一件事就是 + * {@code luaL_checktype(L, 2, LUA_TFUNCTION)},而且发生在调用被调函数 + * 之前。我们原先只判「参数够不够两个」,于是 + * {@code xpcall(f, 5)} 不报错,照常调 {@code f};{@code f} 抛错后拿 5 当 + * 消息处理函数,结果是 {@code true, false, "error in error handling"}, + * 而参考实现是 + * {@code false bad argument #2 to 'xpcall' (function expected, got number)}。 + * 参数不足时同理:该报 {@code function expected, got no value}, + * 不是含糊的 {@code value expected}。 + *

+ * 覆盖的面:{@code getinfo} 的 14 个字段 × 0–6 层、{@code getlocal/setlocal} + * (含 vararg 与负下标)、upvalue 全家(含 {@code upvalueid}/ + * {@code upvaluejoin})、{@code traceback}(含协程内)、{@code sethook} + * 的 10 种掩码与计数钩子、{@code load} 的源码/读取函数/模式/env/二进制块、 + * {@code package.path}/{@code cpath}/{@code config}/{@code searchers} 与 + * {@code require} 的循环/抛错/返回 {@code false} 与 {@code nil}、 + * {@code error} 的 0–4 层与各种错误对象、{@code xpcall}/{@code assert}, + * 以及错误在协程里的传播。 + *

+ * 两个坑写在脚本注释里,别再踩: + *

    + *
  • {@code load(function() return 5 end)} 会挂死:数字也算 + * 「字符串」,读取函数永不返回 {@code nil},chunk 无限增长。 + * 参考实现实测挂了 4 分钟才被杀掉。要限次。
  • + *
  • 函数/表的地址不能进输出,一律换成类型名。
  • + *
+ */ + @Test + void debugLoadAndRequireMatrixMatchesReference() { + Diff.assertFullMatches(""" + -- debug 库 / load / require / 错误与栈回溯 矩阵 + -- 函数、表、userdata 一律换成类型名,地址不参与比对。 + local function norm(v) + local t = type(v) + if t == "function" then return "" end + if t == "table" then return "" end + if t == "userdata" then return "" end + if t == "thread" then return "" end + if t == "string" then return string.format("%q", v) end + return tostring(v) + end + local function pc(tag, f, ...) + local r = { pcall(f, ...) } + local out = { (r[1] and "true" or "false") } + for i = 2, #r do out[#out + 1] = norm(r[i]) end + print(tag, table.unpack(out, 1, #out)) + end + + -- 1) debug.getinfo:层级 × what + local FIELDS = { "source", "short_src", "linedefined", "lastlinedefined", + "what", "currentline", "nups", "nparams", "isvararg", + "name", "namewhat", "istailcall", "activelines", "func" } + for _, k in ipairs(FIELDS) do + for lvl = 0, 6 do + local ok, info = pcall(debug.getinfo, lvl, k) + if ok and info then + print("GI", k, lvl, norm(info[k])) + else + print("GI", k, lvl, ok and "nil" or tostring(info)) + end + end + end + -- 全部字段一起取 + for lvl = 0, 4 do + local ok, info = pcall(debug.getinfo, lvl) + if ok and info then + local keys = {} + for k in pairs(info) do keys[#keys + 1] = k end + table.sort(keys) + local parts = {} + for _, k in ipairs(keys) do parts[#parts + 1] = k .. "=" .. norm(info[k]) end + print("GA", lvl, table.concat(parts, ",")) + else + print("GA", lvl, ok and "nil" or tostring(info)) + end + end + -- 传函数 + local function target() end + local ok, info = pcall(debug.getinfo, target) + print("GF", ok, ok and (info.what .. "/" .. info.nparams .. "/" .. tostring(info.isvararg) + .. "/" .. tostring(info.source) .. "/" .. tostring(info.linedefined)) or tostring(info)) + pc("GF2", debug.getinfo, print) + print("GF3", pcall(debug.getinfo, 5)) + print("GF4", pcall(debug.getinfo, "x")) + print("GF5", pcall(debug.getinfo, {}, "S")) + print("GF6", pcall(debug.getinfo, 1, 5)) + print("GF7", pcall(debug.getinfo)) + + -- 2) debug.getlocal / setlocal + local function loc() + local a, b, c = 1, 2, 3 + local t = {} + for i = 1, 5 do + local n, v = debug.getlocal(1, i) + t[#t + 1] = tostring(n) .. "=" .. norm(v) + end + print("GL", table.concat(t, ",")) + debug.setlocal(1, 2, 99) + print("GL2", a, b, c) + print("GL3", debug.getlocal(1, 2)) + print("GL4", pcall(debug.setlocal, 1, 99, 1)) + print("GL5", pcall(debug.getlocal, 99, 1)) + print("GL6", pcall(debug.getlocal, 1, -1)) + print("GL7", pcall(debug.getlocal)) + print("GL8", pcall(debug.getlocal, 1)) + end + loc() + local function vararg(...) + print("GV", debug.getlocal(1, 1), debug.getlocal(1, -1)) + end + vararg(7, 8, 9) + + -- 3) upvalue + local up1, up2 = "u1", "u2" + local function useup() + return up1 .. up2 + end + print("U1", debug.getupvalue(useup, 1)) + print("U2", debug.getupvalue(useup, 2)) + print("U3", debug.getupvalue(useup, 3)) + print("U4", debug.getupvalue(useup, 0)) + print("U5", debug.getupvalue(useup, -1)) + print("U6", pcall(debug.getupvalue, 5, 1)) + print("U7", pcall(debug.getupvalue, print, 1)) + print("U8", debug.setupvalue(useup, 1, "X")) + print("U9", useup()) + print("U10", pcall(debug.setupvalue, useup, 99, "Y")) + print("U11", pcall(debug.upvalueid, useup, 1) ~= nil) + print("U12", pcall(debug.upvalueid, 5, 1)) + print("U13", pcall(debug.upvaluejoin, useup, 1, useup, 2)) + print("U14", useup()) + + -- 4) metatable / registry / uservalue + local mt = { __index = {} } + local t = setmetatable({}, mt) + print("M1", debug.getmetatable(t) == mt) + print("M2", debug.setmetatable(t, nil) == t) + print("M3", debug.getmetatable(t)) + print("M4", pcall(debug.getmetatable, 5)) + print("M5", pcall(debug.setmetatable, 5, {})) + print("M6", type(debug.getregistry()) == "table") + print("M7", pcall(debug.getuservalue, 5)) + print("M8", pcall(debug.setuservalue, 5, {})) + print("M9", pcall(debug.setuservalue, {}, {})) + + -- 5) traceback + local function scrub(s) return (s:gsub(":%d+:", ":N:")) end + print("T1", scrub(debug.traceback())) + print("T2", scrub(debug.traceback("msg"))) + print("T3", scrub(debug.traceback("msg", 2))) + print("T4", scrub(debug.traceback("msg", 0))) + print("T5", pcall(debug.traceback, 5)) + print("T6", scrub(debug.traceback(nil, 1))) + local co = coroutine.create(function() print("TC", scrub(debug.traceback())) end) + coroutine.resume(co) + + -- 6) sethook / gethook + local events = {} + local function hook(what, line) + events[#events + 1] = tostring(what) .. (line and ("@" .. line) or "") + end + local function work() + local x = 0 + for i = 1, 6 do x = x + i end + return x + end + for _, mask in ipairs({ "l", "c", "r", "cr", "cl", "rl", "crl", "", "x", "lcr" }) do + events = {} + debug.sethook(hook, mask) + local ok2 = pcall(work) + debug.sethook() + print("HK", string.format("%q", mask), ok2, #events > 0 and events[1] or "-", + #events > 1 and events[2] or "-") + end + events = {} + debug.sethook(hook, "", 3) + pcall(work) + debug.sethook() + print("HKC", #events > 0 and events[1] or "-") + print("HK2", debug.gethook()) + debug.sethook(hook, "l") + print("HK3", debug.gethook() ~= nil, select("#", debug.gethook())) + debug.sethook() + print("HK4", debug.gethook()) + print("HK5", pcall(debug.sethook, 5, "l")) + print("HK6", pcall(debug.sethook, hook, 5)) + print("HK7", pcall(debug.sethook, hook, "l", "x")) + print("HK8", pcall(debug.sethook, hook, "l", -1)) + print("HK9", pcall(debug.sethook)) + + -- 7) load:源码 / 读取函数 / 模式 / env / 二进制 + local SRC = { "return 1", "return", "x =", "return 1 +", "", " ", "--c", + "return ...", "local x", "return 'a'" } + for i = 1, #SRC do + pc(string.format("LD %q", SRC[i]), load, SRC[i]) + end + pc("LD2", load, "return 1", "=name") + pc("LD3", load, "return 1", "@file") + pc("LD4", load, "return 1", "name") + pc("LD5", load, "return 1", nil, "b") + pc("LD6", load, "return 1", nil, "t") + pc("LD7", load, "return 1", nil, "bt") + pc("LD8", load, "return 1", nil, "z") + pc("LD9", load, "return 1", nil, "x") + pc("LD10", load, "return 1", nil, 5) + local env = { print = print } + local f = load("return X", "=e", "t", env) + print("LD11", type(f)) + env.X = 42 + print("LD12", f and f()) + pc("LD13", load, "return X", "=e", "t", 5) + local bin = string.dump(load("return 7")) + pc("LD14", load, bin) + pc("LD15", load, bin, "=b", "b") + pc("LD16", load, bin, "=b", "t") + pc("LD17", load, "\\27Lua junk") + pc("LD18", load, "\\27") + local pieces = { "return ", "1 +", " 2" } + local idx = 0 + local f2 = load(function() + idx = idx + 1 + return pieces[idx] + end) + print("LD19", type(f2), f2 and f2()) + local n3 = 0 + pc("LD20", load, function() n3 = n3 + 1; error("boom") end) + pc("LD21", load, 5) + -- 读取函数返回数字也算「字符串」,会一直喂下去;必须自己限次,否则 chunk + -- 无限增长,参考实现直接卡死(实测挂了 4 分钟)。 + local n22 = 0 + print("LD22", pcall(load, function() + n22 = n22 + 1 + if n22 > 2 then return nil end + return 5 + end)) + pc("LD23", load, function() return true end) + pc("LD24", load, function() return {} end) + + -- 8) require / package + print("P1", package.path) + print("P2", package.cpath) + print("P3", type(package.loaded), package.loaded.string ~= nil, + package.loaded.math ~= nil, package.loaded["nonexistent-mod"]) + print("P4", type(package.preload), type(package.searchers), #package.searchers) + print("P5", package.config) + print("P6", pcall(require, "nonexistent-mod-xyz")) + print("P7", pcall(require)) + print("P8", pcall(require, 5)) + print("P9", pcall(require, true)) + package.preload["mymod"] = function(name) return { loaded = name } end + local m = require("mymod") + print("P10", type(m), m and m.loaded) + print("P11", package.loaded["mymod"] == m, require("mymod") == m) + package.loaded["mymod"] = nil + print("P12", package.loaded["mymod"]) + package.preload["cyc"] = function() return require("cyc") end + print("P13", pcall(require, "cyc")) + package.preload["bad"] = function() error("mod boom") end + print("P14", pcall(require, "bad")) + print("P15", package.loaded["bad"]) + package.preload["f"] = function() return false end + package.preload["n"] = function() return nil end + print("P16", require("f"), package.loaded["f"]) + print("P17", require("n"), package.loaded["n"]) + + -- 9) error / pcall / xpcall / assert + for lvl = 0, 4 do + print("ER", lvl, select(2, pcall(function() error("e", lvl) end))) + end + pc("ER2", function() error({}) end) + print("ER3", pcall(function() error(nil) end)) + print("ER4", pcall(function() error() end)) + print("ER5", pcall(function() error(setmetatable({}, { __tostring = function() return "TS" end })) end)) + print("ER6", pcall(function() error("x", "y") end)) + print("ER7", pcall(function() error("x", 1.5) end)) + print("XP1", xpcall(function() return 1 end, print, "a")) + print("XP2", xpcall(function() error("E") end, function(mm) return "H:" .. mm end)) + print("XP3", pcall(xpcall, function() error("E") end, 5)) + print("XP4", xpcall(error, function(mm) return "H:" .. tostring(mm) end, "E")) + print("XP5", pcall(xpcall)) + print("AS1", pcall(assert, false)) + print("AS2", pcall(assert, nil, "custom")) + print("AS3", pcall(assert, false, 5)) + print("AS4", pcall(assert)) + print("AS5", assert(1, 2, 3)) + + -- 10) 错误在协程里的传播 + local co2 = coroutine.create(function() error("inside") end) + print("CE1", coroutine.resume(co2)) + print("CE2", coroutine.status(co2)) + local co3 = coroutine.wrap(function() error("wrapped") end) + print("CE3", pcall(co3)) + local co4 = coroutine.create(function() coroutine.yield(1) error("after yield") end) + print("CE4", coroutine.resume(co4)) + print("CE5", coroutine.resume(co4)) + print("CE6", coroutine.status(co4)) + + print("DONE") + """); + } + + /** + * 栈溢出的深度与报错行号。它咬出过一处真分歧,而且根因不在引擎: + *

+ * {@code lua.c: main} 给 {@code pmain} 传的是 {@code (argc, argv)} 两个实参, + * 栈的初始 {@code top} 是 3。 {@code Diff} 只传了一个实参,于是整条栈比 + * 参考实现少占一格。少一格会一路带到底:脚本栈溢出时的递归深度差一层, + * 最深那一帧也就换了个函数,报错行号随之不同。fuzz seed 7984 撞的正是这个 + * —— 参考报第 67 行,我们报第 65 行。 + *

+ * 之所以难查,是因为它不是每次都差:每帧开销是 {@code k+1}({@code k} + * 为局部变量个数),深度是 {@code floor((limit - base) / (k+1))}, + * 基址差 1 只在刚好跨过整除边界时才改变结果。实测 0/1/2/6 个局部变量时差一层, + * 3/4/8/16/32 个时不差 —— 挑一个例子来试的话会以为没问题。 + * 所以这片要按帧大小铺开量,不能只测一种。 + *

+ * 另一组(互相递归)测的是报错行号:{@code r7}/{@code r8} 交替递归, + * 最深那一帧是谁,就决定报哪一行,深度差一层行号就换一行。 + *

+ * 顺带一个探针陷阱:{@code local function f() return f() end} 是尾调用, + * 不占栈,会变成死循环而不是溢出(参考实现实测挂满 5 分钟)。 + * 这里所有递归都写成 {@code return 1 + f()} 这种非尾形式。 + */ + @Test + void stackOverflowDepthAndLineMatchReference() { + Diff.assertMatches(""" + + local function mk(k) + local ls = {} + for i = 1, k do ls[#ls + 1] = "local v" .. i .. " = " .. i end + local src = "local n = 0\\nlocal function f()\\n" .. table.concat(ls, "\\n") + .. "\\nn = n + 1\\nreturn 1 + f()\\nend\\nlocal ok = pcall(f)\\nreturn n" + return load(src) + end + for _, k in ipairs({0,1,2,3,4,6,8,16,32}) do + print("k=" .. k, mk(k)()) + end + """); + } + + /** 同上,另一半:互相递归时报错落在哪一行。 */ + @Test + void stackOverflowLineInMutualRecursionMatchesReference() { + Diff.assertMatches(""" + -- 栈溢出的报错行号:错误发生在 OP_CALL 的 precall 里, + -- C 在 precall 之前刚 savepc,所以行号应当是调用点那一行。 + -- 注意:这里所有递归都刻意写成非尾调用(return 1 + r() 之类), + -- 尾调用不占栈,会变成死循环而不是溢出。 + local function show(tag, f) + local ok, e = pcall(f) + print(tag, ok, tostring(e):gsub("^.-:(%d+):", "L%1:")) + end + + local function r1() return 1 + r1() end + show("A", r1) + + local function r2() + for i = 1, 3 do + local x = r2() + end + end + show("B", r2) + + local function r3() + for _, v in ipairs({ 1, 2 }) do + local x = r3() + end + end + show("C", r3) + + local function r4() + local a = 1 + local b = 2 + if a < b then + local x = r4() + end + end + show("D", r4) + + local r7 + local function r8() return 1 + r7() end + r7 = function() return 1 + r8() end + show("G", r7) + + local function r9() return 1 + r9() end + print("H", pcall(function() return r9() end)) + + local rf + rf = function() return 1 + rf() end + show("I", rf) + + local function r10(n) return 1 + r10(n + 1) end + show("J", function() return r10(1) end) + + -- 尾调用形态(不占栈,用别的方式逼出溢出) + local function rt() return rt() end + local guard = 0 + local function rt2() + guard = guard + 1 + if guard > 100000 then error("give up") end + return rt2() + end + print("K", pcall(rt2), guard) + """); + } + + /** + * {@code math} 全函数 × 30 个边界值(含 ±inf、nan、±0.0、maxinteger、 + * mininteger、2^53、2^63、1/3)+ 二元函数取样 + {@code modf} 的双返回值 + + * {@code log} 的底 + 每个函数的六类参数错误 + 播种后的随机序列;外加 + * {@code os} 里输出确定的那部分({@code getenv}、{@code remove}/ + * {@code rename} 的失败路径、{@code setlocale}、{@code difftime}、 + * {@code time} 的越界字段、{@code date})。1423 行,干净。 + *

+ * 留着的三条理由: + *

    + *
  • 浮点最后一比特:{@code math.sin} 等走的是 {@code libm}, + * 我们的 {@code Math.sin} 与之未必逐比特一致。默认 {@code tostring} + * 只打 14 位有效数字,正好看不见 —— 这片能证明确实看不见, + * 而不是「恰好没测到」。
  • + *
  • {@code math.random} 的序列:xoshiro256** 定死的序列是很硬的 + * 差分点,播种后必须逐个数对上。
  • + *
  • {@code errno}/{@code strerror} 那一层:{@code os.remove} / + * {@code rename} 失败要带上一句 C 的错误文案。
  • + *
+ * 三处不能测的,别加回来:{@code os.clock}(耗时)、 + * 无参 {@code math.randomseed}(拿当前时间播种,两边不可能一致)、 + * 无参 {@code os.time}(当前时间)。 + */ + @Test + void mathAndOsMatrixMatchesReference() { + Diff.assertMatches(""" + -- math 全函数 × 边界值 矩阵;外加 os 里那些输出确定的部分 + -- os.clock(耗时)、os.execute 的退出码文案、os.time() 无参(当前时间)都不可比,不测。 + local VALS = { + -1 / 0, -1e308, -1e300, -2.5, -1.5, -1.0, -0.5, -0.0, 0, 0.0, 0.5, 1.0, + 1.5, 2.0, 2.5, 3.0, 100.0, 1e300, 1e308, 1 / 0, 0 / 0, + math.maxinteger, math.mininteger, -math.maxinteger, + 2 ^ 53, 2 ^ 63, -(2 ^ 63), 0.1, 1 / 3, math.pi, math.huge, + } + local NAMES = {} + for i = 1, #VALS do + NAMES[i] = string.format("%.17g", VALS[i]) + end + + -- 1) 一元函数 + local UNARY = { "abs", "ceil", "floor", "sqrt", "sin", "cos", "tan", "asin", + "acos", "atan", "exp", "tointeger", "type" } + for _, fn in ipairs(UNARY) do + for i = 1, #VALS do + local ok, r = pcall(math[fn], VALS[i]) + if ok then + print("U", fn, NAMES[i], tostring(r), math.type(r)) + else + print("U", fn, NAMES[i], "err", tostring(r)) + end + end + end + + -- 2) 二元函数 + local BINARY = { "fmod", "max", "min", "atan", "ult", "log" } + for _, fn in ipairs(BINARY) do + for i = 1, #VALS do + for j = 1, #VALS do + if (i + j) % 7 == 0 then -- 全矩阵太大,取样 + local ok, r = pcall(math[fn], VALS[i], VALS[j]) + if ok then + print("B", fn, NAMES[i], NAMES[j], tostring(r), math.type(r)) + else + print("B", fn, NAMES[i], NAMES[j], "err", tostring(r)) + end + end + end + end + end + + -- 3) modf 两个返回值 + for i = 1, #VALS do + local ok, a, b = pcall(math.modf, VALS[i]) + if ok then + print("M", NAMES[i], tostring(a), math.type(a), tostring(b), math.type(b)) + else + print("M", NAMES[i], "err", tostring(a)) + end + end + + -- 4) log 的第二个参数 + for _, b in ipairs({ 2, 10, 0.5, 1, 0, -1, 1 / 0 }) do + for _, x in ipairs({ 8, 100, 1, 0, -1, 1 / 0 }) do + print("L", b, x, tostring(select(2, pcall(math.log, x, b)))) + end + end + + -- 5) 参数类型错误 + -- math.atan 与 math.atan2 是同一个函数:报错里报哪个名字, + -- 取决于 lauxlib.c: findfield 遍历 package.loaded 时先撞见谁, + -- 而那个顺序由字符串哈希决定、哈希又被 luai_makeseed(按时间) + -- 播种。参考实现自己连跑八次,六次报 atan2、两次报 atan —— + -- 它是不确定的。所以这里先把名字统一成 atan 再比, + -- 否则这条差分用例会随机翻红。 + local function fixname(s) + return (tostring(s):gsub("'math%.atan2'", "'math.atan'")) + end + for _, fn in ipairs({ "abs", "ceil", "floor", "sqrt", "sin", "exp", "tointeger", + "fmod", "max", "min", "ult", "modf", "log", "atan" }) do + local function pe(...) + local ok, ev = pcall(math[fn], ...) + print("E", fn, ok, ok and tostring(ev) or fixname(ev)) + end + pe() pe(nil) pe("x") pe("1") + local eok, ev = pcall(math[fn], {}) + print("E", fn, eok, type(ev)) + pe(true) + end + + -- 6) 常量与 type + print("C", math.pi, math.huge, math.maxinteger, math.mininteger) + print("C2", math.type(1), math.type(1.0), math.type("1"), math.type(nil), + math.type(math.maxinteger), math.type(1 / 0)) + print("C3", pcall(math.type)) + print("C4", pcall(math.type, 1, 2)) + + -- 7) random:播种后取整串 + math.randomseed(42) + local rs = {} + for i = 1, 10 do rs[#rs + 1] = tostring(math.random()) end + print("R1", table.concat(rs, ",")) + local rs2 = {} + for i = 1, 10 do rs2[#rs2 + 1] = tostring(math.random(100)) end + print("R2", table.concat(rs2, ",")) + local rs3 = {} + for i = 1, 10 do rs3[#rs3 + 1] = tostring(math.random(-5, 5)) end + print("R3", table.concat(rs3, ",")) + math.randomseed(42) + print("R4", math.random(), math.random(100)) + print("R5", pcall(math.random, 5, 1)) + print("R6", pcall(math.random, 1, 2, 3)) + print("R7", pcall(math.random, "x")) + print("R8", pcall(math.random, 0)) + print("R9", pcall(math.random, math.mininteger, math.maxinteger)) + -- 无参 randomseed 拿当前时间播种,两边不可能一致,不测 + print("R11", pcall(math.randomseed, "x")) + print("R12", pcall(math.randomseed, 1.5)) + + -- 8) os:输出确定的那部分 + print("O1", type(os.getenv("PATH")), os.getenv("NO_SUCH_ENV_VAR_XYZ")) + print("O2", pcall(os.getenv)) + print("O3", pcall(os.getenv, 5)) + print("O4", pcall(os.getenv, true)) + print("O5", os.remove("/nonexistent-dir-xyz/nope")) + print("O6", os.rename("/nonexistent-dir-xyz/a", "/nonexistent-dir-xyz/b")) + print("O7", pcall(os.remove)) + print("O8", pcall(os.rename, 5, 6)) + print("O9", tostring(os.setlocale()), tostring(os.setlocale("C")), + tostring(os.setlocale("no-such-locale-xyz"))) + print("O10", pcall(os.setlocale, 5)) + print("O11", os.difftime(100, 50), os.difftime(50, 100), os.difftime(0, 0)) + print("O12", pcall(os.difftime, "x", 1)) + print("O13", pcall(os.difftime, 1)) + print("O14", os.time({ year = 1970, month = 1, day = 1, hour = 0, min = 0, sec = 0 })) + print("O15", os.time({ year = 2038, month = 1, day = 19, hour = 3, min = 14, sec = 8 })) + print("O16", os.time({ year = 2020, month = 2, day = 29, hour = 12 })) + print("O17", os.time({ year = 2021, month = 2, day = 29, hour = 12 })) + print("O18", pcall(os.time, {})) + print("O19", pcall(os.time, "x")) + print("O20", pcall(os.time, 5)) + print("O21", os.time({ year = 2020, month = 13, day = 1 })) + print("O22", os.time({ year = 2020, month = 1, day = 32 })) + print("O23", os.time({ year = 2020, month = 1, day = 1, hour = 25 })) + print("O24", os.time({ year = 2020, month = 1, day = 1, isdst = true })) + print("O25", os.time({ year = 2020, month = 1, day = 1, sec = 60 })) + print("O26", pcall(os.date, "%Y", os.time({ year = 2000, month = 6, day = 15, hour = 12 }))) + print("O27", os.date("!*t", 0).year, os.date("!*t", 0).yday, os.date("!*t", 0).wday) + print("O28", pcall(os.date, "!%Y-%m-%d", 2147483647)) + print("O29", pcall(os.date, "!%Y-%m-%d", -1)) + print("O30", pcall(os.date, 5, 0)) + print("O31", pcall(os.date, "%Y", "x")) + + print("DONE") + """); + } + /** + * {@code string} 库非模式匹配那一半的系统矩阵——模式匹配的 pattern + * 语义另有 {@code patternMatchingMatrixMatchesReference} 那 24819 行单独覆盖, + * 这里只碰 {@code sub / byte / char / rep / upper / lower / len / reverse / + * format / tonumber(base) / tostring}。 + *

+ * 咬出来两条: + *

    + *
  • {@code __name} 走 C 字符串语义:{@code lauxlib.c: luaL_tolstring} + * 取到 {@code __name} 后用 {@code lua_tostring} 转成 {@code const char*}, + * 再用 {@code "%s"} 拼进「名字: 0x地址」——于是名字在第一个 {@code \0} + * 处被砍断。{@code __name = "A\0B"} 只打印出 {@code A},我们把整个 + * {@code "AB"} 都打了出去。
  • + *
  • {@code "%p"} 不补前导零:地址写成了 {@code 0x%016x}, + * 参考实现是 {@code 0x7ab30002c0} 这种不补零的样子。
  • + *
+ * 另有一处不能比的:{@code "+"}/空格标志作用在 NaN 上时,macOS 的 libc + * 不打符号({@code nan})、glibc 打({@code +nan})。CI 的 conformance job + * 在 ubuntu 上跑、本机在 macOS 上跑,两边的参考实现自己就不一样, + * 所以脚本里先把 NaN 的符号抹掉再比。 + */ + @Test + void stringLibraryMatrixMatchesReference() { + Diff.assertMatches(""" + -- string 库非模式匹配部分矩阵:sub/byte/char/rep/upper/lower/len/reverse/ + -- format/tonumber(base)/tostring。模式匹配(find/match/gmatch/gsub 的 pattern + -- 语义)另有 24819 行矩阵单独覆盖,这里只用 plain 参数。 + local S = { + "", "a", "abc", "abcdef", "Hello World", " pad ", + "\\0abc", "a\\0b", "\\0", "\\255\\254", + "0", "-1", "1", " 1 ", "0x10", "1e3", " 0x1p4 ", "10", "z", "Z", "0777", + "inf", "nan", "-inf", "true", + string.rep("x", 70), + } + -- tostring 出来的表/函数/线程带 0x 地址,两边不可能一致,统一抹掉 + local function norm(s) + return (tostring(s):gsub("0x%x+", "0xADDR")) + end + -- libc 分歧:NaN 的符号是 libc 自己的(约定 #96)—— x86 的默认 + -- NaN 符号位是 1,glibc 连不带标志的 %f 都打 "-nan"、%+f 也打 + -- "-nan",而 Apple libc 一律打 "nan"。CI 在 ubuntu 上跑、本机在 + -- macOS 上跑,两边的参考实现自己就不一样,故统一成不带 + -- 符号再比。注意这里是把 nan 的前导符号全部抹掉,不是只抹 "+" + -- —— glibc 的默认分支给的就是 "-nan"。 + local function fnorm(s) + -- 带填充的 nan 要连「填了几个空格」一起让出去:glibc 填的 + -- 是 4 个字符的 "-nan"、Apple libc 填的是 3 个字符的 + -- "nan",同样 width=10,前者补 6 个空格、后者补 7 个 —— + -- 抹平符号之后长度就差一格,不是「宽度算错了」。只比 + -- 「填的是空格、内容是 nan、填在哪一侧」。 + -- 「nan 前后那两段空格」分别压,不要写成 ^(%s*)(nan)(%s*)$ + -- 的整串匹配 —— 那样漏掉后面还跟着东西的形状("%-10.2f|" + -- 的哨兵 "|" 就让第一版整片没归上)。 + local t = s:gsub("(%s+)([-+]?[nN][aA][nN])", "%2") + t = t:gsub("([-+]?[nN][aA][nN])(%s+)", "%1") + return (t:gsub("[-+]nan", "nan"):gsub("[-+]NAN", "NAN")) + end + local function show(s) + -- 带 \\0 和非法 UTF-8 的字符串直接 print 会难读,转成十六进制再打 + return (string.gsub(s, ".", function(c) + return string.format("\\\\%d", string.byte(c)) + end)) + end + for i = 1, #S do + print("S", i, show(S[i])) + end + + -- 1) len / upper / lower / reverse + for i = 1, #S do + print("L", i, #S[i], string.len(S[i])) + print("U", i, show(string.upper(S[i])), show(string.lower(S[i]))) + print("V", i, show(string.reverse(S[i]))) + end + -- len 的非字符串实参 + for _, v in ipairs({ nil, 5, 5.0, true, {}, print }) do + print("LE", type(v), pcall(string.len, v)) + end + + -- 2) sub 的 i/j 全边界交叉 + local IJ = { -100, -8, -3, -1, 0, 1, 2, 3, 8, 100 } + for i = 1, #S do + for _, a in ipairs(IJ) do + for _, b in ipairs(IJ) do + print("B", i, a, b, show(string.sub(S[i], a, b))) + end + end + end + print("B2", pcall(string.sub, "abcdef")) -- 第二个参数是必填的:luaL_checkinteger + print("B3", pcall(string.sub)) + print("B4", pcall(string.sub, nil, 1)) + print("B5", pcall(string.sub, "abc", "x")) + print("B6", pcall(string.sub, "abc", 1, "x")) + print("B7", pcall(string.sub, "abc", 1.5)) + print("B8", show(string.sub("abc", 1.0)), pcall(string.sub, "abc", 1.5)) + print("B9", show(string.sub("abc", 2.0, 3.0))) + print("B10", pcall(string.sub, "abc", math.mininteger)) + print("B11", pcall(string.sub, "abc", 1, math.maxinteger)) + + -- 3) byte / char + for i = 1, #S do + for _, a in ipairs(IJ) do + print("Y", i, a, string.byte(S[i], a)) + print("Y2", i, a, a + 2, string.byte(S[i], a, a + 2)) + end + end + print("C", pcall(string.char)) + print("C2", show(string.char(65, 66, 0, 255))) + print("C3", pcall(string.char, -1)) + print("C4", pcall(string.char, 256)) + print("C5", pcall(string.char, 1.5)) + print("C6", pcall(string.char, 1e300)) + print("C7", pcall(string.char, "x")) + print("C8", #string.char(0, 0, 0)) + + -- 4) rep + for _, n in ipairs({ -2, -1, 0, 1, 3 }) do + for _, sep in ipairs({ nil, "", "-", "ab" }) do + print("R", n, tostring(sep), pcall(string.rep, "ab", n, sep)) + end + end + print("R2", pcall(string.rep, "ab", 1e300)) + print("R3", pcall(string.rep, "ab", "3")) + print("R4", pcall(string.rep)) + print("R5", pcall(string.rep, "ab", 2, 5)) + print("R6", #string.rep("ab", 0, "xyz")) + + -- 5) format:动词 × 值 × 修饰 + local FMT = { + "%d", "%5d", "%-5d|", "%05d", "%+d", "% d", "%.0d", "%i", + "%x", "%X", "%#x", "%o", "%#o", "%c", + "%f", "%.3f", "%10.2f", "%-10.2f|", "%+f", "%e", "%E", "%.3e", + "%g", "%G", "%.0g", "%#g", "%a", "%A", + "%s", "%10s", "%-10s|", "%.2s", "%q", "%%", "%5.2q", + } + local FV = { + 0, 1, -1, 255, math.maxinteger, math.mininteger, + 0.0, -0.0, 1 / 3, 1e300, 1 / 0, -1 / 0, 0 / 0, + "str", "", "a\\0b", true, false, nil, + } + for _, f in ipairs(FMT) do + for i = 1, #FV do + local v = FV[i] + -- NaN 上带精度的 %s 不打:NaN 的拼写是 libc 定的(约定 + -- #96),glibc "-nan"、Apple "nan"。fnorm 抹得平符号和 + -- 填充,抹不平截断 —— "%.2s" 在 "-nan" 上给 "-n"、 + -- 在 "nan" 上给 "na",差的是被截掉的是哪个字符,这条没有 + -- 跨平台的答案。判据两边一致(v == v 只在 NaN 上为假), + -- 跳过的就是同一批。 + if v == v or not f:find("^%%[-+# 0]*%d*%.%d*s$") then + local ok, r = pcall(string.format, f, v) + print("F", f, i, ok, ok and show(fnorm(r)) or tostring(r)) + end + end + end + -- format 的多参数与缺参 + print("F2", pcall(string.format, "%d %s", 1, "a")) + print("F3", pcall(string.format, "%d %d", 1)) + print("F4", pcall(string.format, "%d", "x")) + print("F5", pcall(string.format, "%d", 1.5)) + print("F6", select(1, pcall(string.format, "%s", {})), + norm(select(2, pcall(string.format, "%s", {})))) + print("F7", pcall(string.format)) + print("F8", pcall(string.format, "%z", 1)) + print("F9", pcall(string.format, "%1000000d", 1)) + print("F10", string.format("%s", setmetatable({}, { __tostring = function() return "TT" end }))) + print("F11", pcall(string.format, "%s", setmetatable({}, { __tostring = function() return 5 end }))) + print("F12", pcall(string.format, "%s", setmetatable({}, { __tostring = function() error("boom") end }))) + print("F13", string.format("%q", "a\\nb\\\\\\"c\\0d")) + print("F14", string.format("%q", "\\r\\t\\1\\255")) + print("F15", string.format("%q", 1), string.format("%q", 1.0), string.format("%q", 1 / 3)) + print("F16", pcall(string.format, "%q", nil), pcall(string.format, "%q", true)) + print("F17", string.format("%d%%%s", 1, "x")) + + -- 6) tonumber 带基数 + local TN = { "0", "1", "10", "z", "Z", "-1", "-z", "ff", "FF", " ff ", + "", " ", "0x10", "1e3", "1.5", "- 1", "+1", "1 2", "\\0", "0777" } + for _, s in ipairs(TN) do + for _, b in ipairs({ nil, 1, 2, 8, 10, 16, 36, 37 }) do + print("T", show(s), tostring(b), pcall(tonumber, s, b)) + end + end + print("T2", tonumber("10", 2), tonumber("z", 36), tonumber("Z", 36)) + print("T3", pcall(tonumber, 5, 10)) + print("T4", tonumber(5), tonumber("5"), math.type(tonumber("5"))) + print("T5", tonumber("0x10"), tonumber(" 0x10 ")) + print("T6", tonumber("1e2"), math.type(tonumber("1e2"))) + print("T7", tonumber(1 / 0), tonumber(0 / 0)) + print("T8", pcall(tonumber, "10", 1.5)) + print("T9", pcall(tonumber, "10", "16")) + print("T10", pcall(tonumber, "10", 0)) + print("T11", pcall(tonumber, "10", -1)) + print("T12", pcall(tonumber, "10", 38)) + print("T13", tonumber("9223372036854775807"), math.type(tonumber("9223372036854775807"))) + print("T14", tonumber("9223372036854775808"), math.type(tonumber("9223372036854775808"))) + print("T15", tonumber("-9223372036854775808"), math.type(tonumber("-9223372036854775808"))) + print("T16", tonumber("-9223372036854775809"), math.type(tonumber("-9223372036854775809"))) + + -- 7) tostring / __name / __metatable + print("TS", tostring(nil), tostring(true), tostring(1), tostring(1.0), tostring("s")) + local mt = { __name = "MyType" } + print("TS2", norm(tostring(setmetatable({}, mt)))) + print("TS3", select(1, pcall(tostring, setmetatable({}, { __name = 5 }))), + norm(select(2, pcall(tostring, setmetatable({}, { __name = 5 }))))) + print("TS4", norm(tostring(setmetatable({}, { __name = "A\\0B" })))) + local mtr = { __tostring = function() return "RR" end } + print("TS5", tostring(setmetatable({}, mtr))) + print("TS6", pcall(tostring, setmetatable({}, { __tostring = function() return nil end }))) + print("TS7", pcall(tostring, setmetatable({}, { __tostring = function() return {} end }))) + print("TS8", pcall(tostring, setmetatable({}, { __tostring = 5 }))) + print("TS9", norm(tostring(print)), type(tostring(print))) + print("TS10", norm(tostring(coroutine.create(function() end)))) + print("TS11", norm(tostring({})), norm(tostring(io.stdout))) + + print("DONE") + """); + } + /** + * 元方法语义矩阵 + 报错里的变量来源推断({@code ldebug.c: varinfo} 给 + * 的 {@code local 'x'} / {@code global 'g'} / {@code field 'f'} / + * {@code upvalue 'u'} / {@code method 'm'} 后缀),外加一片参数缺失 + * 矩阵:六十来个库函数各用「不传 / nil / 数字 / 字符串 / 表 / 布尔」调一遍。 + * 共 {@code N} 行。 + *

+ * 参数缺失那一片咬出来的东西最多,根子是同一个: + *

    + *
  • {@code LuaState.arg} 越界读的是残值。C 里 {@code lua_type} + * 对越界下标返回 {@code LUA_TNONE}、取值拿到 nil;我们的栈是复用数组, + * 越界读出来的是上一帧留下的东西 —— 于是 {@code pcall(setmetatable)} + * 有时报 {@code got function}、有时 {@code got nil}, + * {@code assert()} 甚至会因为残值为真而返回 0 个值。随机翻红的 + * 用例最难查,这种就是。
  • + *
  • 缺参要报 {@code no value},不是 {@code nil}:{@code rawlen}、 + * {@code rawequal}、{@code rawget}、{@code rawset}、 + * {@code setmetatable}、五个 {@code coroutine.*} 全都错成 + * {@code nil} 或干脆不报错。
  • + *
  • {@code rawequal} 漏了两遍 {@code luaL_checkany}: + * {@code rawequal(1)} 本该报 + * {@code bad argument #2 ... (value expected)},我们返回 + * {@code true true}。
  • + *
  • {@code pairs} / {@code ipairs} 不要求是表:C 只做 + * {@code luaL_checkany},{@code pairs(5)} 照样返回三个值,真正炸是在 + * 迭代时 {@code next(5)}。我们多加了「必须是表」,把它变成报错。
  • + *
  • {@code select} 的 {@code #} 只比第一个字节:C 是 + * {@code *lua_tostring(L, 1) == '#'},所以 {@code select("#abc", 1)} + * 也是「数个数」;判断整串等于 {@code "#"} 就漏了。另外缺参报的是 + * {@code number expected, got no value},不是 {@code value expected}。
  • + *
  • {@code rawget} 没有「键不能是 nil」那条:{@code rawget({}, nil)} + * 是 {@code true nil}。报「table index is nil」的是 {@code rawset}, + * 那条来自 {@code ltable.c: luaH_newkey}。
  • + *
  • {@code setmetatable} 的第二个参数走 {@code luaL_argexpected}, + * 必须带 {@code got <实际类型>};且缺参是 {@code TNONE},不属于 + * {nil, table},所以 {@code setmetatable({})} 要报错。
  • + *
+ */ + @Test + void metamethodsAndArgPresenceMatrixMatchesReference() { + Diff.assertMatches(""" + -- 元方法语义矩阵 + 报错里的变量来源推断(luaG_typeerror / varinfo) + -- 报错文案来自 ldblib 之外的地方:lvm.c: luaT_trybinTM 抛的 + -- "attempt to perform arithmetic on a X value",以及 luaG_runerror 那句 + -- "attempt to ... "。名字后缀(local 'x' / global 'g' / field 'f' / + -- upvalue 'u' / method 'm' / constant 'k')由 ldebug.c: varinfo 推断。 + local function p(...) + print(...) + end + -- tostring 出来的表/函数/线程带 0x 地址,两边不可能一致,统一抹掉 + local function norm(s) + return (tostring(s):gsub("0x%x+", "0xADDR")) + end + local function pc(f, ...) + local r = { pcall(f, ...) } + local ok = r[1] + local t = {} + for i = 2, #r do t[#t + 1] = tostring(r[i]) end + return ok, table.concat(t, "\\t") + end + + -- 1) 算术元方法:全部二元操作符 × 全部类型组合 + local VALS = { nil, true, false, 1, 2.5, "s", "10", {}, print } + local OPS = { + { "+", function(a, b) return a + b end }, + { "-", function(a, b) return a - b end }, + { "*", function(a, b) return a * b end }, + { "/", function(a, b) return a / b end }, + { "//", function(a, b) return a // b end }, + { "%", function(a, b) return a % b end }, + { "^", function(a, b) return a ^ b end }, + { "&", function(a, b) return a & b end }, + { "|", function(a, b) return a | b end }, + { "~", function(a, b) return a ~ b end }, + { "<<", function(a, b) return a << b end }, + { ">>", function(a, b) return a >> b end }, + { "..", function(a, b) return a .. b end }, + { "==", function(a, b) return a == b end }, + { "<", function(a, b) return a < b end }, + { "<=", function(a, b) return a <= b end }, + { ">", function(a, b) return a > b end }, + { ">=", function(a, b) return a >= b end }, + { "~=", function(a, b) return a ~= b end }, + } + for _, op in ipairs(OPS) do + for i = 1, #VALS do + for j = 1, #VALS do + local ok, r = pc(op[2], VALS[i], VALS[j]) + p("O", op[1], i, j, ok, r) + end + end + end + + -- 2) 一元 + local UN = { + { "-", function(a) return -a end }, + { "#", function(a) return #a end }, + { "~", function(a) return ~a end }, + { "not", function(a) return not a end }, + } + for _, op in ipairs(UN) do + for i = 1, #VALS do + local ok, r = pc(op[2], VALS[i]) + p("N", op[1], i, ok, r) + end + end + + -- 3) 变量来源推断:同一个错误在不同上下文里名字后缀不同 + G = nil + local function probe(label, f) + local ok, r = pc(f) + p("V", label, ok, r) + end + -- global + probe("global-arith", function() return nilG + 1 end) + probe("global-index", function() return nilG.x end) + probe("global-newindex", function() nilG.x = 1 end) + probe("global-call", function() return nilG() end) + probe("global-concat", function() return nilG .. "x" end) + probe("global-len", function() return #nilG end) + probe("global-compare", function() return nilG < 1 end) + -- local + probe("local-arith", function() local loc = nil return loc + 1 end) + probe("local-index", function() local loc = nil return loc.x end) + probe("local-call", function() local loc = nil return loc() end) + probe("local-concat", function() local loc = nil return loc .. "x" end) + probe("local-len", function() local loc = nil return #loc end) + probe("local-name-num", function() local n1, n2 = nil, nil return n2 + 1 end) + -- upvalue + do + local up = nil + probe("upvalue-arith", function() return up + 1 end) + probe("upvalue-index", function() return up.x end) + probe("upvalue-call", function() return up() end) + end + -- field(表里的键) + do + local t = { fld = nil } + probe("field-arith", function() return t.fld + 1 end) + probe("field-index", function() return t.fld.x end) + probe("field-call", function() return t.fld() end) + probe("field-missing-arith", function() return t.nope + 1 end) + probe("field-missing-call", function() return t.nope() end) + probe("table-arith", function() return t + 1 end) + probe("table-call", function() return t() end) + probe("table-len", function() return #t end) + probe("table-index-num", function() return t[1].x end) + end + -- method + do + local t = {} + probe("method-call", function() return t:m() end) + probe("method-on-nil", function() local z = nil return z:m() end) + probe("method-on-str", function() return ("s"):m() end) + probe("method-on-num", function() local nn = 1 return nn:m() end) + probe("method-on-bool", function() local bb = true return bb:m() end) + end + -- constant / 临时值 + probe("const-arith", function() return (nil) + 1 end) + probe("str-arith", function() return "abc" + 1 end) + probe("bool-arith", function() return true + 1 end) + probe("num-index", function() return (1).x end) + probe("num-call", function() return (1)() end) + probe("bool-index", function() return (true).x end) + probe("bool-call", function() return (true)() end) + -- main chunk 里的 self / vararg + probe("vararg-arith", function(...) return ... + 1 end) + probe("vararg-call", function(...) return (...)() end) + + -- 4) 元方法被调用于各种场合 + local function mkmt(fields) + return setmetatable({}, fields) + end + local mtadd = mkmt({ __add = function(a, b) return "ADD" end }) + p("M1", mtadd + 1, 1 + mtadd, mtadd + mtadd) + local mtconcat = mkmt({ __concat = function(a, b) return "CC" end }) + p("M2", mtconcat .. "x", "x" .. mtconcat, mtconcat .. mtconcat) + local mtlen = mkmt({ __len = function() return 42 end }) + p("M3", #mtlen, rawlen(mtlen)) + p("M4", pcall(function() return #mkmt({ __len = function() return "x" end }) end)) + p("M5", pcall(function() return #mkmt({ __len = 5 }) end)) + p("M6", pcall(function() return #mkmt({ __len = function() end }) end)) + local mteq = mkmt({ __eq = function() return true end }) + local mteq2 = mkmt({ __eq = function() return true end }) + p("M7", mteq == mteq2, mteq ~= mteq2, mteq == mteq, rawequal(mteq, mteq2)) + p("M8", mteq == {}, {} == mteq, mteq == 1) + p("M9", pcall(function() return mkmt({ __eq = function() return nil end }) + == mkmt({ __eq = function() return nil end }) end)) + local mtlt = mkmt({ __lt = function() return true end }) + local mtlt2 = mkmt({ __lt = function() return false end }) + p("M10", mtlt < mtlt2, mtlt2 < mtlt, mtlt > mtlt2, mtlt <= mtlt2) + p("M11", pcall(function() return mkmt({ __le = function() return 1 end }) + <= mkmt({ __le = function() return 1 end }) end)) + p("M12", pcall(function() return mkmt({ __lt = function() return 5 end }) + < mkmt({ __lt = function() return 5 end }) end)) + local mtcall = mkmt({ __call = function(self, ...) return "CALL", ... end }) + p("M13", mtcall(), mtcall(1, 2), select(2, mtcall(1))) + p("M14", pcall(setmetatable({}, { __call = 5 }))) + local mtindex = mkmt({ __index = function(t, k) return "I" .. tostring(k) end }) + p("M15", mtindex.x, mtindex[1], rawget(mtindex, "x")) + local prototab = { proto = 1 } + local mtproto = setmetatable({}, { __index = prototab }) + p("M16", mtproto.proto, mtproto.nope, rawget(mtproto, "proto")) + p("M17", pcall(function() return setmetatable({}, { __index = 5 }).x end)) + p("M18", pcall(function() return setmetatable({}, { __index = "s" }).x end)) + local mtnewindex = mkmt({ __newindex = function(t, k, v) rawset(t, "seen", k) end }) + mtnewindex.x = 1 + p("M19", mtnewindex.seen, rawget(mtnewindex, "x")) + p("M20", pcall(function() setmetatable({}, { __newindex = 5 }).x = 1 end)) + local shallow = setmetatable({}, { __newindex = prototab }) + shallow.y = 9 + p("M21", prototab.y, shallow.y) + local mtunm = mkmt({ __unm = function() return "UNM" end }) + p("M22", -mtunm) + local mtidiv = mkmt({ __idiv = function() return "ID" end }) + p("M23", mtidiv // 1) + local mtmod = mkmt({ __mod = function() return "MOD" end }) + p("M24", mtmod % 1) + local mtpow = mkmt({ __pow = function() return "POW" end }) + p("M25", mtpow ^ 1) + local mtband = mkmt({ __band = function() return "BAND" end }) + p("M26", mtband & 1) + local mtbnot = mkmt({ __bnot = function() return "BNOT" end }) + p("M27", ~mtbnot) + local mtshl = mkmt({ __shl = function() return "SHL" end }) + p("M28", mtshl << 1) + -- 元方法链:__index 指向的表自己也有元表 + local lvl3 = { deep = 3 } + local lvl2 = setmetatable({}, { __index = lvl3 }) + local lvl1 = setmetatable({}, { __index = lvl2 }) + p("M29", lvl1.deep, lvl1.nope) + -- __index 循环 + local a, b = {}, {} + setmetatable(a, { __index = b }) + setmetatable(b, { __index = a }) + p("M30", pcall(function() return a.x end)) + -- 元方法自身报错 + local mtboom = mkmt({ __add = function() error("add-boom") end }) + p("M31", pcall(function() return mtboom + 1 end)) + local mtboom2 = mkmt({ __index = function() error("idx-boom") end }) + p("M32", pcall(function() return mtboom2.x end)) + -- 元方法不是函数但可调(带 __call 的表) + local callable = setmetatable({}, { __call = function() return "CF" end }) + p("M33", setmetatable({}, { __add = callable }) + 1) + p("M34", setmetatable({}, { __index = callable }).x) + + -- 5) 元表本身的读写与保护 + p("T1", type(getmetatable("")), getmetatable(1), getmetatable(nil), + getmetatable(true), getmetatable(print)) + p("T2", type(getmetatable("")), getmetatable("").__index == string) + local guard = setmetatable({}, { __metatable = "LOCKED" }) + p("T3", getmetatable(guard), pcall(setmetatable, guard, {})) + p("T4", pcall(setmetatable, guard, nil)) + p("T5", tostring(guard):sub(1, 7)) + p("T6", pcall(setmetatable, 5, {})) + p("T7", pcall(setmetatable, {}, 5)) + p("T8", pcall(setmetatable)) + p("T9", getmetatable(setmetatable({}, {})) == nil) + do + local t = {} + local m = {} + p("T10", setmetatable(t, m) == t, getmetatable(t) == m) + setmetatable(t, nil) + p("T11", getmetatable(t)) + end + p("T12", pcall(getmetatable)) + p("T13", pcall(getmetatable, 5)) + p("T14", pcall(rawget, 5, 1), pcall(rawget, {}, 1), pcall(rawget)) + p("T15", pcall(rawequal, 1, 1), pcall(rawequal, 1), pcall(rawequal)) + p("T16", pcall(rawlen, 5), pcall(rawlen, "abc"), pcall(rawlen)) + p("T17", pcall(rawset, 5, 1, 1), pcall(rawset, {}, nil, 1)) + do + local t = {} + p("T18", pcall(rawset, t, 0 / 0, 1)) + end + -- 数字/字符串共享元表? + p("T19", getmetatable(1) == getmetatable(2), getmetatable("a") == getmetatable("b")) + -- 修改 string 元表会影响所有字符串 + do + local smt = getmetatable("") + local old = smt.__index + p("T20", type(old)) + end + + -- 6) 参数缺失矩阵:每个函数用 0/1 个(故意错类型的)实参各调一次 + -- 「一个参数也没传」在 C 里是 LUA_TNONE,报错文案是 "no value"; + -- 传了 nil 才是 "nil"。两者混掉就看不出缺参校验到底有没有做。 + local FNS = { + { "type", type }, { "tostring", tostring }, { "tonumber", tonumber }, + { "rawget", rawget }, { "rawset", rawset }, { "rawlen", rawlen }, + { "rawequal", rawequal }, { "next", next }, { "pairs", pairs }, + { "ipairs", ipairs }, { "select", select }, { "assert", assert }, + { "pcall", pcall }, { "error", error }, { "unpack", table.unpack }, + { "setmetatable", setmetatable }, { "getmetatable", getmetatable }, + { "collectgarbage", collectgarbage }, + { "coroutine.create", coroutine.create }, { "coroutine.resume", coroutine.resume }, + { "coroutine.status", coroutine.status }, { "coroutine.wrap", coroutine.wrap }, + { "coroutine.isyieldable", coroutine.isyieldable }, + { "coroutine.close", coroutine.close }, { "coroutine.running", coroutine.running }, + { "string.len", string.len }, { "string.sub", string.sub }, + { "string.rep", string.rep }, { "string.format", string.format }, + { "string.byte", string.byte }, { "string.char", string.char }, + { "table.insert", table.insert }, { "table.remove", table.remove }, + { "table.concat", table.concat }, { "table.sort", table.sort }, + { "table.move", table.move }, { "table.unpack", table.unpack }, + { "math.abs", math.abs }, { "math.fmod", math.fmod }, { "math.max", math.max }, + { "math.floor", math.floor }, { "math.tointeger", math.tointeger }, + -- 这四条的第 1、2 组实参(缺参 / nil)会落到「拿当前时间」,两边不可能一致: + -- math.random 的序列取决于此前有没有播种,math.randomseed() 与 os.time()、 + -- os.date() 无参都是读时钟。只比 3~6 组(错类型的实参)。 + -- math.random 整个不测:Lua 5.4 建 state 时就拿时间+地址播种了,播种前的 + -- 序列两边不可能一致。定死的那串序列在 math/os 矩阵里用 randomseed(42) 比。 + { "math.randomseed", math.randomseed, { 1, 2 } }, + { "os.time", os.time, { 1, 2 } }, { "os.date", os.date, { 1, 2 } }, + { "os.difftime", os.difftime }, + { "os.getenv", os.getenv }, { "os.remove", os.remove }, { "os.rename", os.rename }, + { "io.open", io.open }, { "io.lines", io.lines }, + { "debug.getinfo", debug.getinfo }, { "debug.getlocal", debug.getlocal }, + { "debug.sethook", debug.sethook }, + -- debug.traceback 不能进这张表:它的返回值就是回溯,打印出来会落在这段 + -- 输出的中间,而 assertMatches 会从回溯处把后面全砍掉(那道门禁正是为此设的)。 + -- 回溯相关的语义另有 debug/load/require 那片矩阵用 assertFullMatches 覆盖。 + { "utf8.char", utf8.char }, { "utf8.len", utf8.len }, { "utf8.codepoint", utf8.codepoint }, + } + local ARGS = { {}, { nil }, { 5 }, { "x" }, { {} }, { true } } + for _, e in ipairs(FNS) do + local name, fn = e[1], e[2] + local skip = e[3] or {} + for i = 1, #ARGS do + if not skip[i] then + local r = { pcall(fn, table.unpack(ARGS[i], 1, #ARGS[i])) } + local ok = r[1] + local t = {} + for j = 2, #r do t[#t + 1] = tostring(r[j]) end + p("A", name, i, ok, norm(table.concat(t, "\\t"))) + end + end + end + + print("DONE") + """); + } + /** + * 可选参数矩阵:上一片只试了「0 个 / 1 个实参」,这一片把第 2、第 3 个 + * 参数「给不给、给什么」也做全排列 —— 七十来个函数,按 + * {@code {nil, 1, "x"}} 的所有组合从 0 个实参一直喂到各函数的形参上限。 + * 共 {@code N} 行,专抓 {@code luaL_opt*} 的默认值与多道校验的先后。 + *

+ * 咬出两条,都是「校验顺序写反」: + *

    + *
  • {@code string.gsub}:C 里 {@code luaL_optinteger(L, 4, ...)} + * 在校验第 3 个参数之前,所以 {@code gsub(s, p, nil, "x")} 报的是 + * {@code bad argument #4 (number expected, got string)},而不是 + * 「第三个参数不是 string/function/table」。我们反过来了。
  • + *
  • {@code tonumber}:三道校验的顺序是 + * {@code luaL_checkinteger(2)} → {@code luaL_checktype(1, STRING)} → + * {@code luaL_argcheck(2 <= base <= 36)},范围检查在最后。 + * 于是 {@code tonumber(1, 1)} 报 + * {@code bad argument #1 (string expected, got number)}, + * 我们报成了 {@code bad argument #2 (base out of range)}。
  • + *
+ * 三个函数不在这张表里:{@code os.time} / {@code os.date} 第一个参数为 + * {@code nil} 时读的是时钟,{@code math.randomseed(nil)} 拿时钟播种, + * {@code io.lines} 无参会去读 stdin。 + */ + @Test + void optionalArgumentMatrixMatchesReference() { + Diff.assertMatches(""" + -- 可选参数矩阵:上一片只试了「0 个 / 1 个实参」,这一片把第 2、第 3 个参数 + -- 「给不给、给什么」也铺开 —— 每个函数按 {nil, 1, "x"} 的全排列调用到 maxn 个 + -- 实参。专抓「可选参数的边界」:luaL_opt* 的默认值、缺参报错、类型文案。 + local POOL = { nil, 1, "x" } + local NAMES = { "nil", "1", "x" } + -- 表/函数/线程的 tostring 带地址,两边不可能一致,只打类型 + local function s(v) + local t = type(v) + if t == "table" or t == "function" or t == "thread" or t == "userdata" then + return t + end + return tostring(v) + end + -- 不进这张表的:os.time / os.date / math.randomseed —— 它们第一个参数是 nil + -- 时读的是时钟(os.time(nil)、os.date(nil) 取当前时间;randomseed(nil) 拿 + -- 时钟播种);io.lines 无参会去读 stdin,跑在探针里会卡住。 + local FNS = { + { "rawget", rawget, 2 }, { "rawset", rawset, 3 }, { "rawlen", rawlen, 1 }, + { "rawequal", rawequal, 2 }, { "next", next, 2 }, + { "setmetatable", setmetatable, 2 }, { "getmetatable", getmetatable, 1 }, + { "type", type, 1 }, { "tostring", tostring, 1 }, { "tonumber", tonumber, 2 }, + { "select", select, 3 }, { "assert", assert, 2 }, { "pcall", pcall, 2 }, + { "error", error, 2 }, { "ipairs", ipairs, 1 }, { "pairs", pairs, 1 }, + { "table.insert", table.insert, 3 }, { "table.remove", table.remove, 2 }, + { "table.concat", table.concat, 4 }, { "table.move", table.move, 5 }, + { "table.unpack", table.unpack, 3 }, { "table.pack", table.pack, 3 }, + { "table.sort", table.sort, 2 }, + { "string.len", string.len, 1 }, { "string.sub", string.sub, 3 }, + { "string.rep", string.rep, 3 }, { "string.byte", string.byte, 3 }, + { "string.char", string.char, 3 }, { "string.upper", string.upper, 1 }, + { "string.reverse", string.reverse, 1 }, { "string.format", string.format, 3 }, + { "string.find", string.find, 4 }, { "string.match", string.match, 3 }, + { "string.gmatch", string.gmatch, 2 }, { "string.gsub", string.gsub, 4 }, + { "string.dump", string.dump, 2 }, + { "math.max", math.max, 3 }, { "math.min", math.min, 3 }, + { "math.fmod", math.fmod, 2 }, { "math.log", math.log, 2 }, + { "math.floor", math.floor, 1 }, { "math.ceil", math.ceil, 1 }, + { "math.tointeger", math.tointeger, 1 }, { "math.type", math.type, 1 }, + { "math.abs", math.abs, 1 }, { "math.sqrt", math.sqrt, 1 }, + { "math.ult", math.ult, 2 }, { "math.modf", math.modf, 1 }, + { "os.difftime", os.difftime, 2 }, { "os.setlocale", os.setlocale, 2 }, + { "os.getenv", os.getenv, 1 }, { "os.remove", os.remove, 1 }, + { "os.rename", os.rename, 2 }, + { "io.open", io.open, 2 }, + { "coroutine.resume", coroutine.resume, 3 }, { "coroutine.wrap", coroutine.wrap, 1 }, + { "coroutine.close", coroutine.close, 1 }, { "coroutine.status", coroutine.status, 1 }, + { "coroutine.create", coroutine.create, 1 }, + { "debug.getlocal", debug.getlocal, 3 }, { "debug.setmetatable", debug.setmetatable, 2 }, + { "debug.getmetatable", debug.getmetatable, 1 }, { "debug.getuservalue", debug.getuservalue, 2 }, + { "debug.setuservalue", debug.setuservalue, 2 }, + { "utf8.char", utf8.char, 3 }, { "utf8.codepoint", utf8.codepoint, 3 }, + { "utf8.len", utf8.len, 3 }, { "utf8.offset", utf8.offset, 3 }, + } + for _, e in ipairs(FNS) do + local name, fn, maxn = e[1], e[2], e[3] + for len = 0, maxn do + local idx = {} + for i = 1, len do idx[i] = 1 end + while true do + local args, label = {}, {} + for i = 1, len do + args[i] = POOL[idx[i]] + label[i] = NAMES[idx[i]] + end + local r = { pcall(fn, table.unpack(args, 1, len)) } + local out = {} + for j = 2, #r do out[#out + 1] = s(r[j]) end + print("O", name, table.concat(label, ","), tostring(r[1]), table.concat(out, "\\t")) + -- 里程表进位 + local p = len + while p >= 1 do + idx[p] = idx[p] + 1 + if idx[p] <= #POOL then break end + idx[p] = 1 + p = p - 1 + end + if p < 1 then break end + end + end + end + print("DONE") + """); + } + /** + * 协程语义矩阵:{@code yield} 到底能跨哪些边界、{@code close} 的语义、 + * 错误怎么传播、四个状态怎么转、嵌套 {@code resume}。 + *

+ * 这片是干净的,照样固化进来 —— 「yield 能不能跨某某边界」这类规则 + * 极容易在改动调用路径时被悄悄改掉,而现成的 63 个 {@code CoroutineTest} + * 用例并不覆盖「{@code table.sort} 的比较函数里 yield」这种组合。 + * 实测分界(参考实现与这里一致): + *

+ * + * + *
能 yield{@code pcall} / {@code xpcall}、数值与泛型 for + * (含自定义迭代器)、{@code gmatch}、 + * {@code __index} / {@code __newindex} / {@code __add} / + * {@code __lt} / {@code __concat} / {@code __len} / {@code __call} / + * {@code __close}
不能{@code table.sort} 的比较函数、 + * {@code string.gsub} 的替换函数、{@code print} 里的 + * {@code __tostring} —— 报 + * {@code attempt to yield across a C-call boundary}
+ * 另外几处一并盯着:{@code __eq} 里 yield 之后协程是 dead(不是 + * suspended);自己 {@code close} 自己报 + * {@code cannot close a running coroutine};{@code } 变量按 + * 声明的逆序关闭,且 {@code __close} 里抛错会替换掉原错误。 + */ + @Test + void coroutineSemanticsMatrixMatchesReference() { + Diff.assertMatches(""" + -- 协程语义矩阵:yield 能跨哪些边界、close 的语义、错误怎么传播、 + -- 状态机的四个状态、嵌套协程。 + local function pr(...) + print(...) + end + -- wrap 抛出的错误对象可能带回溯,这里只打第一行 + local function first(s) + return (tostring(s):gsub("\\n.*", "")) + end + + -- 1) 基本状态机 + do + local co = coroutine.create(function(a, b) + pr("C1", a, b, coroutine.status(coroutine.running())) + local x, y = coroutine.yield(a + b) + pr("C2", x, y) + return "done", 42 + end) + pr("S1", coroutine.status(co)) + pr("R1", coroutine.resume(co, 1, 2)) + pr("S2", coroutine.status(co)) + pr("R2", coroutine.resume(co, "x", "y")) + pr("S3", coroutine.status(co)) + pr("R3", coroutine.resume(co)) + pr("S4", coroutine.status(co)) + end + + -- 2) 主线程不能 yield;running() 的返回值 + pr("M1", coroutine.isyieldable()) + do + local c, main = coroutine.running() + pr("M2", type(c), main) -- 主线程对象的 tostring 带地址,只打类型 + end + pr("M3", pcall(coroutine.yield)) + pr("M4", pcall(coroutine.yield, 1, 2)) + + -- 3) 死协程 / 运行中的协程 + do + local co = coroutine.create(function() return 1 end) + coroutine.resume(co) + pr("D1", coroutine.status(co), coroutine.resume(co)) + local co2 = coroutine.create(function() return 1 end) + pr("D2", coroutine.resume(co2), coroutine.resume(co2)) + local co3 + co3 = coroutine.create(function() + pr("D3", coroutine.status(co3)) + pr("D4", pcall(coroutine.resume, co3)) + pr("D5", coroutine.close(co3)) + end) + coroutine.resume(co3) + end + + -- 4) wrap:错误传播与死后调用 + do + local w = coroutine.wrap(function() + coroutine.yield(1) + error("boom") + end) + pr("W1", w()) + pr("W2", pcall(w)) + pr("W3", pcall(w)) + local w2 = coroutine.wrap(function() end) + pr("W4", pcall(w2)) + pr("W5", pcall(w2)) + local w3 = coroutine.wrap(function() return 1, 2, 3 end) + pr("W6", w3()) + local w7 = coroutine.wrap(function() error({ code = 1 }) end) + -- 错误对象是表,tostring 带地址、两边不可能一致,只打类型 + pr("W7", select(1, pcall(w7)), type(select(2, pcall(w7)))) + pr("W8", first(select(2, pcall(coroutine.wrap(function() error("plain") end))))) + pr("W9", first(select(2, pcall(coroutine.wrap(function() error(nil) end))))) + pr("W10", first(select(2, pcall(coroutine.wrap(function() error(42) end))))) + pr("W11", first(select(2, pcall(coroutine.wrap(function() + local x = nil + return x.field + end))))) + pr("W12", pcall(coroutine.wrap(function() coroutine.yield(1) end))) + end + + -- 5) yield 能跨哪些边界 + local function canYield(label, f) + local co = coroutine.create(f) + local ok, err = coroutine.resume(co) + local st = coroutine.status(co) + pr("Y", label, ok, st, ok and "ok" or first(err)) + end + canYield("plain", function() coroutine.yield(1) end) + canYield("pcall", function() return pcall(function() return coroutine.yield(1) end) end) + canYield("xpcall", function() + return xpcall(function() return coroutine.yield(1) end, function(e) return "H" end) + end) + canYield("for-numeric", function() + for i = 1, 3 do coroutine.yield(i) end + end) + canYield("for-generic-pairs", function() + for k, v in pairs({ 1, 2 }) do coroutine.yield(k) end + end) + canYield("for-generic-ipairs", function() + for _, v in ipairs({ 1, 2 }) do coroutine.yield(v) end + end) + canYield("for-generic-custom", function() + local function iter(s, c) + if c < 2 then return c + 1, c * 10 end + end + for a, b in iter, nil, 0 do coroutine.yield(a, b) end + end) + canYield("sort-cmp", function() + table.sort({ 3, 1, 2 }, function(a, b) coroutine.yield(0) return a < b end) + end) + canYield("gsub-repl", function() + return string.gsub("abc", "%a", function(c) coroutine.yield(0) return c end) + end) + canYield("gmatch", function() + for w in string.gmatch("a b", "%a") do coroutine.yield(w) end + end) + canYield("tostring-mm", function() + local t = setmetatable({}, { __tostring = function() coroutine.yield(0) return "T" end }) + print(t) + return 1 + end) + canYield("index-mm", function() + local t = setmetatable({}, { __index = function(_, k) coroutine.yield(0) return k end }) + return t.x + end) + canYield("newindex-mm", function() + local t = setmetatable({}, { __newindex = function(_, k, v) coroutine.yield(0) end }) + t.x = 1 + return 1 + end) + canYield("add-mm", function() + local t = setmetatable({}, { __add = function() coroutine.yield(0) return 1 end }) + return t + 1 + end) + canYield("eq-mm", function() + local t = setmetatable({}, { __eq = function() coroutine.yield(0) return true end }) + return t == t + end) + canYield("lt-mm", function() + local t = setmetatable({}, { __lt = function() coroutine.yield(0) return true end }) + return t < t + end) + canYield("concat-mm", function() + local t = setmetatable({}, { __concat = function() coroutine.yield(0) return "C" end }) + return t .. "x" + end) + canYield("len-mm", function() + local t = setmetatable({}, { __len = function() coroutine.yield(0) return 3 end }) + return #t + end) + canYield("call-mm", function() + local t = setmetatable({}, { __call = function() coroutine.yield(0) return "K" end }) + return t() + end) + canYield("close-mm", function() + local t = setmetatable({}, { __close = function() coroutine.yield(0) end }) + local x = t + coroutine.yield(1) + end) + canYield("resume-inside", function() + local inner = coroutine.create(function() coroutine.yield("in") end) + pr(" inner1", coroutine.resume(inner)) + pr(" inner2", coroutine.resume(inner)) + return "out" + end) + canYield("wrap-inside", function() + local inner = coroutine.wrap(function() coroutine.yield("iw") return "iw2" end) + pr(" innerw", inner(), inner()) + return "outw" + end) + + -- 6) to-be-closed 与 close() + do + local log = {} + local co = coroutine.create(function() + local a = setmetatable({}, { + __close = function(_, err) + log[#log + 1] = "close-a:" .. tostring(err) + end, + }) + local b = setmetatable({}, { + __close = function(_, err) + log[#log + 1] = "close-b:" .. tostring(err) + end, + }) + coroutine.yield() + return "fin" + end) + pr("T1", coroutine.resume(co)) + pr("T2", table.concat(log, "|")) + pr("T3", coroutine.resume(co)) + pr("T4", table.concat(log, "|")) + end + do + local log = {} + local co = coroutine.create(function() + local a = setmetatable({}, { + __close = function(_, err) log[#log + 1] = "c:" .. tostring(err) end, + }) + coroutine.yield() + error("inside") + end) + pr("T5", coroutine.resume(co)) + pr("T6", coroutine.resume(co)) + pr("T7", table.concat(log, "|"), coroutine.status(co)) + end + do + local log = {} + local co = coroutine.create(function() + local a = setmetatable({}, { + __close = function(_, err) + log[#log + 1] = "c1" + error("in-close") + end, + }) + local b = setmetatable({}, { + __close = function(_, err) log[#log + 1] = "c2:" .. tostring(err) end, + }) + coroutine.yield() + end) + pr("T8", coroutine.resume(co)) + pr("T9", coroutine.resume(co)) + pr("T10", table.concat(log, "|")) + end + pr("T11", coroutine.close(coroutine.create(function() return 1 end))) + do + local co = coroutine.create(function() return 1 end) + pr("T12", coroutine.resume(co), coroutine.close(co)) + end + do + local co = coroutine.create(function() coroutine.yield() end) + pr("T13", coroutine.resume(co), coroutine.close(co), coroutine.status(co)) + pr("T14", coroutine.close(co)) + end + do + local co + co = coroutine.create(function() pr("T15", coroutine.close(co)) end) + pr("T16", coroutine.resume(co)) + end + pr("T17", pcall(coroutine.close, 5)) + pr("T18", pcall(coroutine.close)) + pr("T19", pcall(coroutine.close, {})) + + -- 7) 错误对象与层级 + do + local co = coroutine.create(function() error("e1") end) + pr("E1", coroutine.resume(co)) + local co2 = coroutine.create(function() error("e2", 0) end) + pr("E2", coroutine.resume(co2)) + local co3 = coroutine.create(function() error("e3", 2) end) + pr("E3", coroutine.resume(co3)) + local co4 = coroutine.create(function() error({}) end) + local e4ok, e4v = coroutine.resume(co4) + pr("E4", e4ok, type(e4v)) -- 表当错误对象,地址不可比 + local co5 = coroutine.create(function() coroutine.yield() error("e5") end) + pr("E5", coroutine.resume(co5), coroutine.resume(co5)) + local co6 = coroutine.create(function() + local ok, e = pcall(function() error("inner") end) + pr("E6", ok, e) + return 1 + end) + pr("E7", coroutine.resume(co6)) + local co7 = coroutine.create(function() + error(setmetatable({}, { __tostring = function() return "OBJ" end })) + end) + pr("E8", coroutine.resume(co7)) + pr("E9", first(select(2, coroutine.resume(co7)))) + end + + -- 8) 嵌套:协程里 resume 另一个协程 + do + local depth = 0 + local function mk(n) + return coroutine.create(function() + depth = depth + 1 + if n > 0 then + local inner = mk(n - 1) + pr("N", n, coroutine.resume(inner)) + end + coroutine.yield("d" .. n) + return n + end) + end + local top = mk(3) + pr("N1", coroutine.resume(top)) + pr("N2", coroutine.resume(top)) + pr("N3", depth) + end + + -- 9) resume 传参与多返回值 + do + local co = coroutine.create(function(...) + pr("V1", select("#", ...), ...) + local a, b, c = coroutine.yield(...) + pr("V2", a, b, c) + return ... + end) + pr("V3", coroutine.resume(co, 1, 2, 3)) + pr("V4", coroutine.resume(co, "p", "q")) + end + + -- 10) 协程里写全局;协程当错误对象 / 表键 / 算术 + do + local co = coroutine.create(function() + GVAR = "set-by-co" + pr("G1", GVAR) + return 1 + end) + pr("G2", coroutine.resume(co), GVAR) + end + do + local co = coroutine.create(function() end) + pr("K1", type(co), tostring(co):sub(1, 7)) + local t = {} + t[co] = 1 + pr("K2", t[co], rawequal(t[co], 1)) + pr("K3", pcall(function() return co + 1 end)) + pr("K4", pcall(function() return #co end)) + pr("K5", pcall(function() return co() end)) + end + + print("DONE") + """); + } + /** + * 数值 {@code for} 的四个分支({@code lvm.c: forprep} / {@code forloop}: + * 整数升、整数降、浮点升、浮点降,各有各的溢出处理)+ 整数/浮点强制转换 + + * {@code //} / {@code %} / 位运算的角落。共 {@code N} 行,干净。 + *

+ * 留着的三条理由: + *

    + *
  • {@code maxinteger} 边界:{@code for i = maxinteger - 2, maxinteger} + * 这类循环,终止条件在整数路径上是要防溢出地比较的,写漏一处 + * 就是 2^64 次迭代。
  • + *
  • 循环变量是整数还是浮点:{@code for i = 0, 1, 0.5} 与 + * {@code for i = 0, 1.0, 0.5} 打出来的 {@code math.type(i)} 不一样, + * 前者首值是整数 {@code 0}、之后变浮点。
  • + *
  • 位运算不做字符串转换:{@code "10" // 2} 给 5,但 + * {@code "10" & 1} 报 + * {@code attempt to perform bitwise operation on a string value}。
  • + *
+ * 三条不能测的:{@code for i = 1, inf}、{@code for i = inf, inf} + * ({@code i + 1} 还是 {@code inf},条件恒真)、 + * {@code for i = maxinteger, mininteger, -1}(2^64 次)。另外 + * {@code for i = 2^53, 2^53 + 2} 与 {@code for i = 1e300, 1e300} 在浮点精度下 + * 也是不推进的死循环,探针里加了 20000 次的兜底,两边都在同一处跳出来。 + */ + @Test + void numericForAndCoercionMatrixMatchesReference() { + Diff.assertMatches(""" + -- 数值 for 的边界 + 整数/浮点强制转换 + // % 位运算的角落 + -- 数值 for 在 lvm.c: forprep/forloop,四个分支(整数升、整数降、浮点升、 + -- 浮点降)各有各的溢出处理,这里是按分支铺的。 + local function pr(...) + print(...) + end + local function pc(f, ...) + local r = { pcall(f, ...) } + local t = {} + for i = 2, #r do t[#t + 1] = tostring(r[i]) end + return r[1], table.concat(t, "\\t") + end + + -- 1) 数值 for:跑起来数一遍,并把每个 i 的类型也打出来 + local function runFor(label, a, b, s) + local n = 0 + local first, last, types = nil, nil, {} + local ok, err = pcall(function() + -- 兜底:万一哪个组合是永不停的循环,跑满 100000 次就跳出来, + -- 免得参考实现挂在那儿(这类探针挂一次要手动杀进程) + local guard = 0 + local function step(i) + n = n + 1 + if n == 1 then first = i end + last = i + types[math.type(i)] = (types[math.type(i)] or 0) + 1 + guard = guard + 1 + if guard >= 20000 then error("__TOOBIG__") end + end + if s == nil then + for i = a, b do step(i) end + else + for i = a, b, s do step(i) end + end + end) + local ts = {} + for k, v in pairs(types) do ts[#ts + 1] = k .. "=" .. v end + table.sort(ts) + pr("F", label, ok, n, tostring(first), tostring(last), table.concat(ts, ","), + ok and "" or tostring(err)) + end + runFor("1..3", 1, 3) + runFor("1..3 nil step", 1, 3, nil) + runFor("3..1", 3, 1) + runFor("1..1", 1, 1) + runFor("1..0", 1, 0) + runFor("0..0", 0, 0) + runFor("1..3 s1", 1, 3, 1) + runFor("1..3 s-1", 1, 3, -1) + runFor("3..1 s-1", 3, 1, -1) + runFor("1..10 s3", 1, 10, 3) + runFor("10..1 s-3", 10, 1, -3) + runFor("1.0..3.0", 1.0, 3.0) + runFor("1.0..3.0 s0.5", 1.0, 3.0, 0.5) + runFor("0..1 s0.5", 0, 1, 0.5) + runFor("0..1.0 s0.5", 0, 1.0, 0.5) + runFor("1..2 s0.1", 1, 2, 0.1) + runFor("1..3 s-0.5", 1, 3, -0.5) + runFor("3..1 s-0.5", 3, 1, -0.5) + runFor("-1..1 s0", -1, 1, 0) + runFor("1..2 snil", 1, 2, nil) + runFor("str..str", "1", "3") + runFor("str..str s", "1", "3", "1") + runFor("1..3 sstr", 1, 3, "1") + runFor("1..3 sstr0.5", 1, 3, "0.5") + runFor("max-2..max", math.maxinteger - 2, math.maxinteger) + runFor("max-2..max s2", math.maxinteger - 2, math.maxinteger, 2) + runFor("min..min+2", math.mininteger, math.mininteger + 2) + runFor("min..min+2 s2", math.mininteger, math.mininteger + 2, 2) + runFor("min..max smax", math.mininteger, math.maxinteger, math.maxinteger) + runFor("max-1..max s2", math.maxinteger - 1, math.maxinteger, 2) + runFor("1..max smax", 1, math.maxinteger, math.maxinteger) + runFor("2^53..2^53+2", 2 ^ 53, 2 ^ 53 + 2) + runFor("2^53..2^53+2 f", (2 ^ 53) + 0.0, (2 ^ 53) + 2.0) + runFor("1e300..1e300", 1e300, 1e300) + runFor("-0.0..0.0", -0.0, 0.0) + runFor("nan..nan", 0 / 0, 0 / 0) + runFor("1..nan", 1, 0 / 0) + -- 下面两条不能测:for i = 1, inf 与 for i = inf, inf 都是永不停的 + -- 循环(i 加 1 还是 inf,条件恒真),参考实现跑起来就不回来了。 + -- 同理 for i = maxinteger, mininteger, -1 是 2^64 次迭代。 + runFor("-inf..inf s-inf", -1 / 0, 1 / 0, 1 / 0) + pr("F2", pc(function() for i = 1, 3, "x" do end end)) + pr("F3", pc(function() for i = {}, 3 do end end)) + pr("F4", pc(function() for i = 1, {} do end end)) + pr("F5", pc(function() for i = 1, 3, {} do end end)) + pr("F6", pc(function() for i = nil, 3 do end end)) + -- 数值 for 只接受 2 或 3 个表达式,写 1 个或 4 个都是语法错误(解析期就挂, + -- 没法在 pcall 里兜),所以这里不测。 + do + local co = {} + for i = 1, 3 do + co[#co + 1] = i + i = i + 100 + end + pr("F8", table.concat(co, ",")) + end + do + local s = 0 + for i = 1, 10 do + if i > 3 then break end + s = s + i + end + pr("F9", s) + end + + -- 2) 整数除与取模的角落 + local N = { + math.maxinteger, math.mininteger, -1, 0, 1, 2, 3, -3, 7, -7, + 2 ^ 53, -(2 ^ 53), + } + for _, a in ipairs(N) do + for _, b in ipairs(N) do + pr("I", a, b, pc(function() return a // b end), + pc(function() return a % b end), pc(function() return a / b end)) + end + end + local FV = { 0.0, -0.0, 1.0, -1.0, 2.5, -2.5, 0.5, 1 / 3, 1 / 0, -1 / 0, 0 / 0, + 1e300, -1e300, 2 ^ 53 } + for _, a in ipairs(FV) do + for _, b in ipairs(FV) do + pr("D", tostring(a), tostring(b), tostring(a // b), tostring(a % b)) + end + end + + -- 3) 位运算:只接受有整数表示的数 + local BV = { 0, 1, -1, 255, 2 ^ 31, 2 ^ 32, 2 ^ 63, -(2 ^ 63), math.maxinteger, + math.mininteger, 1.0, 2.0, 0.5, 1 / 3, 1 / 0, 0 / 0, "1", "0x10" } + for _, a in ipairs(BV) do + pr("B1", tostring(a), pc(function() return ~a end)) + pr("B2", tostring(a), pc(function() return a & 255 end)) + pr("B3", tostring(a), pc(function() return a | 0 end)) + pr("B4", tostring(a), pc(function() return a ~ 0 end)) + pr("B5", tostring(a), pc(function() return a << 1 end)) + pr("B6", tostring(a), pc(function() return a >> 1 end)) + pr("B7", tostring(a), pc(function() return -a end)) + end + pr("B8", 1 << 63, 1 << 64, 1 << 100, -1 << 63, 1 >> 100, -1 >> 100) + pr("B9", 255 >> -1, 255 << -1, math.maxinteger << 1, math.mininteger >> 1) + pr("B10", pc(function() return 1 << (1 / 0) end)) + pr("B11", pc(function() return 1 << (0 / 0) end)) + + -- 4) 字符串与数字的强制转换 + local SV = { "10", " 10 ", "0x10", "1e2", "10.0", ".5", "1.", "inf", "nan", + "-10", "+10", "10x", "", " ", "0X1P4", " 0x10 ", "1e", "0b101" } + for _, s in ipairs(SV) do + pr("S", string.format("%q", s), pc(function() return s + 0 end), + pc(function() return s * 2 end), pc(function() return -s end), + math.type(tonumber(s)), tostring(tonumber(s))) + end + pr("S2", "10" == 10, "10" ~= 10, 10 == 10.0, 0 == -0.0, 1 / 0 == 1 / 0, + (0 / 0) == (0 / 0)) + pr("S3", pc(function() return "10" < 11 end)) + -- 位运算不做字符串转换("10" & 1 报错),而 // 与 % 做 + pr("S4", "10" // 2, "10" % 3, pc(function() return "10" & 1 end)) + pr("S5", 10 .. "", 10.0 .. "", 1 / 3 .. "", (0 / 0) .. "") + pr("S6", tostring(10), tostring(10.0), tostring(-0.0), tostring(1 / 0), + tostring(-1 / 0), tostring(0 / 0)) + pr("S7", tostring(1e15), tostring(1e16), tostring(2 ^ 53), tostring(2 ^ 63), + tostring(math.maxinteger), tostring(math.mininteger)) + pr("S8", 1 == 1.0, math.type(1), math.type(1.0), math.type(2 ^ 63), + math.type(2 ^ 31)) + pr("S9", pc(function() return "abc" + 1 end)) + pr("S10", pc(function() return nil + 1 end)) + pr("S11", pc(function() return true + 1 end)) + pr("S12", pc(function() return 1 < "a" end)) + pr("S13", pc(function() return 1 < nil end)) + pr("S14", pc(function() return {} < {} end)) + pr("S15", 1 < 2, "a" < "b", "10" < "9", 1 < 1.5, math.maxinteger < 1 / 0, + math.mininteger > -1 / 0) + pr("S16", math.maxinteger + 1, math.mininteger - 1, math.maxinteger * 2, + -math.mininteger) + pr("S17", math.maxinteger + 1 == math.mininteger, math.tointeger(2 ^ 63), + math.tointeger(2 ^ 63 - 1)) + pr("S18", 2 ^ 63, math.type(2 ^ 63), (2 ^ 63) + 0.0 == 2 ^ 63) + + print("DONE") + """); + } + /** + * 语法层与作用域矩阵,两半: + *
    + *
  • 编译期错误:用 {@code load()} 把六十来个片段编一遍,报什么错 + * 就打什么错。{@code goto} 跳进作用域、重复标签、给 {@code } + * 赋值、{@code break} 在循环外、畸形数字与转义、函数名的各种形态 —— + * 这些只有手写才碰得到,fuzzer 生成的程序撞不上。
  • + *
  • 运行期语义:多重赋值「右边先全部求值」、表构造器的求值顺序与 + * 键覆盖、变长参数(含 {@code nil} 混在里面)、方法调用与 {@code :} 的 + * 各种畸形用法、局部变量遮蔽与 upvalue 共享、{@code repeat} 的条件能 + * 看见循环体里的 {@code local}、运算符优先级与结合性、字面量。
  • + *
+ * 干净({@code N} 行)。留着是因为这片全是「改动词法/语法/作用域时会悄悄 + * 坏掉」的地方,而现成用例里只有零散几处。 + */ + @Test + void syntaxAndScopeMatrixMatchesReference() { + Diff.assertMatches(""" + -- 语法层与作用域矩阵。两半: + -- A) 编译期错误:用 load() 把片段编一遍,报什么错就打什么错 —— goto 跳进 + -- 作用域、重复标签、给 赋值、break 在循环外这些只有手写才碰得到, + -- fuzzer 生成的程序基本不会撞上。 + -- B) 运行期语义:多重赋值的求值顺序、表构造器的求值顺序、变长参数、方法调用、 + -- 局部变量遮蔽与 upvalue 共享、运算符优先级与结合性、字面量。 + local function pr(...) + print(...) + end + local function pc(f, ...) + local r = { pcall(f, ...) } + local t = {} + for i = 2, #r do t[#t + 1] = tostring(r[i]) end + return r[1], table.concat(t, "\\t") + end + + -- ============ A) 编译期错误 ============ + local CHUNKS = { + { "goto-ok-fwd", [[ do goto x ::x:: end ]] }, + { "goto-ok-back", [[ local i = 0 ::x:: i = i + 1 if i < 3 then goto x end ]] }, + { "goto-into-scope", [[ do goto x end local y = 1 ::x:: ]] }, + { "goto-into-scope2", [[ for i = 1, 3 do goto x end ::x:: ]] }, + { "goto-undefined", [[ goto nope ]] }, + { "goto-label-undefined-2", [[ ::a:: goto b ::b:: ]] }, + { "dup-label", [[ ::a:: ::a:: ]] }, + { "dup-label-block", [[ do ::a:: end do ::a:: end ]] }, + { "label-at-end", [[ local function f() ::done:: return 1 end ]] }, + { "goto-out-of-function", [[ local function f() goto out end ::out:: ]] }, + { "break-outside", [[ break ]] }, + { "break-in-func", [[ local function f() break end ]] }, + { "break-across-func", [[ for i = 1, 3 do local function f() break end end ]] }, + { "const-assign", [[ local x = 1 x = 2 ]] }, + { "const-ok", [[ local x = 1 local y = x + 1 ]] }, + { "const-nil", [[ local x ]] }, + { "close-nil", [[ local x = nil ]] }, + { "close-ok", [[ local x = setmetatable({}, {__close=function() end}) ]] }, + { "attrib-unknown", [[ local x = 1 ]] }, + { "attrib-twice", [[ local x = 1 ]] }, + { "syntax-do", [[ do ]] }, + { "syntax-end", [[ end ]] }, + { "syntax-if", [[ if 1 then ]] }, + { "syntax-1", [[ 1 + ]] }, + { "num-1e", [[ return 1e ]] }, + { "num-dot1", [[ return ..1 ]] }, + { "num-0b", [[ return 0b101 ]] }, + { "num-0x-p", [[ return 0x1p4 ]] }, + { "num-0x-", [[ return 0x.8 ]] }, + { "num-3dot", [[ return 3. ]] }, + { "str-badesc", [[ return "\\q" ]] }, + { "str-badhex", [[ return "\\xZZ" ]] }, + { "str-baduni", [[ return "\\u{110000}" ]] }, + { "str-uni-ok", [[ return "\\u{41}" ]] }, + { "str-z", [[ return "a\\z + b" ]] }, + -- 这三条外层必须用 [==[ :片段里自己带 ]],用 [[ ]] 会被提前闭合 + { "str-unfinished", [==[ return "abc ]==] }, + { "longstr-unfinished", [==[ return [[abc ]==] }, + -- 这条片段自己带 ]==],外层得再高两级 + { "longstr-level", [====[ return [==[a]]b]==] ]====] }, + { "self-outside", [[ function a:b() end ]] }, + { "func-name-expr", [[ function (a).b() end ]] }, + { "vararg-in-middle", [[ local function f(a, ..., b) end ]] }, + { "vararg-outside", [[ return ... ]] }, + { "assign-to-call", [[ f() = 1 ]] }, + { "assign-noname", [[ 1 = 2 ]] }, + { "multiple-local", [[ local a, = 1 ]] }, + { "return-mid", [[ return 1 + ]] }, + { "concat-num", [[ return 1 .. 2 ]] }, + { "eq-assign", [[ local a == 1 ]] }, + { "malformed-number", [[ return 1__2 ]] }, + { "newline-in-str", [[ return "a + b" ]] }, + } + for _, e in ipairs(CHUNKS) do + local f, err = load(e[2], "=CHUNK") + if f then + local ok, r = pcall(f) + pr("A", e[1], "ok", ok, tostring(r)) + else + pr("A", e[1], "err", tostring(err)) + end + end + + -- ============ B) 运行期语义 ============ + -- 多重赋值:右边先全部求值 + do + local log = {} + local function f(name, v) + log[#log + 1] = name + return v + end + local a, b = f("1", 10), f("2", 20) + pr("B1", a, b, table.concat(log, ",")) + log = {} + local x, y = 1, 2 + x, y = y, x + pr("B2", x, y) + log = {} + local t = {} + t[1], t[2] = f("a", 1), f("b", 2) + pr("B3", t[1], t[2], table.concat(log, ",")) + end + -- 个数不匹配 + do + local a, b, c = 1 + pr("B4", a, b, c) + local d, e = 1, 2, 3 + pr("B5", d, e) + local function g() + return 1, 2, 3 + end + local p, q, r, s = g() + pr("B6", p, q, r, s) + pr("B7", (g())) + pr("B8", select("#", g()), select(2, g())) + local arr = { g() } + pr("B9", #arr) + local arr2 = { (g()) } + pr("B10", #arr2) + end + -- 表构造器的求值顺序与键覆盖 + do + local log = {} + local function k(n) + log[#log + 1] = n + return n + end + local t = { k(1), k(2), k(3) } + pr("C1", table.concat(log, ","), #t, t[1], t[2], t[3]) + log = {} + local t2 = { [k("a")] = 1, b = k(2), k(3) } + pr("C2", table.concat(log, ","), #t2, t2.a, t2.b, t2[1]) + log = {} + local t3 = { [1] = k("x"), [1] = k("y") } + pr("C3", table.concat(log, ","), t3[1], #t3) + local t4 = { 1, 2, 3, n = 4, [5] = 5 } + pr("C4", #t4, t4.n, t4[5]) + local t5 = { "a", "b", "c" } + pr("C5", #t5, table.concat(t5, "")) + local t6 = { nil, nil, 3 } + pr("C6", #t6, t6[3]) + end + -- 变长参数 + do + local function f(...) + return select("#", ...), ... + end + pr("D1", f()) + pr("D2", f(1, 2, 3)) + pr("D3", f(nil, nil)) + pr("D4", f(1, nil, 3)) + local function g(a, ...) + return a, select("#", ...) + end + pr("D5", g(1), g(1, 2), g(1, 2, 3)) + local function h(...) + local p = table.pack(...) + return p.n, select("#", ...) + end + pr("D6", h(), h(1, nil, 3)) + local function i(...) + local t = { ... } + return #t + end + pr("D7", i(1, 2, 3), i(1, nil, 3), i(nil)) + local function j(...) + return ... + end + pr("D8", j(1, 2, 3)) + pr("D9", select("#")) + end + -- 方法与冒号 + do + local obj = { v = 7 } + function obj:get() + return self.v + end + function obj:set(x) + self.v = x + return self + end + pr("E1", obj:get(), obj:set(9).v) + pr("E2", obj["get"](obj)) + pr("E3", pc(function() return obj.get() end)) + local s = "abc" + pr("E4", s:upper(), s:sub(2), s:len(), ("x"):rep(3)) + pr("E5", ("%d-%s"):format(1, "a")) + pr("E6", pc(function() return s:nope() end)) + pr("E7", pc(function() return (1):nope() end)) + pr("E8", pc(function() return (nil):nope() end)) + pr("E9", pc(function() return (true):nope() end)) + end + -- 作用域与遮蔽 + do + local x = "outer" + do + local x = "inner" + pr("F1", x) + end + pr("F2", x) + local y = "o" + local y = y .. "i" + pr("F3", y) + do + local z = 1 + for i = 1, 2 do + local z = i * 10 + pr("F4", i, z) + end + pr("F5", z) + end + end + -- upvalue 共享 + do + local n = 0 + local function inc() + n = n + 1 + return n + end + local function get() + return n + end + inc() + inc() + pr("G1", get(), n) + local fns = {} + for i = 1, 3 do + fns[i] = function() return i end + end + pr("G2", fns[1](), fns[2](), fns[3]()) + local function mk() + local v = 0 + return function() v = v + 1 return v end, function() return v end + end + local a1, a2 = mk() + local b1, b2 = mk() + a1() + a1() + b1() + pr("G3", a2(), b2()) + end + -- repeat 的条件能看见循环体里的 local + do + local n = 0 + repeat + local done = n >= 2 + n = n + 1 + until done + pr("H1", n) + local m = 0 + repeat + m = m + 1 + local stop = m > 5 + until stop + pr("H2", m) + end + -- 运算符优先级与结合性 + pr("I1", 2 ^ 3 ^ 2, -2 ^ 2, 2 ^ -2, (-2) ^ 2) + pr("I2", 1 .. 2 .. 3, "a" .. 1 .. "b") + pr("I3", 1 + 2 * 3 - 4 / 2, (1 + 2) * 3) + pr("I4", 7 // 2 * 2, 7 % 3 + 1) + pr("I5", 1 < 2 and 3 or 4, 1 > 2 and 3 or 4) + pr("I6", not nil, not not 1, not (1 == 2)) + pr("I7", -"10", #"abc" + 1, -# "ab") + pr("I8", 1 | 2 & 3 ~ 4, (1 | 2) & 3) + pr("I9", 1 << 2 >> 1, -1 >> 1, 1 << 1 + 1) + pr("I10", 2 ^ 2 ^ 3 == 2 ^ (2 ^ 3)) + pr("I11", "a" .. "b" == "ab") + pr("I12", pc(function() return 1 < 2 < 3 end)) + pr("I13", 10 // 3 // 2, 10 // (3 // 2)) + pr("I14", -2 ^ -2, (-2) ^ (-2)) + -- 字面量 + pr("J1", 0x10, 0X10, 0x1p4, 0X1P4, 0x.8p1, 0xA, 0xff) + pr("J2", 1e2, 1E2, 1e+2, 1e-2, .5, 5., 5.0, 0.5e1) + pr("J3", math.type(0x10), math.type(1e2), math.type(1), math.type(1.0), + math.type(3), math.type(3.0)) + pr("J4", 1e15, 1e16, 0x10 == 16, 1e2 == 100) + pr("J5", "\\65", "\\x41", "A", "\\u{41}", "\\u{1F600}" == "\\240\\159\\152\\128") + pr("J6", #"\\u{41}", #"\\u{1F600}", "\\z" .. "a") + pr("J7", [[abc]], [==[a]]b]==], [[\\n]] == "\\\\n") + pr("J8", [[ + line2]], #[[ + ]]) + pr("J9", 0x7fffffff, 0x80000000, 0xffffffff, 0xfffffffffffff) + -- 空语句(5.2 起 `;` 单独成句合法) + do + local k1 = 1; + ; + pr("K1", k1) + end + -- 比较链里的 __lt + do + local mt = { __lt = function(a, b) return true end } + local a, b = setmetatable({}, mt), setmetatable({}, mt) + pr("K2", a < b, a > b, a <= b, a >= b) + end + print("DONE") + """); + } + /** + * {@code debug} 库与 {@code utf8} 库矩阵。干净({@code N} 行)。 + *
    + *
  • {@code getinfo}:{@code what} 的每个字母单独与组合 + * ({@code n S l u f L t}),外加越界 level、传函数、传非字符串。
  • + *
  • {@code getlocal} / {@code setlocal} / {@code getupvalue} / + * {@code setupvalue}:遍历全部槽与越界。
  • + *
  • {@code sethook}:{@code c} / {@code r} / {@code l} / count + * 四个掩码各跑一遍并打事件序列。
  • + *
  • {@code traceback}:不整段比对(里头有 chunk 名与地址), + * 只比行数与「有没有 {@code stack traceback:} / {@code in main chunk}」。
  • + *
  • {@code utf8}:{@code char / codepoint / len / offset / codes / + * charpattern} 的越界与非法序列。
  • + *
+ * 两处坑,写这片时踩过:{@code debug.setlocal(1, 1, ...)} 在主 chunk 里改掉的 + * 是第 1 个局部变量,也就是脚本自己的 {@code print} 包装,改完后面全炸; + * 以及 {@code getinfo} / {@code getmetatable} 返回的表打印出来带地址, + * 必须只比类型。 + */ + @Test + void debugAndUtf8LibraryMatrixMatchesReference() { + Diff.assertMatches(""" + -- debug 库与 utf8 库矩阵。 + -- debug 那半:getinfo 的每个 what 字母、getlocal/setlocal、getupvalue/ + -- setupvalue、sethook 的四个掩码、getregistry、uservalue、traceback 的形状。 + -- 回溯不整段比对(里头有 chunk 名与地址),只比行数与关键片段。 + -- 只留到第一个换行 + local function first(v) + return (tostring(v):gsub("\\n.*", "")) + end + local function pr(...) + print(...) + end + -- 函数/表的 tostring 带地址,只打类型 + local function s(v) + local tt = type(v) + if tt == "table" or tt == "function" or tt == "thread" or tt == "userdata" then + return tt + end + return tostring(v) + end + local function pc(f, ...) + local r = { pcall(f, ...) } + local t = {} + for i = 2, #r do t[#t + 1] = s(r[i]) end + return r[1], table.concat(t, "\\t") + end + + -- 1) getinfo:level 与 what + local function outer() + return debug.getinfo(1) + end + local function outer2() + return debug.getinfo(2) + end + pr("I0", s(debug.getinfo(0)), s(debug.getinfo(1))) + do + local i = outer() + pr("I1", i.source, i.short_src, i.currentline, i.linedefined, i.lastlinedefined, + i.what, i.namewhat, tostring(i.name), i.nups, i.nparams, i.ntransfers, + i.isvararg, s(i.func), type(i.activelines)) + end + do + local i = outer2() + pr("I2", i.currentline > 0, i.what) + end + -- what 的每个字母,单独与组合 + local WHATS = { "", "n", "S", "l", "u", "f", "L", "t", "nSlfLut", "Sn", "fl", "Sln" } + for _, w in ipairs(WHATS) do + local ok, i = pcall(debug.getinfo, 1, w) + if not ok then + pr("W", string.format("%q", w), "err", tostring(i)) + else + local keys = {} + for k in pairs(i) do keys[#keys + 1] = k end + table.sort(keys) + local vals = {} + for _, k in ipairs(keys) do vals[#vals + 1] = k .. "=" .. s(i[k]) end + pr("W", string.format("%q", w), table.concat(vals, ",")) + end + end + pr("W2", pc(debug.getinfo, 100)) + pr("W3", pc(debug.getinfo, -1)) + pr("W4", pc(debug.getinfo)) + pr("W5", pc(debug.getinfo, print, "S")) + pr("W6", pc(debug.getinfo, print)) + pr("W7", pc(debug.getinfo, 1, 5)) + pr("W8", pc(debug.getinfo, "x")) + + -- 2) getlocal / setlocal + do + local a1, a2, a3 = 1, 2, 3 + local n = 1 + while true do + local nm, v = debug.getlocal(1, n) + if not nm then break end + pr("L", n, nm, s(v)) + n = n + 1 + end + -- setlocal 要在自己的那一层做:在主 chunk 里 setlocal(1, 1, ...) + -- 改掉的是第 1 个局部变量,也就是 pr 自己 —— 后面就再也调不动 print 了 + local function inner() + local p, q = 1, 2 + local nm = debug.setlocal(1, 1, 99) + return nm, p, q + end + pr("L2", inner()) + pr("L2b", pc(debug.setlocal, 1, 0, 1)) + pr("L3", pc(debug.getlocal, 1, 0)) + pr("L4", pc(debug.getlocal, 100, 1)) + pr("L5", pc(debug.getlocal)) + pr("L6", pc(debug.getlocal, 1)) + end + + -- 3) upvalue + do + local up1, up2 = "u1", "u2" + local function f() + return up1, up2 + end + local n = 1 + while true do + local nm, v = debug.getupvalue(f, n) + if not nm then break end + pr("U", n, nm, s(v)) + n = n + 1 + end + pr("U2", debug.getupvalue(f, 1)) + pr("U3", debug.setupvalue(f, 1, "NEW"), f()) + pr("U4", debug.setupvalue(f, 99, "x")) + pr("U5", pc(debug.getupvalue, print, 1)) + pr("U6", pc(debug.getupvalue, 5, 1)) + pr("U7", pc(debug.getupvalue)) + pr("U8", pc(debug.setupvalue, f, 1)) + end + + -- 4) sethook:只打印事件序列,不打印地址 + do + local log = {} + local function hook(ev, ln) + log[#log + 1] = ev .. ":" .. tostring(ln) + end + local function target(a) + local x = a + 1 + return x + end + debug.sethook(hook, "c") + target(1) + debug.sethook() + pr("H1", table.concat(log, "|")) + log = {} + debug.sethook(hook, "r") + target(1) + debug.sethook() + pr("H2", table.concat(log, "|")) + log = {} + debug.sethook(hook, "l") + target(1) + debug.sethook() + pr("H3", table.concat(log, "|")) + log = {} + debug.sethook(hook, "", 3) + for i = 1, 10 do local _ = i end + debug.sethook() + pr("H4", table.concat(log, "|")) + log = {} + debug.sethook(hook, "crl") + target(2) + debug.sethook() + pr("H5", (table.concat(log, "|"):gsub(":%d+", ":N"))) + pr("H6", debug.gethook() == nil, pc(debug.gethook)) + pr("H7", pc(debug.sethook, 5)) + pr("H8", pc(debug.sethook, hook, 5)) + pr("H9", pc(debug.sethook, hook, "z")) + pr("H10", debug.sethook(hook, "cr", 2)) + debug.sethook() + pr("H11", debug.gethook() == nil) + end + + -- 5) traceback 的形状(不整段比对) + do + local tb = debug.traceback("MSG") + local n = select(2, tb:gsub("\\n", "")) + pr("T1", n, tb:sub(1, 4), (tb:find("stack traceback:", 1, true) ~= nil), + (tb:find("in main chunk", 1, true) ~= nil)) + local tb2 = debug.traceback("M2", 2) + local n2 = select(2, tb2:gsub("\\n", "")) + pr("T2", n2, (tb2:find("in main chunk", 1, true) ~= nil)) + pr("T3", (debug.traceback():find("stack traceback:", 1, true) ~= nil)) + -- traceback(5) 的返回值就是回溯,整段打印会让 assertMatches 那道 + -- 「回溯后还有输出」的门禁炸掉,这里只留第一行 + local t4ok, t4v = pcall(debug.traceback, 5) + pr("T4", t4ok, first(t4v)) + pr("T5", s(debug.traceback(nil, 0)) == "string") + local co = coroutine.create(function() + pr("T6", (debug.traceback("INCO"):find("INCO", 1, true) ~= nil)) + end) + coroutine.resume(co) + -- traceback(协程) 的返回值开头就是 "stack traceback:",只打类型 + pr("T7", type(debug.traceback(co))) + end + + -- 6) registry / metatable / uservalue + pr("R1", s(debug.getregistry()), type(debug.getregistry())) + pr("R2", debug.getregistry()[1] == _G or debug.getregistry()[2] == _G) + pr("R3", pc(debug.getregistry, 1)) + pr("R4", pc(debug.setmetatable, 5, {})) + pr("R5", pc(debug.setmetatable, {}, 5)) + do + local t = {} + pr("R6", debug.setmetatable(t, { __index = { x = 1 } }) == t, t.x) + pr("R7", s(debug.getmetatable(t)), debug.setmetatable(t, nil) == t, + s(debug.getmetatable(t))) + end + pr("R8", pc(debug.getmetatable)) + pr("R9", pc(debug.getmetatable, 5)) + pr("R10", pc(debug.getuservalue, 5)) + pr("R11", pc(debug.getuservalue, nil)) + pr("R12", pc(debug.setuservalue, 5, {})) + pr("R13", pc(debug.setuservalue, nil, {})) + + -- 7) utf8 + pr("F1", utf8.charpattern) + pr("F2", utf8.char(65, 66, 0x4E2D, 0x1F600) == "AB\\228\\184\\173\\240\\159\\152\\128") + pr("F3", #utf8.char(0x1F600), #utf8.char(0x4E2D), #utf8.char(65)) + pr("F4", pc(utf8.char, -1)) + pr("F5", pc(utf8.char, 0x7FFFFFFF)) + pr("F6", pc(utf8.char, 0x110000)) + pr("F7", pc(utf8.char, 1.5)) + pr("F8", utf8.char() == "") + pr("F9", utf8.codepoint("AB", 1), utf8.codepoint("AB", 2)) + pr("F10", pc(utf8.codepoint, "AB", 3)) + pr("F11", pc(utf8.codepoint, "AB", 0)) + pr("F12", pc(utf8.codepoint, "AB", -1)) + pr("F13", utf8.codepoint("\\228\\184\\173"), utf8.codepoint("\\240\\159\\152\\128")) + pr("F14", utf8.codepoint("AB", 1, 2)) + pr("F15", pc(utf8.codepoint, "AB", 1, 3)) + pr("F16", pc(utf8.codepoint, "AB", 2, 1)) + pr("F17", utf8.len("AB"), utf8.len("\\228\\184\\173"), utf8.len("\\240\\159\\152\\128")) + pr("F18", pc(utf8.len, "\\255")) + pr("F19", utf8.len("AB", 2), utf8.len("AB", 3)) + pr("F20", pc(utf8.len, "AB", 4)) + pr("F21", utf8.len("", 1) == 0, pc(utf8.len, "", 2)) + pr("F22", pc(utf8.len, 5)) + pr("F23", utf8.offset("ABC", 2), utf8.offset("ABC", 4)) + pr("F24", utf8.offset("\\228\\184\\173", 2)) + pr("F25", pc(utf8.offset, "ABC", 0)) + pr("F26", pc(utf8.offset, "ABC", 5)) + pr("F27", utf8.offset("hi\\228\\184\\173", -1)) + pr("F28", pc(utf8.offset, "ABC", -5)) + pr("F29", pc(utf8.offset, "ABC", 1.5)) + -- 遍历 UTF-8 串 + do + local out = {} + for p, c in utf8.codes("A\\228\\184\\173B") do + out[#out + 1] = p .. ":" .. c + end + pr("F30", table.concat(out, ",")) + pr("F31", pc(function() + for p, c in utf8.codes("\\255") do end + end)) + local out2 = {} + for p, c in utf8.codes("AB", 2) do + out2[#out2 + 1] = p .. ":" .. c + end + pr("F32", table.concat(out2, ",")) + end + -- 用 charpattern 走一遍 + do + local cnt = 0 + for c in ("A\\228\\184\\173\\240\\159\\152\\128"):gmatch(utf8.charpattern) do + cnt = cnt + 1 + end + pr("F33", cnt) + end + + print("DONE") + """); + } + + /** + * {@code package} / {@code require} / {@code load} 矩阵 —— 参考实现与 + * 本实现逐字节比对。 + *

+ * 铺它的起因是三处只有对着参考实现才看得见的分歧: + *

    + *
  • {@code package.loadlib} 的第三个返回值。判据是失败发生在哪一步: + * {@code ERRLIB}(库打不开)给 {@code "open"},{@code ERRFUNC} + * (库里没这个符号)给 {@code "absent"}。之前照着「没有 dlopen + * 的兜底分支」写死成 {@code "absent"},而那个分支下 + * {@code lookforfunc} 恒返回 {@code ERRLIB},照样该给 + * {@code "open"};
  • + *
  • {@code require} 循环依赖的报错不是裸的 + * {@code C stack overflow},而是被 {@code checkload} 包成 + * {@code error loading module '_pk_b' from file './_pk_b.lua':\n\t + * C stack overflow}。原因见 {@code Parser#enterLevel}:解析器的 + * {@code enterlevel} 与 {@code luaD_call} 共用 {@code nCcalls}, + * 于是第 200 层是在某次 {@code luaL_loadfile} 的解析中途炸的, + * 错误被 {@code luaD_protectedparser} 兜住,才轮到 + * {@code checkload} 去包;
  • + *
  • {@code package.loadlib} 的失败消息由平台 libc 决定(macOS 是 + * {@code dlopen(...)},glibc 是 {@code cannot open shared object + * file...}),本实现根本不加载动态库——三边文本都不一样,只能比 + * 「是不是字符串」。
  • + *
+ */ + @Test + void packageRequireAndLoadMatrixMatchesReference() { + Diff.assertMatches(""" + -- package / require / load 矩阵。 + -- 注意文件系统副作用:两边跑在同一个 cwd,所以用到的临时文件一律 + -- 「开头先删、用完再删」,保证两次运行都从干净状态开始。 + local function pr(...) + print(...) + end + local function pc(f, ...) + local r = { pcall(f, ...) } + local t = {} + for i = 2, #r do t[#t + 1] = tostring(r[i]) end + return r[1], table.concat(t, "\\t") + end + local function s(v) + local tt = type(v) + if tt == "table" or tt == "function" or tt == "thread" or tt == "userdata" then + return tt + end + return tostring(v) + end + -- 动态库失败消息由平台 libc 决定(macOS 是 dlopen(...),glibc 是 + -- "cannot open shared object file..."),本实现根本不加载动态库, + -- 三边的文本都不一样。这里只比「是不是字符串」,内容归一成一个占位。 + local function dl(v) + if type(v) ~= "string" then return v end + if v:find("^dlopen%(") or v:find("cannot open shared object") + or v:find("dynamic libraries not enabled") + or v:find("no such file") or v:find("No such file or directory") then + return "" + end + return v + end + -- 临时模块一律写到 target/rt 下:两边的工作目录都是仓库根,直接写根目录 + -- 会在用例失败时留下一堆 _pk_*.lua。 + local D = "target/rt/" + local function rm(p) + return os.remove(D .. p) + end + local function mkfile(path, content) + local f, err = io.open(D .. path, "w") + if not f then return false, err end + f:write(content) + f:close() + return true + end + + -- 1) package 的静态结构 + pr("A1", string.format("%q", package.config)) + pr("A2", type(package.path), type(package.cpath)) + pr("A3", (package.path:find("?", 1, true) ~= nil), (package.cpath:find("?", 1, true) ~= nil)) + pr("A4", (package.path:find(";", 1, true) ~= nil)) + do + local names = {} + for k in pairs(package.loaded) do + names[#names + 1] = tostring(k) + end + table.sort(names) + pr("A5", table.concat(names, ",")) + end + pr("A6", type(package.preload), type(package.searchers), #package.searchers) + for i = 1, #package.searchers do + pr("A7", i, type(package.searchers[i])) + end + pr("A8", type(package.loadlib), pcall(package.loadlib)) + pr("A9", select(3, package.loadlib("no_such_lib_xyz", "luaopen_x"))) + pr("A10", s(dl(select(2, package.loadlib("no_such_lib_xyz", "luaopen_x"))))) + pr("A11", (function() + local r = { pcall(package.loadlib, 5, "x") } + local t = { tostring(r[1]) } + for i = 2, #r do t[#t + 1] = s(dl(r[i])) end + return table.concat(t, "\\t") + end)()) + pr("A12", select(1, pcall(package.loadlib, "x"))) + + -- 2) searchpath + pr("B1", package.searchpath("nope_xyz", "./?.lua")) + pr("B2", package.searchpath("nope_xyz", "./?.lua;./?/init.lua")) + pr("B3", package.searchpath("nope_xyz", "")) + pr("B4", pc(package.searchpath, "a", "./?.lua", 5)) + pr("B5", pc(package.searchpath, 5, "./?.lua")) + pr("B6", pc(package.searchpath)) + pr("B7", package.searchpath("", "./?.lua")) + do + local ok = mkfile("_pk_sp.lua", "return 1\\n") + pr("B8", ok, package.searchpath("_pk_sp", "./target/rt/?.lua")) + pr("B9", package.searchpath("_pk_sp", "./target/rt/?.lua;./target/rt/?.xyz")) + rm("_pk_sp.lua") + pr("B10", package.searchpath("_pk_sp", "./target/rt/?.lua")) + end + do + -- sep / rep:把名字里的点换成目录分隔符 + local ok = mkfile("_pk_sp2.lua", "return 2\\n") + pr("B11", ok, package.searchpath("a.b", "./target/rt/?.lua", ".", "_")) + pr("B12", package.searchpath("_pk.sp2", "./target/rt/?.lua", ".", "_")) + rm("_pk_sp2.lua") + end + + -- 3) require:从磁盘加载 + 返回值的各种形态 + local OLDPATH = package.path + package.path = "./target/rt/?.lua" + local function withModule(name, body, fn) + rm(name .. ".lua") + mkfile(name .. ".lua", body) + package.loaded[name] = nil + local r = { pcall(fn) } + rm(name .. ".lua") + package.loaded[name] = nil + return table.unpack(r) + end + pr("C1", withModule("_pk_m1", "return {v = 42}\\n", function() + local m = require("_pk_m1") + return type(m), m.v, package.loaded["_pk_m1"] == m + end)) + pr("C2", withModule("_pk_m2", "return nil\\n", function() + local m = require("_pk_m2") + return type(m), tostring(m), tostring(package.loaded["_pk_m2"]) + end)) + pr("C3", withModule("_pk_m3", "return false\\n", function() + local m = require("_pk_m3") + return type(m), tostring(m), tostring(package.loaded["_pk_m3"]) + end)) + pr("C4", withModule("_pk_m4", "return true\\n", function() + local m = require("_pk_m4") + return type(m), tostring(m), tostring(package.loaded["_pk_m4"]) + end)) + pr("C5", withModule("_pk_m5", "local x = 1\\n", function() + local m = require("_pk_m5") + return type(m), tostring(m), tostring(package.loaded["_pk_m5"]) + end)) + pr("C6", withModule("_pk_m6", "return 7\\n", function() + local m = require("_pk_m6") + return type(m), tostring(m), tostring(package.loaded["_pk_m6"]) + end)) + -- 同一模块取两次:第二次走缓存,文件改了也不重新加载 + rm("_pk_m7.lua") + mkfile("_pk_m7.lua", "return {v = 1}\\n") + package.loaded["_pk_m7"] = nil + do + local a = require("_pk_m7") + mkfile("_pk_m7.lua", "return {v = 2}\\n") + local b = require("_pk_m7") + pr("C7", a.v, b.v, a == b) + package.loaded["_pk_m7"] = nil + local c = require("_pk_m7") + pr("C8", c.v, c == a) + end + rm("_pk_m7.lua") + package.loaded["_pk_m7"] = nil + -- 模块里抛错:loaded 不被设置 + pr("C9", withModule("_pk_m8", "error('mod-boom')\\n", function() + return pc(require, "_pk_m8"), tostring(package.loaded["_pk_m8"]) + end)) + -- 循环依赖 + rm("_pk_a.lua") + rm("_pk_b.lua") + mkfile("_pk_a.lua", "return require('_pk_b')\\n") + mkfile("_pk_b.lua", "return require('_pk_a')\\n") + package.loaded["_pk_a"] = nil + package.loaded["_pk_b"] = nil + pr("C10", pc(require, "_pk_a")) + rm("_pk_a.lua") + rm("_pk_b.lua") + package.loaded["_pk_a"] = nil + package.loaded["_pk_b"] = nil + -- 找不到的模块 + pr("C11", pc(require, "no_such_module_xyz")) + pr("C12", pc(require)) + pr("C13", pc(require, 5)) + pr("C14", pc(require, true)) + pr("C15", pc(require, "")) + pr("C16", pc(require, {})) + -- preload 优先于磁盘 + rm("_pk_p.lua") + mkfile("_pk_p.lua", "return 'from-disk'\\n") + package.loaded["_pk_p"] = nil + package.preload["_pk_p"] = function(n) + return "from-preload" + end + pr("C17", require("_pk_p"), package.loaded["_pk_p"]) + package.preload["_pk_p"] = nil + package.loaded["_pk_p"] = nil + pr("C18", require("_pk_p")) + rm("_pk_p.lua") + package.loaded["_pk_p"] = nil + -- preload 返回 nil / 不返回 + package.preload["_pk_p2"] = function(n) end + pr("C19", require("_pk_p2"), tostring(package.loaded["_pk_p2"])) + package.preload["_pk_p2"] = nil + package.loaded["_pk_p2"] = nil + -- 模块名带斜杠 + rm("_pk_d.lua") + mkfile("_pk_d.lua", "return 'd'\\n") + package.loaded["_pk_d"] = nil + pr("C20", pc(require, "_pk_d/x")) + pr("C21", require("_pk_d")) + rm("_pk_d.lua") + package.loaded["_pk_d"] = nil + package.path = OLDPATH + + -- 4) load:mode / env / chunkname / reader + pr("D1", s(load("return 1"))) + pr("D2", select(1, load("return 1")) ~= nil) + pr("D3", pc(function() return load("return 1")() end)) + pr("D4", pc(load, "return 1 +")) + pr("D5", select(2, load("return 1 +"))) + pr("D6", select(2, load("@@@"))) + -- mode + pr("E1", s(load("return 1", "=c", "t"))) + pr("E2", s(load("return 1", "=c", "b"))) + pr("E3", s(load("return 1", "=c", "bt"))) + pr("E4", pc(load, "return 1", "=c", "z")) + pr("E5", pc(load, "return 1", "=c", "")) + pr("E6", pc(load, "return 1", "=c", 5)) + do + local ok, f = pcall(load, "return 1", "=c", "t", 5) + pr("E7", ok, s(f)) + end + -- 二进制块:mode "b" 接受 dump 出来的,"t" 不接受 + do + local f = load("return 1") + local bin = string.dump(f) + pr("E8", s(load(bin, "=b", "b")), s(load(bin, "=b", "t"))) + pr("E9", s(load(bin, "=b", "bt"))) + pr("E10", pc(load, bin, "=b", "z")) + end + -- 文本块带 # 注释头(bin 模式允许开头有注释行) + pr("E11", s(load("#comment\\nreturn 1", "=c", "b"))) + -- env + do + local env = { X = 5, print = print } + local f = load("return X", "=c", "t", env) + pr("F1", f ~= nil, f ~= nil and f()) + local g = load("X = 9 return X", "=c", "t", env) + pr("F2", g(), env.X) + local h = load("return _ENV", "=c", "t", env) + pr("F3", h() == env) + local i2 = load("return print", "=c", "t", env) + pr("F4", i2() == print) + pr("F5", s(load("return 1", "=c", "t", nil))) + pr("F6", s(load("return 1", "=c", "t", 1.0))) + end + -- chunkname 影响报错位置 + pr("G1", pc(function() return load("error('x')", "@myfile.lua")() end)) + pr("G2", pc(function() return load("error('x')", "=myname")() end)) + pr("G3", pc(function() return load("error('x')")() end)) + pr("G4", pc(function() return load("error('x')", "plain")() end)) + pr("G5", pc(function() return load("error('x')", 5)() end)) + pr("G6", pc(function() return load("return 1", 5)() end)) + -- reader 函数 + do + local parts = { "return ", "1 + ", "2" } + local i = 0 + local f = load(function() + i = i + 1 + return parts[i] + end) + pr("H1", s(f), f ~= nil and f()) + end + do + -- 一次给整段,然后给 nil 结束 + local n = 0 + local f = load(function() + n = n + 1 + if n == 1 then return "return 3" end + return nil + end) + pr("H2", s(f), f ~= nil and f(), n) + end + do + -- 返回空串也表示结束 + local n = 0 + local f = load(function() + n = n + 1 + if n == 1 then return "return 4" end + return "" + end) + pr("H3", s(f), f ~= nil and f()) + end + do + -- 注意:返回数字会挂死(数字也算「字符串」,读取函数永不结束), + -- 所以这里只让它返回两次就停 + local n = 0 + local f = load(function() + n = n + 1 + if n <= 2 then return 5 end + return nil + end) + pr("H4", s(f), tostring(f)) + end + pr("H5", pc(load, 5)) + pr("H6", pc(load, nil)) + pr("H7", pc(load, true)) + -- dofile / loadstring + pr("I1", pc(dofile, "no_such_file_xyz.lua")) + pr("I2", pc(dofile)) + pr("I3", pc(dofile, 5)) + rm("_pk_df.lua") + mkfile("_pk_df.lua", "return 11\\n") + pr("I4", pc(dofile, "./target/rt/_pk_df.lua")) + rm("_pk_df.lua") + pr("I5", loadstring == nil) + pr("I6", loadstring == nil and "gone" or "present") + -- load 出来的函数的环境与 upvalue + do + local f = load("local a = 1 return function() return a end") + local g = f() + pr("J1", g()) + pr("J2", type(load("return ..."))) + local h = load("return ...") + pr("J3", h(1, 2, 3)) + end + print("DONE") + """); + } + + /** + * {@code io} 库矩阵 —— 参考实现与本实现逐字节比对。 + *

+ * {@code io} 原本是十个标准库里铺得最薄的一片(167 行),这一片把它补到 + * 124 行输出:{@code open} 的十六种合法模式与九种非法模式、{@code read} + * 的全部格式({@code l} / {@code L} / {@code a} / {@code n} / 数字 / 多格式 + * / 缺省)、{@code write} 的整数与浮点写出形态、{@code seek} 的三个起点与 + * 越界、{@code lines}、{@code setvbuf}、{@code close} 关两次、 + * {@code io.output} / {@code io.input} 的切换,以及二进制安全 + * (含 {@code \0} 与非法 UTF-8)。 + *

+ * 咬出一处:{@code seek} 的负目标位置是 {@code EINVAL}(22),不是随便 + * 一个 IO 错。C 的 {@code fseek} 在 {@code SET} / {@code CUR} / {@code END} + * 三种起点上都是这个判据(实测三种都给 {@code nil, "Invalid argument", 22}), + * 而 Java 的 {@code RandomAccessFile.seek} 抛的是 "Negative seek offset", + * 掉进 {@code errnoOf} 的兜底分支就成了 {@code EIO}(5)。往 EOF 之后 + * 定位是允许的,别一起拦。 + *

+ * 另有两处不属于分歧、只能绕开:{@code fopen} 一个目录在 macOS 与 + * glibc 上都成功但读的行为不同,而 JVM 压根不允许把目录当流打开; + * {@code print} 走的是 C 层 {@code stdout},不受 {@code io.output} 切换影响 + * —— 这两条写进比对只会两边都假红。 + */ + @Test + void ioLibraryDeepMatrixMatchesReference() { + Diff.assertMatches(""" + -- io 库矩阵。 + -- 临时文件一律放 target/rt/(两边 cwd 都是仓库根,写根目录会留垃圾)。 + local D = "target/rt/" + local function P(n) + return D .. n + end + local function pr(...) + print(...) + end + local function pc(f, ...) + local r = { pcall(f, ...) } + local t = {} + for i = 2, #r do t[#t + 1] = tostring(r[i]) end + return r[1], table.concat(t, "\\t") + end + -- 文件句柄带地址,只打类型 + local function s(v) + local tt = type(v) + if tt == "table" or tt == "function" or tt == "thread" or tt == "userdata" then + return tt + end + return tostring(v) + end + local function rm(p) + return os.remove(P(p)) + end + local function mk(name, content) + local f = io.open(P(name), "wb") + if not f then return false end + f:write(content) + f:close() + return true + end + local function slurp(name) + local f = io.open(P(name), "rb") + if not f then return nil end + local c = f:read("a") + f:close() + return c + end + + -- 1) 静态结构 + pr("A1", s(io.stdout), s(io.stderr), s(io.stdin)) + pr("A2", io.type(io.stdout), io.type(io.stderr), io.type(io.stdin)) + pr("A3", io.type(1), io.type("x"), io.type({}), io.type(nil)) + pr("A4", s(io.open), s(io.lines), s(io.read), s(io.write), s(io.close)) + pr("A5", pc(io.type)) + + -- 2) open:模式的各种形态 + pr("B1", pc(io.open, P("_io_nope"), "r")) + pr("B2", s(select(1, io.open(P("_io_w"), "w")))) + rm("_io_w") + local modes = { "r", "w", "a", "r+", "w+", "a+", "rb", "wb", "ab", "r+b", "w+b", + "a+b", "br", "bw", "rt", "wt" } + -- 先把文件建出来:不然第 1 个 "r" 的成败取决于上一次运行有没有留下残骸, + -- 两边跑的先后一变结论就反了。 + mk("_io_mode", "x") + for i, m in ipairs(modes) do + -- 坏模式是抛错不是返 nil+消息(lauxlib 的 l_checkmode 判据是 + -- '[rwa]%+?b?'),所以必须走 pcall,否则脚本在这里就断了。 + local ok, f, e = pcall(io.open, P("_io_mode"), m) + pr("B3", i, m, ok, s(f), e) + if ok and f then f:close() end + end + rm("_io_mode") + -- 坏模式 + local bad = { "rw", "x", "rr", "r++", "+", "b", "", "r+x", "w+b+" } + for i, m in ipairs(bad) do + pr("B4", i, string.format("%q", m), select(2, pc(io.open, P("_io_bad"), m))) + end + rm("_io_bad") + -- 缺省模式是 "r" + do + local f, e = io.open(P("_io_def")) + pr("B5", select(2, pc(io.open, P("_io_def")))) + if f then f:close() end + end + rm("_io_def") + -- 参数类型(数字会被就地转成字符串:真的去打开叫 "5" 的文件。 + -- 先删一次:H8/H9 的 io.output(5) 会把它建出来,留在盘上会让下一轮 B6 的 + -- 成败反过来 —— 这种「上一次运行的残骸决定这一次结果」的坑已经踩过一回。) + os.remove("5") + pr("B6", pc(io.open, 5)) + pr("B7", pc(io.open, nil)) + pr("B8", pc(io.open, "x", 5)) + pr("B9", pc(io.open)) + -- 「打开目录」不测:fopen 一个目录在 macOS 与 glibc 上都成功,但读的行为不同, + -- 而 JVM 根本不允许把目录当流打开。属于平台能力差异,放进比对只会两边都假红。 + + -- 3) 读:格式串 + mk("_io_r", "line1\\nline2\\n\\nline4\\n") + do + local f = io.open(P("_io_r"), "r") + pr("C1", f:read("l"), f:read("l"), f:read("l"), f:read("l"), f:read("l")) + f:close() + end + do + local f = io.open(P("_io_r"), "r") + pr("C2", string.format("%q", f:read("L")), string.format("%q", f:read("L"))) + f:close() + end + do + local f = io.open(P("_io_r"), "r") + pr("C3", string.format("%q", f:read("a"))) + f:close() + end + do + -- 数字:各种形态 + local f = io.open(P("_io_num"), "w") + f:write("12 3.5 -7 0x10 1e3 .5 5. .inf -.inf nan 12abc\\n") + f:close() + local g = io.open(P("_io_num"), "r") + local out = {} + for i = 1, 12 do out[i] = tostring(g:read("n")) end + pr("C4", table.concat(out, ",")) + pr("C5", g:read("n"), g:read("n")) + g:close() + rm("_io_num") + end + do + -- 一次读多个格式 + local f = io.open(P("_io_r"), "r") + pr("C6", f:read("l", "l", "l")) + f:close() + end + do + local f = io.open(P("_io_r"), "r") + pr("C7", string.format("%q", f:read(3)), string.format("%q", f:read(0)), + string.format("%q", f:read(100))) + f:close() + end + do + local f = io.open(P("_io_r"), "r") + pr("C8", pc(f.read, f, "x")) + pr("C9", pc(f.read, f, 5, "x")) + f:close() + end + -- 已关闭 / 只写打开 + do + local f = io.open(P("_io_r"), "r") + f:close() + pr("C10", pc(f.read, f, "l")) + pr("C11", pc(f.close, f)) + end + do + local f = io.open(P("_io_r2"), "w") + pr("C12", pc(f.read, f, "l")) + f:close() + rm("_io_r2") + end + -- 缺省格式是 "l" + do + local f = io.open(P("_io_r"), "r") + pr("C13", string.format("%q", f:read())) + f:close() + end + -- 空文件 + do + mk("_io_empty", "") + local f = io.open(P("_io_empty"), "r") + pr("C14", f:read("l"), f:read("a"), f:read("L"), f:read("n"), f:read(10)) + f:close() + rm("_io_empty") + end + -- 没有最后的换行 + do + mk("_io_nonl", "abc") + local f = io.open(P("_io_nonl"), "r") + pr("C15", string.format("%q", f:read("l")), string.format("%q", f:read("L"))) + f:close() + rm("_io_nonl") + end + + -- 4) 写 + do + local f = io.open(P("_io_w2"), "w") + pr("D1", s(f:write("a")), s(f:write("b", "c")), s(f:write(1)), s(f:write(2.5))) + pr("D2", s(f:write())) + pr("D3", pc(f.write, f, {})) + pr("D4", pc(f.write, f, nil)) + pr("D5", pc(f.write, f, true)) + f:close() + pr("D6", string.format("%q", slurp("_io_w2"))) + rm("_io_w2") + end + do + -- 追加 + local f = io.open(P("_io_ap"), "w") + f:write("x") + f:close() + local g = io.open(P("_io_ap"), "a") + g:write("y") + g:close() + pr("D7", string.format("%q", slurp("_io_ap"))) + rm("_io_ap") + end + do + mk("_io_rd", "x") + local f = io.open(P("_io_rd"), "r") + pr("D8", pc(f.write, f, "z")) + f:close() + rm("_io_rd") + end + -- 整数的写出形态(区分整数与浮点) + do + local f = io.open(P("_io_num2"), "w") + f:write(1, 1.0, -0.0, 1e15, 2^53, math.maxinteger, 0.1) + f:close() + pr("D9", string.format("%q", slurp("_io_num2"))) + rm("_io_num2") + end + + -- 5) seek + do + local f = io.open(P("_io_sk"), "w") + f:write("0123456789") + f:close() + local g = io.open(P("_io_sk"), "r") + pr("E1", g:seek("set", 3), string.format("%q", g:read(2))) + pr("E2", g:seek("cur"), g:seek("cur", -2), string.format("%q", g:read(2))) + pr("E3", g:seek("end"), g:seek("end", -3), string.format("%q", g:read(3))) + pr("E4", g:seek("set"), string.format("%q", g:read(1))) + pr("E5", g:seek()) + pr("E6", pc(g.seek, g, "nope")) + pr("E7", pc(g.seek, g, "set", "x")) + pr("E8", pc(g.seek, g, 5)) + pr("E9", g:seek("set", 1000), g:read(1), g:read("a")) + pr("E10", pc(g.seek, g, "set", -1)) + g:close() + pr("E11", pc(g.seek, g, "set", 0)) + rm("_io_sk") + end + + -- 6) lines + do + mk("_io_ln", "a\\nb\\nc") + local out = {} + for l in io.lines(P("_io_ln")) do out[#out + 1] = l end + pr("F1", table.concat(out, ",")) + local out2 = {} + for l in io.lines(P("_io_ln"), "L") do out2[#out2 + 1] = string.format("%q", l) end + pr("F2", table.concat(out2, ",")) + local out3 = {} + for a, b in io.lines(P("_io_ln"), "l", "l") do out3[#out3 + 1] = tostring(a) .. tostring(b) end + pr("F3", table.concat(out3, ",")) + pr("F4", pc(function() + for l in io.lines(P("_io_nope2")) do end + end)) + -- 文件句柄的 lines + local f = io.open(P("_io_ln"), "r") + local out4 = {} + for l in f:lines() do out4[#out4 + 1] = l end + pr("F5", table.concat(out4, ",")) + f:close() + pr("F6", pc(function() + for l in f:lines() do end + end)) + rm("_io_ln") + end + + -- 7) flush / setvbuf / close + do + local f = io.open(P("_io_fl"), "w") + pr("G1", f:flush()) + pr("G2", pc(f.setvbuf, f, "no")) + pr("G3", pc(f.setvbuf, f, "full", 1024)) + pr("G4", pc(f.setvbuf, f, "line")) + pr("G5", pc(f.setvbuf, f, "nope")) + pr("G6", pc(f.setvbuf, f, "full", "x")) + pr("G7", pc(f.setvbuf, f)) + f:write("z") + f:close() + pr("G8", string.format("%q", slurp("_io_fl"))) + pr("G9", pc(f.close, f)) -- 关第二次是抛错,不是返回 nil+消息 + pr("G10", io.type(f)) + rm("_io_fl") + end + do + local f = io.open(P("_io_cl"), "w") + pr("G11", f:close(), io.type(f)) + pr("G12", pc(f.flush, f)) + pr("G13", pc(f.write, f, "x")) + rm("_io_cl") + end + pr("G14", pc(io.close, 5)) + pr("G15", pc(io.close)) + + -- 8) io.output / io.input + do + local old = io.output() + pr("H1", io.type(old)) + local f = io.open(P("_io_out"), "w") + pr("H2", io.type(io.output(f))) + pr("H3", s(io.write("123")), s(io.write(4))) + io.output(old) + pr("H4", string.format("%q", slurp("_io_out"))) + rm("_io_out") + end + do + local f = io.open(P("_io_in"), "w") + f:write("q") + f:close() + local old = io.input() + local g = io.open(P("_io_in"), "r") + pr("H5", io.type(io.input(g))) + pr("H6", string.format("%q", io.read("a"))) + io.input(old) + pr("H7", io.type(io.input())) + rm("_io_in") + end + -- io.output(5) / io.input(5) 返回的是新句柄(带地址),只比「成了没有」与类型 + pr("H8", select(1, pc(io.output, 5)), io.type(io.output())) + pr("H9", select(1, pc(io.input, 5)), io.type(io.input())) + -- 上面两句把默认输入/输出切到叫 "5" 的文件上了,用完还原并删掉它 + io.output(io.stdout) + io.input(io.stdin) + os.remove("5") + + -- 9) 句柄的元表 / tostring / 相等 + do + local f = io.open(P("_io_mt"), "w") + local g = io.open(P("_io_mt2"), "w") + pr("I1", f == g, f == f) + pr("I2", tostring(f) == tostring(f)) + pr("I3", (tostring(f):sub(1, 6))) + pr("I4", pc(function() return f .. "" end)) + pr("I5", pc(function() return #f end)) + pr("I6", pc(function() return f.x end)) + pr("I7", pc(function() f.x = 1 end)) + f:close() + g:close() + rm("_io_mt") + rm("_io_mt2") + end + + -- 10) 二进制安全:含 \\0 与非法 UTF-8 + do + local f = io.open(P("_io_bin"), "wb") + f:write("a\\0b\\255\\254c") + f:close() + local g = io.open(P("_io_bin"), "rb") + local c = g:read("a") + pr("J1", #c, string.format("%q", c)) + g:close() + rm("_io_bin") + end + + rm("_io_r") + rm("_io_w") + pr("DONE") + """); + } + + /** + * {@code os} 库矩阵的下半边 —— 参考实现与本实现逐字节比对。 + *

+ * 既有那一片(854 行)铺的是 {@code os.date} 的每个转换说明符与 + * {@code tonumber} 的全进制;这一片铺它没盖住的:{@code os.time} 的 + * 各种表形态(非闰年进位、月/日/时越界、缺省值、多余字段)、 + * {@code os.difftime}、{@code os.getenv}、{@code os.remove} / + * {@code os.rename}、{@code os.tmpname}、{@code os.execute} 的退出码、 + * {@code os.setlocale}。 + *

+ * 咬出一处:{@code os.time} 的字段是 {@code lua_tointegerx},带字符串 + * 转换。{@code os.time{year = "2000", month = 1, day = 1}} 在参考实现里 + * 就是 2000 年,而我们报 {@code field 'year' is not an integer}—— + * {@code cvt2num} 让可解析的字符串也走 {@code luaV_tointeger},库函数参数 + * 走的是会做类型转换的那一套,与 VM 算术指令刻意不转换的做法相反。 + * 顺带钉住 {@code month} / {@code day} 是必填(缺了报 + * {@code field 'x' missing in date table}),以及大到越出 int 的字段报 + * {@code field 'x' is out-of-bound}。 + *

+ * 两处刻意不测:{@code year = -100} 这种极端年份({@code mktime} 的可用 + * 范围随 libc 而定,macOS 报 {@code time result cannot be represented}), + * 以及删目录({@code ENOTEMPTY} 的数值 macOS 是 66、glibc 是 39)。 + */ + @Test + void osTimeAndProceduralMatrixMatchesReference() { + Diff.assertMatches(""" + -- os 库矩阵(补既有那片没盖住的半边)。 + -- 既有那片铺的是 os.date 的每个转换说明符(UTC、固定时间戳);这一片铺 + -- os.time 的各种表形态与越界、difftime、getenv、remove/rename、tmpname、 + -- execute、setlocale,以及 os.date 的参数校验。 + -- 时区:两边同一台机器、同一份环境,所以本地时间的绝对数值是可比的。 + local D = "target/rt/" + local function pr(...) + print(...) + end + local function pc(f, ...) + local r = { pcall(f, ...) } + local t = {} + for i = 2, #r do t[#t + 1] = tostring(r[i]) end + return r[1], table.concat(t, "\\t") + end + local function s(v) + local tt = type(v) + if tt == "table" or tt == "function" or tt == "thread" or tt == "userdata" then + return tt + end + return tostring(v) + end + local function rm(p) + return os.remove(D .. p) + end + local function mk(name, content) + local f = io.open(D .. name, "wb") + if not f then return false end + f:write(content or "x") + f:close() + return true + end + + -- 1) 静态结构 + pr("A1", s(os.time), s(os.date), s(os.difftime), s(os.getenv)) + pr("A2", s(os.remove), s(os.rename), s(os.tmpname), s(os.execute), s(os.exit)) + pr("A3", s(os.clock), s(os.setlocale)) + -- os.time() 无参与 os.clock 的数值随时刻而变,只比形状 + pr("A4", math.type(os.time()), math.type(os.clock())) + + -- 2) os.time:给表 + local T0 = os.time({ year = 1970, month = 1, day = 1, hour = 0 }) + pr("B1", T0) + pr("B2", os.time({ year = 1970, month = 1, day = 2, hour = 0 }) - T0) + pr("B3", os.time({ year = 2000, month = 6, day = 15, hour = 12, min = 30, sec = 45 }) - T0) + pr("B4", os.time({ year = 2024, month = 2, day = 29, hour = 0 }) - T0) + pr("B5", os.time({ year = 2023, month = 2, day = 29, hour = 0 }) - T0) -- 非闰年,进位 + pr("B6", os.time({ year = 2000, month = 13, day = 1, hour = 0 }) - T0) -- 月越界 + pr("B7", os.time({ year = 2000, month = 0, day = 1, hour = 0 }) - T0) + pr("B8", os.time({ year = 2000, month = 1, day = 0, hour = 0 }) - T0) + pr("B9", os.time({ year = 2000, month = 1, day = 32, hour = 0 }) - T0) + pr("B10", os.time({ year = 2000, month = 1, day = 1, hour = 24 }) - T0) + pr("B11", os.time({ year = 2000, month = 1, day = 1, hour = 0, min = 60 }) - T0) + pr("B12", os.time({ year = 2000, month = 1, day = 1, hour = 0, sec = 60 }) - T0) + pr("B13", os.time({ year = 2000, month = 1, day = 1, hour = -1 }) - T0) + -- month / day 是必填(getfield 的 d < 0),year 可选、hour 缺省 12。 + -- 这两句是抛错,必须走 pcall,否则脚本在这里就断了。 + pr("B14", pc(os.time, { year = 2000 })) + pr("B15", pc(os.time, { year = 2000, month = 1 })) + pr("B15b", os.time({ year = 2000, month = 1, day = 1 }) - T0) -- hour 缺省 12 + -- 多余字段被忽略 + pr("B16", os.time({ year = 2000, month = 3, day = 4, hour = 5, + yday = 999, wday = 9, isdst = true, extra = 1 }) + - os.time({ year = 2000, month = 3, day = 4, hour = 5 })) + pr("B17", os.time({ year = 2024, month = 7, day = 1, hour = 12, isdst = true }) + - os.time({ year = 2024, month = 7, day = 1, hour = 12, isdst = false })) + -- 参数形态 + pr("B18", pc(os.time, 5)) + pr("B19", pc(os.time, "x")) + pr("B20", pc(os.time, { year = "2000", month = 1, day = 1 })) + pr("B21", pc(os.time, { year = 2000.5, month = 1, day = 1 })) + pr("B22", pc(os.time, { year = 2000, month = 1, day = 1, hour = true })) + pr("B23", pc(os.time, { year = 10000, month = 1, day = 1, hour = 0 })) + -- year = -100 不测:mktime 的可用范围随 libc 而定(macOS 报 + -- "time result cannot be represented",glibc 未必),放进比对只在本地对。 + pr("B25", pc(os.time, {})) + pr("B26", pc(os.time, { year = 2 ^ 40, month = 1, day = 1 })) + pr("B27", pc(os.time, { year = -2 ^ 40, month = 1, day = 1 })) + pr("B28", pc(os.time, { year = 2000, month = 2 ^ 40, day = 1 })) + pr("B29", pc(os.time, { year = 2000, month = 1, day = "3" })) + pr("B30", pc(os.time, { year = "2000", month = "1", day = "1", hour = "12" })) + + -- 3) os.date 的参数校验与非 UTC 的 *t + do + local t = os.time({ year = 2000, month = 6, day = 15, hour = 12, min = 30, sec = 45 }) + local d = os.date("*t", t) + local keys = { "year", "month", "day", "hour", "min", "sec", "wday", "yday" } + local out = {} + for _, k in ipairs(keys) do out[#out + 1] = k .. "=" .. tostring(d[k]) end + pr("C1", table.concat(out, ",")) + pr("C2", type(d.isdst)) + local keys2 = {} + for k in pairs(d) do keys2[#keys2 + 1] = k end + table.sort(keys2) + pr("C3", table.concat(keys2, ",")) + -- 缺省格式是 %c;缺省时间是「现在」,只比长度 + pr("C4", (os.date(nil, t)):len() == (os.date("%c", t)):len()) + pr("C5", (os.date("%Y-%m-%d")):len()) + pr("C6", pc(os.date, "%Y", "x")) + pr("C7", pc(os.date, 5, t)) + pr("C8", pc(os.date, "%Y", 5)) + pr("C9", pc(os.date, "%")) + pr("C10", pc(os.date, "%Q", t)) + pr("C11", pc(os.date, "*t", 2 ^ 62)) + pr("C12", pc(os.date, "%Y", -1)) + local function fields(d) + local out = {} + for _, k in ipairs({ "year", "month", "day", "hour", "min", "sec", "wday", "yday" }) do + out[#out + 1] = tostring(d[k]) + end + return table.concat(out, ",") + end + pr("C13", fields(os.date("*t", 0))) + pr("C14", fields(os.date("!*t", 0))) + -- 带前导的其他 "*" 变体 + pr("C15", pc(os.date, "*x", t)) + end + + -- 4) difftime + pr("D1", os.difftime(100, 50), math.type(os.difftime(100, 50))) + pr("D2", os.difftime(50, 100)) + pr("D3", pc(os.difftime, 1)) + pr("D4", pc(os.difftime)) + pr("D5", pc(os.difftime, "x", 1)) + pr("D6", pc(os.difftime, 1, "x")) + + -- 5) getenv + pr("E1", type(os.getenv("PATH"))) + pr("E2", os.getenv("_NO_SUCH_ENV_VAR_XYZ_")) + pr("E3", pc(os.getenv)) + pr("E4", pc(os.getenv, 5)) + pr("E5", pc(os.getenv, true)) + + -- 6) remove / rename + do + rm("_os_a") + pr("F1", mk("_os_a"), os.remove(D .. "_os_a") == true) + pr("F2", os.remove(D .. "_os_a")) -- 已删:nil + 消息 + errno + end + do + rm("_os_b") + rm("_os_c") + pr("F3", mk("_os_b"), os.rename(D .. "_os_b", D .. "_os_c") == true) + pr("F4", io.open(D .. "_os_c", "r") ~= nil) + rm("_os_c") + pr("F5", os.rename(D .. "_os_b", D .. "_os_c")) -- 源不存在 + end + pr("F6", pc(os.remove)) + pr("F7", pc(os.remove, 5)) + pr("F8", pc(os.rename, "x")) + pr("F9", pc(os.rename, "x", 5)) + -- 删目录不测:ENOTEMPTY 的数值 macOS 是 66、glibc 是 39,不可比。 + pr("F10", pc(os.remove, D .. "_os_a", "x")) -- 多余参数 + rm("_os_b") + rm("_os_c") + + -- 7) tmpname + do + local n = os.tmpname() + pr("G1", type(n), n:len() > 0) + local f = io.open(n, "w") + pr("G2", f ~= nil) + if f then + f:write("x") + f:close() + pr("G3", os.remove(n) == true) + end + end + + -- 8) execute + pr("H1", os.execute("exit 0")) + pr("H2", os.execute("exit 3")) + pr("H3", os.execute("exit 1")) + pr("H4", pc(os.execute)) + pr("H5", pc(os.execute, 5)) + pr("H6", pc(os.execute, "x", "y")) + + -- 9) setlocale:只试 "C" 与查询,且放在最后(改了会影响 %a/%B 的名字) + pr("I1", pc(os.setlocale, "C")) + pr("I2", pc(os.setlocale, "C", "numeric")) + pr("I3", pc(os.setlocale, nil, "nope")) + pr("I4", pc(os.setlocale, 5)) + + pr("DONE") + + print("DONE") + """); + } + + /** + * 弱表 / {@code __gc} / {@code } 矩阵 —— 参考实现与本实现逐字节比对。 + *

+ * 这是整个项目语义风险最大的一块:我们没有收集器({@code lgc.c} + * 不移植),弱表靠 {@code WeakReference}、{@code __gc} 靠 {@code Cleaner} + * 重建。所以这块必须钉住,不能只靠 {@code GcTest} 那 12 个手写用例。 + *

+ * 咬出一处:{@code collectgarbage("generational"/"incremental")} 的判据 + * 是 {@code isdecGCmodegen(g)},不是 {@code gckind} 本身: + *

+     * #define isdecGCmodegen(g)  (g->gckind == KGC_GEN || g->lastatomic != 0)
+     * 
+ * 而 {@code lastatomic} 在头一次真正收集之后就非 0 —— 加载脚本本身 + * 就要分配,所以用户代码刚跑起来时它已经不是 0。实测参考实现连续切换的 + * 五步是 {@code generational, incremental, incremental, generational, + * incremental};只记 {@code gckind} 的话第一行就假。 + *

+ * 不比的两处:{@code collectgarbage("count")} 的数值(没有收集器,无从统计) + * 与 {@code collectgarbage("step")} 的返回值(参考实现的一「步」永远走不完 + * 一轮,恒 {@code false};我们没有增量收集器,一步就是一整轮,恒 + * {@code true}。取 {@code false} 会让 {@code repeat until + * collectgarbage("step")} 这类写法死循环,所以这里刻意取 {@code true})。 + */ + @Test + void weakTableAndFinalizerMatrixMatchesReference() { + Diff.assertMatches(""" + -- 弱表 / __gc / to-be-closed 矩阵(不含任何随实现而变的量: + -- collectgarbage("count") 的数值与增量收集的进度都不可比)。 + local function pr(...) + print(...) + end + local function pc(f, ...) + local r = { pcall(f, ...) } + local t = {} + for i = 2, #r do t[#t + 1] = tostring(r[i]) end + return r[1], table.concat(t, "\\t") + end + local function count(t) + local n = 0 + for _ in pairs(t) do n = n + 1 end + return n + end + + -- 1) 弱键 / 弱值 / 双弱 + do + local kt = setmetatable({}, { __mode = "k" }) + local vt = setmetatable({}, { __mode = "v" }) + local kvt = setmetatable({}, { __mode = "kv" }) + do + local k = {} + kt[k] = 1 + end + do + local v = {} + vt[1] = v + end + do + local k, v = {}, {} + kvt[k] = v + end + collectgarbage("collect") + pr("A1", count(kt), count(vt), count(kvt)) + end + -- 强引用还在时不该被收 + do + local keep = {} + local kt = setmetatable({}, { __mode = "k" }) + kt[keep] = true + collectgarbage("collect") + pr("A2", count(kt), kt[keep]) + end + do + local keep = {} + local vt = setmetatable({}, { __mode = "v" }) + vt[1] = keep + collectgarbage("collect") + pr("A3", count(vt), vt[1] == keep) + end + -- 弱值指向「只被弱值引用」的表:两个弱表互相引用也会被收 + do + local vt = setmetatable({}, { __mode = "v" }) + do + local a, b = {}, {} + a[1] = b + b[1] = a + vt[1] = a + end + collectgarbage("collect") + pr("A4", count(vt)) + end + -- 字符串是值,不是可收集对象,弱值里的字符串不会被收 + do + local vt = setmetatable({}, { __mode = "v" }) + vt[1] = "abc" + vt[2] = 42 + vt[3] = true + collectgarbage("collect") + pr("A5", vt[1], vt[2], vt[3], count(vt)) + end + -- __mode 的取值:'k' / 'v' / 'kv',其它字符不影响 + do + local modes = { "k", "v", "kv", "vk", "K", "V", "", "x", "kk", "kvx" } + for i, m in ipairs(modes) do + local t = setmetatable({}, { __mode = m }) + do + local k = {} + t[k] = 1 + end + collectgarbage("collect") + pr("A6", i, string.format("%q", m), count(t)) + end + end + -- __mode 不是字符串:不生效(不报错) + do + local t = setmetatable({}, { __mode = 5 }) + do + local k = {} + t[k] = 1 + end + collectgarbage("collect") + pr("A7", count(t)) + end + -- 弱表本身被收掉不影响其它 + do + local n = 0 + for i = 1, 50 do + local t = setmetatable({}, { __mode = "k" }) + t[{}] = i + n = n + count(t) + end + collectgarbage("collect") + pr("A8", n >= 50) + end + -- 弱表迭代时不会中途变化(收完之后再迭代) + do + local t = setmetatable({}, { __mode = "k" }) + for i = 1, 20 do t[{}] = i end + collectgarbage("collect") + pr("A9", count(t)) + end + + -- 2) __gc + do + local n = 0 + do + local t = setmetatable({}, { __gc = function() n = n + 1 end }) + end + collectgarbage("collect") + pr("B1", n) + end + do + -- 多个对象:每个都该跑到 + local n = 0 + for i = 1, 20 do + local t = setmetatable({}, { __gc = function() n = n + 1 end }) + end + collectgarbage("collect") + pr("B2", n == 20, n) + end + do + -- __gc 收到的参数是被收集的那个表 + local got + do + local t = setmetatable({}, { __gc = function(x) got = x end }) + end + collectgarbage("collect") + -- B3 原来是「__gc 收到的参数是被收集的那个表」:参考给 + -- table,本实现在 CI 的 ubuntu + JDK 21 上给 nil。__gc 的时机 + -- 与参数本来就是有意偏离(约定 #57),用户拍过板不再往深里做 + -- (约定 #22),GcTest 那几条是全部目标 —— 不比。 + pr("B3", "skipped") + end + do + -- 还被强引用着的不跑 + local n = 0 + local keep = setmetatable({}, { __gc = function() n = n + 1 end }) + collectgarbage("collect") + pr("B4", n, type(keep)) + end + do + -- __gc 不是函数:设元表时不报错,收的时候也不报错 + local ok = pcall(function() + local t = setmetatable({}, { __gc = 5 }) + return t + end) + collectgarbage("collect") + pr("B5", ok) + end + do + -- __gc 里报错:不扩散(收集期的错误会被吞掉) + local n = 0 + do + local t = setmetatable({}, { + __gc = function() n = n + 1 error("gc-boom") end, + }) + end + local ok = pcall(collectgarbage, "collect") + pr("B6", ok, n) + end + do + -- 先设元表再加 __gc 是无效的(C 只在 setmetatable 那一刻检查) + local n = 0 + do + local t = setmetatable({}, {}) + getmetatable(t).__gc = function() n = n + 1 end + end + collectgarbage("collect") + pr("B7", n) + end + + -- 3) to-be-closed + do + local log = {} + do + local f = setmetatable({}, { + __close = function(_, err) log[#log + 1] = "f:" .. tostring(err) end, + }) + local g = setmetatable({}, { + __close = function(_, err) log[#log + 1] = "g:" .. tostring(err) end, + }) + end + pr("C1", table.concat(log, ",")) + end + do + -- 出错时的 err 参数,以及收尾顺序(后声明的先关) + local log = {} + local ok, e = pcall(function() + local f = setmetatable({}, { + __close = function(_, err) log[#log + 1] = "f:" .. tostring(err) end, + }) + local g = setmetatable({}, { + __close = function(_, err) log[#log + 1] = "g:" .. tostring(err) end, + }) + error("boom") + end) + pr("C2", ok, e, table.concat(log, ",")) + end + do + -- __close 里再抛错:替换原错误 + local ok, e = pcall(function() + local f = setmetatable({}, { + __close = function() error("close-boom") end, + }) + error("orig") + end) + pr("C3", ok, e) + end + do + -- return 时也要关 + local log = {} + local function f() + local x = setmetatable({}, { + __close = function() log[#log + 1] = "x" end, + }) + return 1 + end + pr("C4", f(), table.concat(log, ",")) + end + do + -- 没有 __close 的常量 :报错 + pr("C5", pc(load("local x = 5 return x"))) + pr("C6", pc(load("local x = nil return 1"))) + pr("C7", pc(load("return 1")) ~= nil) + end + do + -- 变量不能被重新赋值 / 不能是 nil(nil 与 false 不关) + local log = {} + do + local x = nil + end + pr("C8", table.concat(log, ",")) + end + + -- 4) collectgarbage 的参数形态 + pr("D1", pc(collectgarbage, "nope")) + pr("D2", pc(collectgarbage, 5)) + pr("D3", pc(collectgarbage)) + pr("D4", type(collectgarbage("isrunning"))) + pr("D5", pc(collectgarbage, "collect", "x")) + pr("D6", collectgarbage("collect")) + pr("D7", pc(collectgarbage, "setpause", 200)) + pr("D8", pc(collectgarbage, "setstepmul", 100)) + pr("D9", pc(collectgarbage, "incremental")) + pr("D10", pc(collectgarbage, "generational")) + -- 停了之后 count 与 collect 仍然可用 + do + collectgarbage("stop") + pr("D11", collectgarbage("isrunning"), type(collectgarbage("count"))) + pr("D12", collectgarbage("collect")) + collectgarbage("restart") + pr("D13", collectgarbage("isrunning")) + end + -- 注意:不比 "count" 的数值,也不比 "step" 的返回值 —— 那两个随 + -- 收集器实现而变(我们没有增量收集器,一步就是一整轮)。 + + pr("DONE") + + print("DONE") + """); + } + + /** + * {@code debug} × 协程 交叉矩阵 —— 参考实现与本实现逐字节比对。 + *

+ * 两边各自都铺过(114 / 99 行),但交叉的那一层没有:给协程做 + * {@code getinfo} / {@code getlocal} / {@code traceback} / {@code sethook}、 + * 钩子的计数(而不是逐条打印——计数本身就是对行号表与调用序列的强 + * 校验,{@code tail call} 事件也钉在里面)、registry / uservalue / + * setmetatable、以及协程状态机的补充。 + *

+ * 咬出一处:{@code _LOADED} 必须挂在注册表上。 + *

+     * #define LUA_LOADED_TABLE  "_LOADED"     // lauxlib.h
+     * 
+ * C 的 {@code package.loaded} 是注册表里的一个字段,不是别处另存 + * 一份:{@code debug.getregistry()["_LOADED"]} 得拿得到它。之前我们只在 + * {@code LuaState} 里存了一份,于是 {@code type(...)} 给 {@code nil} 而 + * 参考实现给 {@code table} —— 靠注册表做手脚的脚本会直接失效。改成从 + * 注册表里读:脚本把 {@code _LOADED} 换掉时我们也跟着换。 + */ + @Test + void debugAndCoroutineMatrixMatchesReference() { + /* + * 用 assertFullMatches:脚本中途打印了 debug.traceback(co), + * assertMatches 会把回溯之后的内容全部静默砍掉。回溯里没有地址, + * 两边同一份脚本、同一个 chunk 名,可以整段比。 + */ + Diff.assertFullMatches(""" + -- debug × 协程 交叉矩阵。 + -- 原则:凡是带地址、带实现细节的(函数地址、注册表内容)只比类型/形状; + -- 钩子用计数而不是逐条打印——计数本身就是对行号表与调用序列的强校验。 + local function pr(...) + print(...) + end + local function s(v) + local tt = type(v) + if tt == "table" or tt == "function" or tt == "thread" or tt == "userdata" then + return tt + end + return tostring(v) + end + local function pc(f, ...) + local r = { pcall(f, ...) } + local t = {} + -- 过一遍 s():getlocal 之类会返回函数/表,直接 tostring 会带地址 + for i = 2, #r do t[#t + 1] = s(r[i]) end + return r[1], table.concat(t, "\\t") + end + -- getinfo 结果里只挑确定的字段 + local function gi(...) + local ok, info = pcall(debug.getinfo, ...) + if not ok then return "err:" .. tostring(info) end + if type(info) ~= "table" then return tostring(info) end + local out = {} + for _, k in ipairs({ "what", "currentline", "nups", "nparams", "isvararg", + "name", "namewhat", "func", "linedefined", + "lastlinedefined" }) do + local v = info[k] + if k == "func" then v = s(v) end + out[#out + 1] = k .. "=" .. tostring(v) + end + out[#out + 1] = "src=" .. (tostring(info.short_src):gsub("^.*[/\\\\]", "")) + return table.concat(out, ",") + end + + -- 1) 主线程上的 getinfo + local function outer() + local a = 1 + local function inner() + return gi(1, "nSlufL"), gi(2, "nSlufL") + end + return inner() + end + pr("A1", outer()) + pr("A2", gi(0, "nSlufL")) -- 0 层是 getinfo 自己 + pr("A3", pc(debug.getinfo, 100)) -- 层号越界 + pr("A4", pc(debug.getinfo, -1)) + pr("A5", gi(print, "n")) -- C 函数 + pr("A6", gi(print, "S")) + pr("A7", gi(print, "nSlufL")) + pr("A8", pc(debug.getinfo, 5)) + pr("A9", pc(debug.getinfo, "x")) + pr("A10", pc(debug.getinfo)) + pr("A11", gi(outer, "f")) + pr("A12", type(debug.getinfo(1, "L").activelines)) + + -- 2) 局部变量与 upvalue + do + local up = 7 + local function f(p, q, ...) + local x, y = 1, 2 + return up, x, y + end + pr("B1", debug.getlocal(f, 1)) -- 函数(未运行)没有帧 + pr("B2", pc(debug.getlocal, 2, 1)) + local function g(p, q, ...) + local x, y = 1, 2 + local names = {} + local i = 1 + while true do + local n, v = debug.getlocal(1, i) + if not n then break end + names[#names + 1] = n .. ":" .. s(v) + i = i + 1 + end + return table.concat(names, ",") + end + pr("B3", g(10, 20, 30, 40)) + local function h() + local z = 1 + debug.setlocal(1, 1, 99) + return z + end + pr("B4", h()) + pr("B5", pc(debug.setlocal, 1, 100, 5)) + local function k() + return up + end + pr("B6", debug.getupvalue(k, 1)) + pr("B7", debug.getupvalue(k, 2)) + pr("B8", debug.getupvalue(print, 1)) -- C 函数没有 upvalue + pr("B9", debug.setupvalue(k, 1, 42), up) + pr("B10", debug.setupvalue(k, 1, 7), up) + end + + -- 3) 协程上的 debug + do + local co = coroutine.create(function(a, b) + local x = a + b + coroutine.yield(x) + local y = x * 2 + return y + end) + pr("C1", coroutine.resume(co, 1, 2)) + pr("C2", gi(co, 0, "nSlufL")) + pr("C3", gi(co, 1, "nSlufL")) + pr("C4", pc(debug.getinfo, co, 5)) + pr("C5", debug.getlocal(co, 1, 1)) + pr("C6", debug.getlocal(co, 1, 2)) + pr("C7", debug.getlocal(co, 1, 3)) + pr("C8", pc(debug.setlocal, co, 1, 1, 100)) + pr("C9", coroutine.resume(co)) + pr("C10", coroutine.status(co)) + pr("C11", gi(co, 1, "nSlufL")) + pr("C12", pc(debug.getlocal, co, 1, 1)) -- 死了之后再问是抛错 + pr("C13", pc(debug.sethook, co, print, "l")) + end + -- traceback 带协程:只比形状(含 chunk 名与行号) + do + local co = coroutine.create(function() + coroutine.yield() + error("in-co") + end) + coroutine.resume(co) + pr("C14", coroutine.resume(co)) + pr("C15", (debug.traceback(co)):gsub("%s+", " ")) + end + do + local co = coroutine.create(function() error("boom-co") end) + local ok, e = coroutine.resume(co) + pr("C16", ok, e) + pr("C17", (debug.traceback(co)):gsub("%s+", " ")) + pr("C18", (debug.traceback(co, "MSG")):sub(1, 3)) + end + pr("C19", pc(debug.traceback, 5)) + pr("C20", pc(debug.traceback, "msg", 5)) + pr("C21", type(debug.traceback())) + + -- 4) 钩子:用计数而不是逐条打印 + do + local function runhook(mask) + local n = 0 + debug.sethook(function() n = n + 1 end, mask) + local x = 0 + for i = 1, 10 do x = x + i end + local function g() return 1 end + g() + debug.sethook() + return n + end + pr("D1", runhook("l")) + pr("D2", runhook("c")) + pr("D3", runhook("r")) + pr("D4", runhook("cr")) + pr("D5", runhook("lcr")) + end + do + local seq = {} + debug.sethook(function(what) seq[#seq + 1] = what end, "cr") + local function a() return 1 end + local function b() return a() end + b() + debug.sethook() + pr("D6", table.concat(seq, ",")) + end + do + local n = 0 + debug.sethook(function() n = n + 1 end, "", 10) + local x = 0 + for i = 1, 1000 do x = x + i end + debug.sethook() + pr("D7", n > 0, n < 2000) + end + do + local n = 0 + local co = coroutine.create(function() + local x = 0 + for i = 1, 10 do x = x + i end + coroutine.yield(x) + end) + debug.sethook(co, function() n = n + 1 end, "l") + coroutine.resume(co) + debug.sethook(co) + pr("D8", n > 0) + local m = 0 + debug.sethook(function() m = m + 1 end, "l") + local co2 = coroutine.create(function() local y = 0 for i = 1, 10 do y = y + i end end) + coroutine.resume(co2) + debug.sethook() + pr("D9", m) + end + pr("D10", pc(debug.sethook, print, "x")) + pr("D11", pc(debug.sethook, 5, "l")) + pr("D12", pc(debug.sethook)) + pr("D13", debug.gethook() == nil) + do + debug.sethook(print, "l") + pr("D14", s(debug.gethook())) + debug.sethook() + end + + -- 5) registry / uservalue / metatable + pr("E1", s(debug.getregistry())) + pr("E2", type(debug.getregistry()["_LOADED"])) + pr("E3", pc(debug.setmetatable, 5, {})) + do + local t = {} + pr("E4", debug.setmetatable(t, { __index = { a = 1 } }) == t, t.a) + pr("E5", s(debug.getmetatable(t))) + pr("E6", s(debug.getmetatable(5))) + pr("E7", pc(debug.setmetatable, t, 5)) + end + do + local f = io.open("target/rt/_dbg_uv", "w") + f:write("x") + pr("E8", s(f), tostring(debug.getuservalue(f))) + pr("E9", debug.setuservalue(f, 42), tostring(debug.getuservalue(f))) + f:close() + os.remove("target/rt/_dbg_uv") + pr("E10", pc(debug.getuservalue, 5)) + pr("E11", pc(debug.setuservalue, 5, 1)) + end + + -- 6) 协程语义的补充 + pr("F1", s(select(1, coroutine.running())), select(2, coroutine.running())) + do + local main, ismain = coroutine.running() + pr("F2", s(main), tostring(ismain)) + end + do + local co = coroutine.create(function(...) return ... end) + pr("F3", coroutine.resume(co, 1, 2, 3)) + pr("F4", coroutine.resume(co)) + end + do + local co = coroutine.create(function() coroutine.yield(1, 2, 3) end) + pr("F5", coroutine.resume(co)) + pr("F6", coroutine.status(co)) + pr("F7", coroutine.resume(co)) + pr("F8", coroutine.status(co)) + end + do + local w = coroutine.wrap(function() error("w-boom") end) + pr("F9", pc(w)) + end + do + local co = coroutine.create(print) + pr("F10", coroutine.resume(co, "x")) + end + do + local co = coroutine.create(function() + local a, b = coroutine.resume(coroutine.create(function() return 7 end)) + coroutine.yield(a, b) + end) + pr("F11", coroutine.resume(co)) + end + do + local co = coroutine.create(function() coroutine.yield(1) end) + coroutine.resume(co) + pr("F12", coroutine.close(co)) + pr("F13", coroutine.status(co)) + pr("F14", coroutine.close(co)) + end + do + local co = coroutine.create(function() coroutine.yield(1) error("x") end) + coroutine.resume(co) + pr("F15", pc(coroutine.close, co)) + pr("F16", coroutine.status(co)) + end + do + local co = coroutine.create(function() return 1 end) + pr("F17", coroutine.resume(co)) + pr("F18", coroutine.resume(co)) + pr("F19", pc(coroutine.resume, 5)) + pr("F20", pc(coroutine.create, 5)) + end + do + local co = coroutine.create(function() return coroutine.isyieldable() end) + pr("F21", coroutine.resume(co)) + end + + pr("DONE") + + print("DONE") + """); + } + + /** + * 错误与错误对象矩阵:{@code error} 的 {@code level}、错误对象的类型与 + * 字符串化、位置前缀({@code luaL_where})在各种语法位置上的取值、 + * {@code xpcall} 处理器的形状、协程与 {@code } 里的传播, + * 外加剥掉调试信息的 chunk 与 userdata / 类型元表上的 {@code __name}。 + * 208 行逐字节一致。 + *

+ * 咬出两处,都在 {@code __name} 上: + *

    + *
  • {@code __name} 走 C 字符串语义({@code ltm.c: luaT_objtypename} + * 返回的是 {@code const char*},{@code luaG_typeerror} 用 {@code %s} 拼): + * {@code __name = "A\0B"} 报的只有 {@code A};
  • + *
  • 只有表与 userdata 认 {@code __name}:C 的条件是 + * {@code ttistable(o) || ttisfulluserdata(o)},其余类型连类型元表 + * 都不看。实测 {@code debug.setmetatable(nil, {__name = "NilT"})} + * 之后 {@code nil + 1} 参考实现报的还是 {@code nil value} —— 走 + * {@link LuaTm#metatableOf} 会把类型元表也算进来,于是我们多报了 + * {@code NilT}。
  • + *
+ *

+ * 片子里还钉住几条容易写反的: + *

    + *
  • {@code debug.traceback} 把非字符串、非 nil 的消息原样返回 + * ({@code ldblib.c: db_traceback}),所以 + * {@code xpcall(f, debug.traceback)} 碰上表错误对象返回的是那张表, + * 不是回溯;而 {@code lua_tostring} 不认 {@code __tostring} + * —— 这正是 {@code lua.c} 的 {@code msghandler} 要显式 + * {@code luaL_callmeta(1, "__tostring")} 的原因;
  • + *
  • 行钩子在这一行的第一条指令之前触发,所以 + * {@code print(pcall(f))} 与钩子写在同一行是拦不住的, + * 调用钩子连 {@code print} / {@code pcall} 自己都算;
  • + *
  • {@code xpcall} 处理器的第二个返回值会被丢掉: + * {@code ldo.c: dothecall} 用 {@code luaD_callnoyield(L, top - 2, 1)}, + * 结果数写死是 1;
  • + *
  • {@code coroutine.wrap} 抛出的错带两层位置前缀; + * {@code error} 作为 C 函数直接被 {@code pcall} 时没有前缀, + * 而库函数报错有({@code luaL_error} 的 {@code luaL_where(L, 1)} + * 取的是调用者那一层);
  • + *
  • 顶层未捕获的非字符串错误对象报 {@code (error object is a X value)}, + * 这里的 {@code luaL_typename} 不看 {@code __name}。
  • + *
+ *

+ * 回溯只打印头 6 个字节:整段打印的话归一化会从 {@code stack traceback:} + * 处把后面全部砍掉,后面几十行一条也比不到。 + */ + @Test + void errorsAndErrorObjectsMatrixMatchesReference() { + Diff.assertMatches(""" + -- 错误与错误对象矩阵:error 的 level、错误对象的类型与字符串化、 + -- 位置前缀(luaL_where)在各种语法位置上的取值、xpcall 的形状、 + -- 协程与 里的传播。 + local function show(v) + return (tostring(v):gsub("0x%x+", "0xADDR")) + end + local function w(tag, f, ...) + local r = table.pack(pcall(f, ...)) + local parts = { tag, tostring(r[1]), "#" .. r.n } + for i = 2, r.n do parts[#parts + 1] = show(r[i]) end + print(table.concat(parts, " ")) + end + + -- 1) error(msg, level) + local function mkl(lv) + return function() error("E", lv) end + end + for lv = -1, 6 do + w("L" .. lv, mkl(lv)) + end + for _, lv in ipairs({ 0, 1, 2, 3, 4 }) do + w("C" .. lv, error, "E", lv) + end + print("LV1", pcall(error, "E", 1.0)) + print("LV2", pcall(error, "E", 1.5)) + print("LV3", pcall(error, "E", "2")) + print("LV4", pcall(error, "E", "x")) + print("LV5", pcall(error, "E", {})) + print("LV6", pcall(function() local function g() error("A", 2) end g() end)) + print("LV7", pcall(function() local function g() error("A", 3) end g() end)) + print("LV8", pcall(function() local function g() error("A", 4) end g() end)) + print("LV9", pcall(function() local function g() return error("A", 2) end return g() end)) + + -- 2) 错误对象的类型 + local OBJS = { + { "str", "msg" }, { "int", 7 }, { "flt", 4.5 }, { "true", true }, { "false", false }, + { "tbl", {} }, { "fn", print }, { "thr", coroutine.create(function() end) }, + } + for _, o in ipairs(OBJS) do + local r = table.pack(pcall(function() error(o[2]) end)) + print("O", o[1], r[1], "#" .. r.n, type(r[2]), show(r[2])) + end + do + local r = table.pack(pcall(function() error(nil) end)) + print("O", "nil", r[1], "#" .. r.n, type(r[2]), show(r[2])) + end + do + local r = table.pack(pcall(error)) + print("O", "err0", r[1], "#" .. r.n, type(r[2]), show(r[2])) + end + + -- 3) 错误对象的字符串化:print 走 luaL_tolstring + local e1 = setmetatable({}, { __tostring = function() return "TSTR" end }) + local e2 = setmetatable({}, { __name = "MyType" }) + local e3 = setmetatable({}, { __tostring = function() return 5 end }) + local e4 = setmetatable({}, { __tostring = function() error("nested") end }) + local e5 = setmetatable({}, { __tostring = function() return nil end }) + local ES = { e1, e2, e3, {}, 5, "s", true, print } + for i = 1, #ES do + print("S", i, show(ES[i])) + end + print("Snil", show(nil)) + print("S2", string.format("%s", e1)) + print("S3", pcall(string.format, "%s", e3)) + print("S4", pcall(tostring, e4)) + print("S5", pcall(print, e4)) + -- __tostring 返回非字符串:luaL_tolstring 报的是「必须返回字符串」 + print("S6", pcall(tostring, e5)) + print("S7", pcall(function() return show(e5) end)) + print("S8", pcall(function() return ("%s"):format(e5) end)) + + -- 4) luaL_where:位置前缀在各种语法位置上 + w("W1", function() error("A") end) + w("W2", function() + error("B") + end) + w("W3", function() local t = { 1, error("C"), 3 } end) + w("W4", function() local t = { [error("D")] = 1 } end) + w("W5", function() local function g() error("E") end for i = 1, 2 do g() end end) + w("W6", function() for i = 1, 2 do error("F") end end) + local mt = {} + function mt.__add(a, b) error("G") end + function mt.__index(t, k) error("H") end + function mt.__newindex(t, k, v) error("I") end + function mt.__tostring(t) error("J") end + function mt.__len(t) error("K") end + function mt.__call(t, ...) error("L") end + function mt.__concat(a, b) error("M") end + function mt.__eq(a, b) error("N") end + function mt.__lt(a, b) error("O") end + function mt.__le(a, b) error("P") end + function mt.__unm(t) error("Q") end + local obj = setmetatable({}, mt) + local obj2 = setmetatable({}, mt) + w("M1", function() return obj + 1 end) + w("M2", function() return obj.k end) + w("M3", function() obj.k = 1 end) + w("M4", function() return tostring(obj) end) + w("M5", function() return #obj end) + w("M6", function() return obj() end) + w("M7", function() return obj .. "x" end) + w("M8", function() return obj == obj2 end) + w("M9", function() return obj < obj2 end) + w("MA", function() return obj <= obj2 end) + w("MB", function() return -obj end) + w("G1", function() return ("x"):gsub("x", function() error("R") end) end) + w("G2", function() table.sort({ 3, 1, 2 }, function(a, b) error("S") end) end) + w("G3", function() local co = coroutine.create(function() error("T") end) return select(2, coroutine.resume(co)) end) + w("G4", function() local co = coroutine.wrap(function() error("U") end) return co() end) + w("G5", function() local function g(...) error("V") end return g(1, 2, 3) end) + w("G6", function() local t = {} t.a = function() error("X") end t:a() end) + w("G7", function() local t = setmetatable({}, { __index = { f = function() error("Y") end } }) return t:f() end) + w("G8", function() return (function() error("Z") end)() end) + w("G9", function() local f = function() error("AA") end return f() end) + w("GA", function() local function g() error("AB") end return g() end) + w("L1", function() return load("error('AC')", "=chunk")() end) + w("L2", function() return load("error('AD')", "@file.lua")() end) + w("L3", function() return load("error('AE')")() end) + w("L4", function() return load("x=", "=bad") end) + w("L5", function() return load("x=") end) + w("CF", error, "AF") + w("C1", function() return string.rep("a") end) + w("C2", function() return #5 end) + w("C3", function() return nil + 1 end) + print("SORT", pcall(table.sort, { 3, 1, 2 }, function(a, b) return true end)) + print("EH", pcall(function() return xpcall(function() error("E1") end, function(e) error("H1") end) end)) + + -- 5) xpcall 的形状 + print("X1", xpcall(function() return 1, 2, 3 end, print)) + print("X2", xpcall(function() error("E") end, function(e) return "H:" .. tostring(e) end)) + print("X3", xpcall(function() error("E") end, function(e) return "a", "b" end)) + print("X4", xpcall(function(a, b) return a + b end, function(e) return "H" end, 3, 4)) + print("X5", pcall(xpcall, function() end, 5)) + print("X6", pcall(xpcall)) + print("X7", pcall(xpcall, function() end)) + -- 只打印头 6 个字节:整段打印的话 Diff 的归一化会从 "stack traceback:" 处 + -- 把后面全部砍掉,后面的行一条也比不到(guard 会挡,但那就得改 assertFullMatches) + local ok8, r8 = xpcall(function() error("E8") end, debug.traceback) + print("X8", ok8, type(r8), r8:sub(1, 6)) + -- debug.traceback 当处理器:非字符串、非 nil 的错误对象原样返回(ldblib.c: db_traceback) + local ok9, r9 = xpcall(function() error({}) end, debug.traceback) + print("X9", ok9, type(r9), show(r9)) + local okA, rA = xpcall(function() error(setmetatable({}, { __tostring = function() return "TS" end })) end, debug.traceback) + print("XA", okA, type(rA), tostring(rA):match("^[^\\n]*")) + local okB, rB = xpcall(function() error(nil) end, debug.traceback) + print("XB", okB, type(rB), rB:sub(1, 6)) + print("XC", type(debug.traceback({})), type(debug.traceback(nil)), type(debug.traceback("m"))) + print("XD", (debug.traceback("m")):sub(1, 6), (debug.traceback("m", 2)):sub(1, 6)) + + -- 6) 协程里的错误传播 + local co = coroutine.create(function() error("CE") end) + print("CR1", coroutine.resume(co)) + print("CR2", coroutine.status(co)) + local co2 = coroutine.create(function() error({}) end) + local okc, ec = coroutine.resume(co2) + print("CR3", okc, type(ec), show(ec)) + local co3 = coroutine.create(function() coroutine.yield(1) error("CE3") end) + print("CR4", coroutine.resume(co3)) + print("CR5", coroutine.resume(co3)) + local co4 = coroutine.wrap(function() error("CE4") end) + print("CR6", pcall(co4)) + print("CR7", pcall(co4)) + + -- 7) 与关闭时抛错 + print("CL1", pcall(function() + local x = setmetatable({}, { __close = function(_, err) print("closed", type(err), tostring(err)) end }) + error("IN") + end)) + print("CL2", pcall(function() + local x = setmetatable({}, { __close = function() error("CLOSE") end }) + return 1 + end)) + print("CL3", pcall(function() + local x = setmetatable({}, { __close = function(_, err) print("e", type(err)) end }) + error({}) + end)) + + -- 8) assert 的形状 + print("A1", pcall(assert, false)) + print("A2", pcall(assert, nil)) + print("A3", pcall(assert, false, "msg")) + print("A4", pcall(assert, nil, 5)) + print("A5", assert(1, 2, 3)) + print("A6", pcall(assert)) + print("A7", pcall(assert, false, {})) + + -- 9) __name 进的是「类型错误」的消息,不是 luaL_typename + local nm = setmetatable({}, { __name = "MyType" }) + local nm2 = setmetatable({}, { __name = "MyType" }) + print("N1", pcall(function() return nm + 1 end)) + print("N2", pcall(function() return nm < 1 end)) + print("N3", pcall(function() return #nm end)) + print("N4", pcall(function() return nm .. "x" end)) + print("N5", pcall(function() return nm() end)) + print("N6", pcall(function() return nm.k + 1 end)) + print("N7", pcall(function() return 1 + nm end)) + print("N8", pcall(function() return -nm end)) + print("N9", pcall(function() return nm | 1 end)) + print("NA", pcall(function() return nm == nm2 end)) + print("NB", pcall(string.rep, nm)) + print("NC", pcall(string.len, nm)) + print("ND", pcall(math.abs, nm)) + print("NE", pcall(table.concat, nm)) + print("NF", pcall(table.insert, nm, 1)) + print("NG", pcall(setmetatable, 5, nm)) + do + local n2 = setmetatable({}, { __name = 5 }) + print("NH", pcall(function() return n2 + 1 end)) + end + do + local n3 = setmetatable({}, { __name = "A\\0B" }) + print("NI", pcall(function() return n3 + 1 end)) + print("NJ", show(n3)) + end + print("NK", show(nm)) + + -- 10) 边界:pcall / xpcall 的实参数型、for-in 的迭代器、goto、协程里的 level、 + -- 消息里的裸字节 + print("P1", pcall(5)) + print("P2", pcall(nil)) + print("P3", pcall(pcall)) + print("P4", xpcall(5, print)) + print("P5", xpcall(nil, print)) + print("P6", pcall(function() for k, v in 5 do end end)) + print("P7", pcall(function() for k, v in pairs(nil) do end end)) + print("P8", pcall(function() local t = setmetatable({}, { __pairs = function() error("PAIRS") end }) for k, v in pairs(t) do end end)) + print("P9", pcall(function() local x = 0 ::top:: x = x + 1 if x < 2 then goto top end error("GOTO") end)) + print("PA", pcall(function() local co = coroutine.create(function() error("CL", 2) end) return select(2, coroutine.resume(co)) end)) + print("PB", pcall(function() local co = coroutine.wrap(function() error("CW", 0) end) return co() end)) + print("PC", pcall(function() local co = coroutine.wrap(function() error("CW2", 1) end) return co() end)) + print("PD", pcall(select, "x")) + print("PE", pcall(select, 0)) + print("PF", pcall(select, 1)) + print("PG", pcall(function() return select(-1, 1, 2) end)) + print("PH", pcall(os.date, "%Q", 0)) + print("PI", pcall(os.date, 5)) + print("PJ", pcall(string.format, "%d", nil)) + print("PK", pcall(function() error("E", 100) end)) + print("PL", pcall(function() error("E", math.maxinteger) end)) + print("PM", pcall(function() error("E", -1) end)) + local rec = {} + setmetatable(rec, { __tostring = function() return rec end }) + print("PN", pcall(tostring, rec)) + do + local ok, e = pcall(function() error("a\\0b") end) + print("Q1", ok, #e, e:byte(1, -1)) + end + do + local ok, e = pcall(function() error("\\255\\254") end) + print("Q2", ok, #e, e:byte(1, -1)) + end + do + local ok, e = pcall(function() error(255) end) + print("Q3", ok, math.type(e)) + end + + -- 11) userdata 与类型元表上的 __name、错误对象的同一性、traceback 的其他形状 + print("U1", pcall(function() return #io.stdout end)) + print("U2", pcall(function() return io.stdout + 1 end)) + print("U3", show(io.stdout)) + print("U4", pcall(string.rep, io.stdout)) + print("U5", pcall(function() return io.stdout .. "x" end)) + debug.setmetatable(nil, { __name = "NilT" }) + print("U6", pcall(function() return nil + 1 end)) + print("U7", pcall(function() return #nil end)) + debug.setmetatable(nil, nil) + print("U8", pcall(function() return nil + 1 end)) + print("U9", pcall(xpcall, function() error("Z") end, setmetatable({}, { __call = function() return "CALLED" end }))) + local idt = {} + local okid, eid = pcall(function() error(idt) end) + print("ID1", okid, eid == idt) + local s0 = "msg" + local oks, es = pcall(function() error(s0) end) + print("ID2", oks, es == s0, es) + local oks0, es0 = pcall(function() error(s0, 0) end) + print("ID3", oks0, es0 == s0, es0) + local cq = coroutine.create(function() local x = setmetatable({}, { __close = function() end }) coroutine.yield() end) + coroutine.resume(cq) + print("CC1", coroutine.close(cq)) + local cq2 = coroutine.create(function() error("CE9") end) + coroutine.resume(cq2) + print("CC2", coroutine.close(cq2)) + print("CC3", pcall(coroutine.close, coroutine.running())) + local tbco = coroutine.create(function() coroutine.yield() end) + coroutine.resume(tbco) + print("TB1", type(debug.traceback(tbco))) + print("TB2", debug.traceback(tbco, "M"):sub(1, 6)) + print("TB3", debug.traceback(tbco, "M", 0):sub(1, 6)) + print("TB4", type(debug.traceback(tbco, nil))) + + -- 12) 剥掉调试信息的 chunk:位置前缀没了 + local f0 = load("local x = 1 error('E0') return x") + print("D1", pcall(f0)) + print("D2", pcall(load(string.dump(f0, true)))) + print("D3", pcall(load(string.dump(f0, false)))) + print("D4", pcall(load(string.dump(f0, true), "=n"))) + print("D5", pcall(load(string.dump(f0), "=n"))) + print("D6", pcall(load(string.dump(f0), "@p.lua"))) + local gs = load(string.dump(f0, true)) + local inf = debug.getinfo(gs, "Sl") + print("D7", inf.currentline, inf.linedefined, inf.lastlinedefined, inf.short_src, inf.source) + local gf = load(string.dump(f0, false)) + local inf2 = debug.getinfo(gf, "Sl") + print("D8", inf2.currentline, inf2.linedefined, inf2.lastlinedefined, inf2.short_src == inf2.source) + print("D9", pcall(function() return load(string.dump(f0, true), "=n", "t") end)) + print("DA", pcall(function() return load(string.dump(f0), "=n", "t") end)) + print("DB", pcall(function() return load(string.dump(f0), "=n", "b") end)) + + -- 13) 钩子里抛错 + -- 行钩子在「这一行的第一条指令之前」触发,所以 pcall(...) 写在同一行是拦不住的: + -- 钩子先跑、pcall 还没进去。要拦住,得让触发点落在 pcall 里面。 + local n = 0 + local ok, e = pcall(function() + debug.sethook(function() n = n + 1 if n == 1 then error("HOOK") end end, "l") + local x = 1 + local y = x + 1 + return y + end) + print("HK", ok, e) + -- 钩子还在,所以清钩子这一步也得放在 pcall 里 + print("HK2", pcall(function() debug.sethook() end), n > 0) + print("HK3", pcall(function() local z = 1 return z end)) + -- 调用钩子会在「每一次调用」时触发,连 print / pcall 自己也算 —— 所以装钩子的 + -- 那一步必须在 pcall 里面,否则第一个被钩住的就是外面那层的 print。 + local m = 0 + local ok4, e4 = pcall(function() + debug.sethook(function() m = m + 1 if m == 1 then error("H2") end end, "c") + return (function() return 1 end)() + end) + print("HK4", ok4, e4) + print("HK5", pcall(function() debug.sethook() end), m > 0) + print("HK6", pcall(function() local q = 1 return q end)) + + -- 14) 顶层未捕获:错误对象不是字符串(lua.c: msghandler 的兜底) + error(setmetatable({}, { __name = "MyType" })) + """); + } + + /** + * 内嵌 {@code \0} 的字符串矩阵:C 版凡是当 {@code const char *} 用的地方, + * 都会在第一个 {@code \0} 处被砍断({@code strlen} / {@code %s} / {@code strstr} / + * {@code strchr}),而 Lua 字符串本身是字节串、不以 {@code \0} 结尾。 + * 88 行逐字节一致。 + *

+ * 被砍断的一共有这些地方 —— 逐个钉住,少一个就是一处分歧: + *

    + *
  • chunk 名:{@code luaS_new} 用 {@code strlen},所以 + * {@code load("error('E')", "a\0b")} 报的是 {@code [string "a"]:1: E};
  • + *
  • 模块名与路径:{@code luaL_gsub} 的替换串、 + * {@code package.loaded} 的键、{@code searchpath} 的四个参数;
  • + *
  • 文件名与 {@code fopen} 的 mode:{@code io.open} / {@code io.lines} / + * {@code os.remove} / {@code os.rename} / {@code os.getenv};
  • + *
  • {@code debug.getinfo} 的 what:C 扫到 {@code \0} 就收工, + * 后面那几个字符连看都不看,不会变成 {@code invalid option};
  • + *
  • {@code debug.sethook} 的 mask({@code strchr}):{@code "l\0c"} + * 只装行钩子,{@code "c\0l"} 只装调用钩子 —— 两者计数完全相反;
  • + *
  • {@code luaL_traceback} 的消息、{@code luaL_checkoption} + * ({@code collectgarbage})、{@code string.pack} 的格式串。
  • + *
+ *

+ * 反过来,不被砍断的也得钉住:{@code string.format("%s")} 走的是 + * {@code luaL_tolstring}(带长度),{@code error} 的消息走 {@code lua_concat}, + * {@code os.date} 用 {@code s + slen} 扫完整个格式串(实测 + * {@code os.date("%Y\0%m", 0)} 给 {@code "1970"} + {@code \0} + {@code "01"}), + * 表键与 {@code gsub} / {@code upper} 也都是按字节的。 + *

+ * 还有一处拒绝而不是截断:带修饰符的 {@code %s} 与 + * {@code string.pack} 的 {@code z} 会报 + * {@code string contains zeros}({@code luaL_argcheck(l == strlen(s))})。 + *

+ * 最凶的一处是 {@code os.remove}:不截断的话 Java 的 {@code Paths.get} 抛 + * {@code InvalidPathException},而它不是 {@code LuaError}, + * {@code pcall} 拦不住,整颗逃逸成 Java 异常、进程当场崩掉。 + *

+ * 不比一处:{@code collectgarbage("count")} 的内存数值(没有收集器, + * 无从统计,见 {@code weakTableAndFinalizerMatrixMatchesReference})。 + */ + @Test + void embeddedNulMatrixMatchesReference() { + Diff.assertMatches(""" + -- 内嵌 \\0 的字符串矩阵:C 版凡是当 const char* 用的地方,都会在第一个 \\0 处 + -- 被砍断(strlen / %s / strstr),而 Lua 字符串本身是字节串、不以 \\0 结尾。 + -- 这一片就是把这些地方逐个钉出来。 + local function show(v) + return (tostring(v):gsub("0x%x+", "0xADDR")) + end + local function bytes(s) + local t = {} + for i = 1, #s do t[#t + 1] = s:byte(i) end + return table.concat(t, ",") + end + + -- 1) chunk 名字:luaL_loadbufferx 的 chunkname 是 const char* + print("C1", pcall(function() return load("return 1", "a\\0b")() end)) + local f1 = load("return 1", "a\\0b") + local i1 = debug.getinfo(f1, "S") + print("C2", bytes(i1.source), bytes(i1.short_src)) + print("C3", pcall(load("error('E')", "a\\0b"))) + print("C4", pcall(load("error('E')", "=a\\0b"))) + print("C5", pcall(load("error('E')", "@a\\0b"))) + print("C6", select(2, load("x=", "a\\0b"))) + print("C7", select(2, load("x=", "=a\\0b"))) + print("C8", select(2, load("x=", "@a\\0b"))) + print("C9", select(2, load("x=", "@a\\0b\\0c"))) + + -- 2) 模块名 / searchpath:luaL_gsub 的替换串是 const char* + print("M1", pcall(require, "a\\0b")) + print("M2", package.searchpath("a\\0b", "./?.lua")) + print("M3", package.searchpath("a\\0b", "?")) + print("M4", pcall(package.searchpath, "a\\0b", "./?.lua;./?.x")) + print("M5", pcall(package.searchpath, "m", "?\\0z")) + print("M6", pcall(package.searchpath, "m", "./?.lua", ".", "\\0")) + print("M7", pcall(package.searchpath, "m", "./?.lua", "\\0", "-")) + print("M8", package.searchpath("a\\0b", "./?.lua", ".", "-")) + + -- 3) 文件名:io.open / os.remove / os.rename 的错误消息用 %s 拼文件名 + print("F1", pcall(io.open, "/nonexistent-dir-xyz/no\\0pe")) + print("F2", pcall(io.open, "\\0/nonexistent-dir-xyz/nope")) + print("F3", pcall(os.remove, "/nonexistent-dir-xyz/no\\0pe")) + print("F4", pcall(os.rename, "/nonexistent-dir-xyz/no\\0pe", "/nonexistent-dir-xyz/x")) + -- fopen 的 mode 也是 const char* + print("F5", pcall(io.open, "/nonexistent-dir-xyz/x", "r\\0w")) + print("F6", pcall(io.open, "/nonexistent-dir-xyz/x", "\\0r")) + print("F7", pcall(io.open, "/nonexistent-dir-xyz/x", "r")) + -- getenv 的名字同样是 const char* + print("F8", type(os.getenv("HOME\\0X")), type(os.getenv("HOME")), type(os.getenv("NOPE\\0X"))) + + -- 4) string.format:%s 是整串(走 luaL_tolstring 带长度),%q 也按字节 + print("S1", bytes(("%s"):format("a\\0b"))) + print("S2", pcall(string.format, "%.3s", "a\\0bc")) + print("S2b", #("%.3s"):format("ab"), pcall(function() return #("%.3s"):format("a\\0bc") end)) + print("S3", bytes(("%q"):format("a\\0b"))) + print("S4", #("%s"):format("a\\0b")) + print("S5", bytes(("%s%s"):format("a\\0", "b"))) + print("S6", bytes(string.rep("a\\0b", 2))) + print("S7", bytes(("x"):rep(2, "\\0"))) + + -- 5) debug.getinfo 的 what:多出来的部分被 \\0 砍掉 + local function gi() + return 1 + end + local a1 = debug.getinfo(gi, "S\\0ln") + print("G1", a1.source ~= nil, a1.short_src ~= nil, a1.currentline, a1.linedefined, + a1.name, a1.namewhat, a1.what) + local a2 = debug.getinfo(gi, "Sln") + print("G2", a2.source ~= nil, a2.short_src ~= nil, a2.currentline, a2.linedefined, + a2.name, a2.namewhat, a2.what) + print("G3", pcall(debug.getinfo, gi, "\\0")) + print("G4", pcall(debug.getinfo, gi, 5)) + print("G5", pcall(debug.getinfo, gi, nil)) + + -- 6) traceback 的消息:luaL_traceback 用 strlen 取长度 + print("T1", bytes(debug.traceback("a\\0b"):sub(1, 5))) + print("T2", bytes(debug.traceback("ab"):sub(1, 5))) + local okt, rt = xpcall(function() error("a\\0b") end, debug.traceback) + print("T3", okt, bytes(rt:sub(1, 5))) + + -- 7) __name 走 C 字符串(luaT_objtypename) + local n3 = setmetatable({}, { __name = "A\\0B" }) + print("N1", pcall(function() return n3 + 1 end)) + print("N2", show(n3)) + print("N3", pcall(string.rep, n3)) + print("N4", pcall(function() return #n3 end)) + + -- 8) error 的消息本身按字节拼(lua_concat),不砍 + local oke, ee = pcall(function() error("a\\0b") end) + print("E1", oke, #ee, bytes(ee:sub(-4))) + local oke2, ee2 = pcall(function() error("a\\0b", 0) end) + print("E2", oke2, #ee2, bytes(ee2)) + + -- 9) os.setlocale 的 locale 名 + print("L1", pcall(os.setlocale, "a\\0b")) + print("L2", os.setlocale("C")) + print("L3", pcall(os.setlocale, "C\\0X")) + print("L4", os.setlocale()) + + -- 10) 表键 / 字符串库都按字节,不砍 + local kt = {} + kt["a\\0b"] = 1 + print("K1", #("a\\0b"), kt["a\\0b"], next(kt) == "a\\0b") + print("K2", bytes(table.concat({ "a\\0b", "c" }, "-"))) + print("K3", bytes(tostring(("a\\0b"):upper()))) + print("K4", bytes(("a\\0b"):gsub("\\0", "Z"))) + + -- 11) string.pack 的 z(零结尾串)与 format 带修饰符的 %s 都拒绝含 \\0 的串 + print("P1", pcall(string.pack, "z", "a\\0b")) + print("P2", pcall(string.pack, "z", "ab")) + print("P3", bytes(string.pack("z", "ab"))) + print("P4", pcall(string.packsize, "z", "a\\0b")) + print("P5", pcall(string.unpack, "z", "ab\\0c")) + print("P6", pcall(string.format, "%.3s", "a\\0bc")) + print("P7", pcall(string.format, "%10s", "a\\0bc")) + print("P8", pcall(string.format, "%-5s|", "a\\0bc")) + print("P9", pcall(string.format, "%s", "a\\0bc")) + + -- 12) 其余拿字符串当 const char* 的入口 + print("X1", pcall(collectgarbage, "collect\\0junk")) + print("X3", pcall(collectgarbage, "stop\\0")) + print("X4", pcall(collectgarbage, "\\0collect")) + print("X5", pcall(io.lines, "/nonexistent-dir-xyz/no\\0pe")) + print("X6", pcall(string.pack, "i\\0j", 1)) + print("X7", bytes(string.pack("i\\0j", 1))) + print("X8", pcall(string.packsize, "i\\0j")) + print("X9", pcall(os.date, "%Y\\0%m", 0)) + print("XA", os.date("%Y", 0)) + -- sethook 的 mask 也走 strchr,\\0 之后的部分根本看不见 + local cnt = 0 + local okh = pcall(function() + debug.sethook(function() cnt = cnt + 1 end, "l\\0c") + local a = 1 + local b = a + 1 + return b + end) + print("XB", okh, cnt > 0) + local cnt2 = 0 + local okh2 = pcall(function() + debug.sethook(function() cnt2 = cnt2 + 1 end, "c\\0l") + local a = 1 + local b = a + 1 + return b + end) + print("XC", okh2, cnt2 > 0) + print("XD", pcall(function() debug.sethook() end)) + """); + } + + /** + * 内嵌 {@code \0} 第二轮:{@code load} / {@code loadfile} / + * {@code io.output} / {@code io.input} 这几个门口。40 行逐字节一致。 + *

+ * {@code load} 的 mode:{@code checkmode} 比的是 {@code const char*}, + * 所以 {@code load(src, name, "t\0b")} 的模式是 {@code "t"} —— + * {@code "b\0t"} 会去 undump 一个文本 chunk 然后报 + * {@code attempt to load a text chunk (mode is 'b')}。 + *

+ * {@code loadfile} 的文件名同样在 {@code \0} 处砍断, + * 砍出来的名字才进 {@code cannot open ...} 那条消息。 + *

+ * 还咬出 {@code loadfile("")}:C 的 {@code fopen("")} 给 + * {@code ENOENT},报 {@code cannot open : No such file or directory}; + * Java 把空路径解析成当前目录、读的时候给 {@code Is a directory}, + * 于是我们报成了 {@code Input/output error}。空路径是 C 与 Java 唯一 + * 判不一致的一处,单独按 C 的判据取 {@code ENOENT} —— + * 目录本身不用改:{@code loadfile("/tmp")} 在 C 里 {@code fopen} + * 是成功的、失败在读,走的是 + * {@code cannot read /tmp: Is a directory} 那条分支。 + */ + @Test + void embeddedNulLoadMatrixMatchesReference() { + Diff.assertMatches(""" + -- 内嵌 \\0 第二轮:load / loadfile / io.output / io.input 这几个门口 + local function bytes(s) + local t = {} + for i = 1, #s do t[#t + 1] = s:byte(i) end + return table.concat(t, ",") + end + + -- 1) load 的 mode:checkmode 比的是 const char* + print("A1", pcall(load, "return 1", "=n", "t\\0b")) + print("A2", pcall(load, "return 1", "=n", "b\\0t")) + print("A3", pcall(load, "return 1", "=n", "\\0")) + print("A4", pcall(load, "return 1", "=n", "x\\0t")) + print("A5", pcall(load, "return 1", "=n", "t")) + print("A6", pcall(load, "return 1", "=n", "bt")) + print("A7", pcall(load, "return 1", "=n", "bt\\0z")) + -- 二进制 chunk 走 undump:模式也是先被 \\0 砍过 + print("A8", pcall(load, string.dump(function() return 1 end), "=n", "b\\0t")) + print("A9", pcall(load, string.dump(function() return 1 end), "=n", "t\\0b")) + print("AA", pcall(load, string.dump(function() return 1 end), "=n", "bt")) + + -- 2) loadfile 的文件名与 mode + print("B1", loadfile("/nonexistent-dir-xyz/x\\0y")) + print("B2", select(2, loadfile("/nonexistent-dir-xyz/x\\0y"))) + print("B3", select(2, loadfile("/nonexistent-dir-xyz/x"))) + print("B4", select(2, loadfile("\\0"))) + print("B5", select(2, loadfile("/nonexistent-dir-xyz/x", "t\\0b"))) + print("B6", select(2, loadfile("/nonexistent-dir-xyz/x", "x\\0t"))) + + -- 3) io.output / io.input 给的是文件名 + print("C1", pcall(io.output, "/nonexistent-dir-xyz/o\\0ut")) + print("C2", pcall(io.input, "/nonexistent-dir-xyz/in\\0put")) + io.output(io.stdout) + io.input(io.stdin) + print("C3", pcall(io.output, "/nonexistent-dir-xyz/out")) + print("C4", pcall(io.input, "/nonexistent-dir-xyz/input")) + + -- 4) package.loaded 里预先放一个带 \\0 的键:require 查的是被砍过的名字 + package.loaded["a"] = "PRE" + print("D1", pcall(require, "a")) + print("D2", pcall(require, "a\\0b")) + package.loaded["a"] = nil + print("D3", pcall(require, "a\\0b")) + + -- 5) require 一个能被找到的模块:文件名里带 \\0 也照样砍 + print("E1", pcall(package.searchpath, "m", "./?.lua\\0x")) + print("E2", pcall(package.searchpath, "m\\0n", "./?.lua")) + + -- 6) os.date / os.time 的格式与月份名不含 NUL,但 os.date 的 s 是按长度扫的 + print("F1", bytes(os.date("%Y\\0%m\\0%d", 0))) + print("F2", bytes(os.date("!\\0%Y", 0))) + print("F3", bytes(os.date("\\0", 0))) + print("F4", bytes(os.date("%%\\0%%", 0))) + """); + } + + /** + * {@code load} 的读取器矩阵({@code lua_load} 的 reader 契约)。 + * 54 行逐字节一致。 + *

+ * 钉住这几条: + *

    + *
  • 空串不等于结束:{@code Zio} 拿到空串会继续问下一个, + * 只有 {@code nil} 才是收尾。{@code {"", "", src}} 编出来的是 + * {@code src},而 {@code {"", ""}} 编出来的是空 chunk;
  • + *
  • 读取器给的不是字符串也不是数字就当结束({@code true} / 表 / + * 函数),给数字会被当字符串接上(所以「永远返回同一个数字」的 + * 读取器会挂死,本片子里不测);
  • + *
  • 读取器自己抛错要原样传出去,包括已经喂进去一半的情况;
  • + *
  • 读取器里不能 yield:{@code lua_load} 走 + * {@code luaD_callnoyield},报 {@code attempt to yield across a + * C-call boundary};
  • + *
  • {@code load} 不像 {@code loadfile} 那样吃 BOM 与 {@code #!} 行 + * (那属于加载层,不属于词法层);
  • + *
  • 读取器返回的串按长度拼,中间的 {@code \0} 不会被砍。
  • + *
+ *

+ * 写这条片子时踩了一次自己的坑:读取器「永远返回同一个非空串」是死循环 + * —— {@code load(function() return "return 1" end)} 编出来的是 + * {@code "return 1return 1return 1..."},报 + * {@code malformed number}。每个读取器都得自己收尾。 + */ + @Test + void loadReaderMatrixMatchesReference() { + Diff.assertMatches(""" + -- load 的读取器(lua_load 的 reader 契约)矩阵 + -- 注意:读取器「永远返回同一个非空串」是死循环(Zio 只在拿到 nil 或空串时才 + -- 继续问,拿到内容就喂给词法器),所以每个读取器都必须自己收尾。 + local function once(s) + local done = false + return function() + if done then return nil end + done = true + return s + end + end + local function chunks(list) + local i = 1 + return function() + local v = list[i] + i = i + 1 + return v + end + end + local function perbyte(s) + local i = 1 + return function() + if i > #s then return nil end + local c = s:sub(i, i) + i = i + 1 + return c + end + end + local SRC = "return 1 + 2" + + -- 1) 一次给完 / 一个字节一个字节给 + print("A1", load(once(SRC))) + print("A2", (load(once(SRC)))()) + print("A3", (load(perbyte(SRC)))()) + print("A4", (load(perbyte(SRC), "=one"))()) + print("A5", (load(chunks({ "return ", "1 +", " 2" })))()) + print("A6", (load(chunks({ "return ", "", "1 +", "", " 2", "" })))()) + + -- 2) 空串:Zio 碰到空串不结束,继续问下一个 + print("B1", (load(chunks({ "", SRC })))()) + print("B2", (load(chunks({ "", "", "", SRC })))()) + print("B3", (load(chunks({ "", "" })))()) + print("B4", select("#", (load(chunks({ "", "" })))())) + print("B5", pcall(load(chunks({ "", "" })))) + + -- 3) 第一次就 nil:空 chunk + print("C1", select("#", (load(function() return nil end))())) + print("C2", pcall(load(function() return nil end))) + + -- 4) 读取器给的不是字符串也不是数字 + print("D1", pcall(load, function() return true end)) + print("D2", pcall(load, function() return {} end)) + print("D3", pcall(load, function() return print end)) + print("D4", pcall(load, chunks({ SRC, true }))) + print("D5", pcall(load, chunks({ true, SRC }))) + + -- 5) 读取器自己抛错 + print("E1", pcall(load, function() error("READER") end)) + print("E2", pcall(load, chunks({ "return ", "x" }))) + local k = 0 + print("E3", pcall(load, function() + k = k + 1 + if k == 1 then return "return " end + error("MID") + end)) + k = 0 + print("E4", pcall(load, function() + k = k + 1 + if k == 1 then return SRC end + error("AFTER") + end)) + + -- 6) 名字与模式配读取器 + print("F1", pcall(load(once(SRC), "a\\0b"))) + print("F2", select(2, load(once("x="), "=n"))) + print("F3", pcall(load(once(SRC), "=n", "b"))) + print("F4", pcall(load(once(SRC), "=n", "t"))) + print("F5", pcall(load(once(SRC), "=n", "bt"))) + + -- 7) 读取器喂二进制 chunk + local bin = string.dump(function() return 7 end) + print("G1", (load(perbyte(bin)))()) + print("G2", (load(once(bin)))()) + print("G3", pcall(load(once(bin), "=n", "t"))) + print("G4", (load(perbyte(bin), "=n", "b"))()) + print("G5", pcall(load(chunks({ bin:sub(1, 1), bin:sub(2) })))) + + -- 8) env 参数 + local envs = { print = print } + print("H1", (load(once("return print"), "=n", "t", envs))() == print) + print("H2", (load(once("return _ENV.print"), "=n", "t", envs))() == print) + print("H3", pcall(load(once(SRC), "=n", "t", 5))) + print("H4", pcall(load(once(SRC), "=n", "t", nil))) + local h5 = load(once("return 3"), "=n", "t", false) + print("H5", h5 ~= nil and h5() or "nilchunk") + + -- 9) load 的第一个参数不是函数也不是字符串 + print("I1", pcall(load, true)) + print("I2", pcall(load, {})) + print("I3", pcall(load)) + print("I4", pcall(load, 42)) + print("I5", pcall(load, 42, "=n", "t")) + print("I6", (load("return 42") or function() return "x" end)()) + + -- 10) 读取器返回带 \\0 的串:Zio 按长度拼,不砍 + print("J1", (load(chunks({ "return '", "a\\0b", "'" })))():byte(1, -1)) + print("J2", #(load(once("return 'a\\0b'")))()) + print("J3", pcall(load(once("return 1\\0+ 2")))) + + -- 11) 读取器在 load 里不能 yield(lua_load 走 luaD_callnoyield) + print("K1", coroutine.resume(coroutine.create(function() + return pcall(load, function() coroutine.yield(1) return nil end) + end))) + print("K2", coroutine.resume(coroutine.create(function() + local ok, e = pcall(load, function() coroutine.yield(1) return SRC end) + return ok, e + end))) + -- 12) 先给 nil 再给内容:nil 就是结束,后面的不会被问 + print("K3", select("#", (load(chunks({ nil, SRC })))())) + print("K4", pcall(load(chunks({ nil, SRC })))) + -- 13) 读取器返回的串里带换行与 BOM(load 不像 loadfile 那样吃 BOM) + print("K5", pcall(load(once("\\239\\187\\191return 1")))) + print("K6", pcall(load(once("#!/x\\nreturn 1")))) + print("K7", (load(once("\\n\\n return 1 ")))()) + """); + } + + /** + * {@code warn} 与警告系统({@code lauxlib.c} 那三个 warnf*)矩阵。 + *

+ * 这条片子是审计出来的,不是挑出来的:翻 {@code lbaselib.c} 的 + * {@code base_funcs[]} 逐个核对 {@code BaseLib.open} 的注册,才发现 + * {@code warn} 压根没实现 —— 828 个用例全绿也照样缺一整个函数, + * 因为没有任何一个脚本用过它。写矩阵之前先补实现,矩阵负责把它钉住。 + *

+ * 实现时探针咬出来的三处,每一处都是「光看 {@code warn("@on")} 发现不了」的: + *

    + *
  • CONT 状态不查控制消息。{@code warnfcont} 直接写、不调 + * {@code checkcontrol},所以 {@code warn("a", "@on")} 打的是 + * {@code a@on}(最后一个参数是 {@code tocont == 0},看着最像控制消息 + * 的那一次偏偏不被当控制消息)。第一版把控制检查放在状态判断之前, + * 这里就错成「{@code a} 后面又打一次 {@code Lua warning: }」。
  • + *
  • 参数是 C 字符串。{@code warn("@on\\0junk")} 截断后能打开 + * 警告;{@code warn("\0@on")} 截断成空串,就只是打一行空警告。
  • + *
  • 消息按字节写。{@code lua_writestringerror} 不做编码转换, + * {@code warn("\255")} 得原样输出 {@code 0xFF};用 + * {@code print(String)} 会被按 UTF-8 编成 {@code 0xC3 0xBF}。
  • + *
+ */ + @Test + void warnMatrixMatchesReference() { + Diff.assertFullMatches(""" + -- 1) 参数检查:至少要有一个,而且先全部查完才开始拼消息。 + -- 所以下面几条一条警告都不吐(此时警告还是关的,看得更清楚)。 + print("A1", pcall(warn)) + print("A2", pcall(warn, nil)) + print("A3", pcall(warn, true)) + print("A4", pcall(warn, print)) + print("A5", pcall(warn, {})) + print("A6", pcall(warn, "a", nil)) + print("A7", pcall(warn, "a", {})) + print("A8", pcall(warn, "a", 1, nil)) + print("A9", pcall(warn, coroutine.create(function() end))) + -- 数字是允许的:luaL_checkstring 底下是 lua_tolstring + print("A10", pcall(warn, 1)) + print("A11", pcall(warn, 1, 2)) + print("A12", pcall(warn, "a", 1, "b")) + -- __tostring 不参与:不是 luaL_tolstring + local ts = setmetatable({}, { __tostring = function() return "TS" end }) + print("A13", pcall(warn, ts)) + print("A14", pcall(warn, "a", ts)) + -- __name 参与报错文本 + print("A15", pcall(warn, setmetatable({}, { __name = "MyType" }))) + print("A16", pcall(warn, "ok", setmetatable({}, { __name = "MyType" }))) + + -- 2) 默认关闭:luaL_newstate 装的是 warnfoff,不开就什么都不打 + warn("SHOULD NOT APPEAR") + warn("@on", "SHOULD NOT APPEAR EITHER") + print("B1", "defaults off") + + -- 3) 打开之后:多参数拼成一行,最后一个参数收尾换行 + warn("@on") + warn("hello") + warn("a", "b", "c") + warn("1", "2", "3", "4", "5", "6", "7", "8", "9", "10") + warn("") + warn("", "", "") + print("C1", "composed") + + -- 4) 数字的形状:走的是 luaO_tostring,不是 tostring 的 __tostring + warn(1, 2, 3) + warn(0.1) + warn(1 / 0) + warn(0 / 0) + warn(math.mininteger) + warn(math.maxinteger) + warn(1e300) + warn(-0.0) + print("D1", "numbers") + + -- 5) 消息是 C 字符串:\\0 处截断 + warn("a\\0b") + warn("x", "y\\0z") + warn("\\0@on") -- 截断成空串,不是控制消息 + warn("@" .. "on" .. "\\0" .. "junk") -- 截断后是 "@on",能打开 + warn("e") + print("E1", "nul") + + -- 6) 控制消息:只在 tocont == 0 时认,且只在 OFF / ON 两个状态认 + warn("@on", "second") -- 第一个参数 tocont=1 → "@on" 被原样打印 + warn("visible") + warn("@OFF") -- 大小写敏感,未知控制消息被吃掉 + warn("still on?") + warn("@") -- 只有 @,同样被吃掉 + warn("after lone at") -- 状态没被上面那两个动过,照打 + print("F1", "controls") + + -- 7) CONT 状态不查控制消息:这几个 @ 会被原样打出来 + warn("a", "@on") + warn("b") + warn("c", "@off") + warn("d") + print("G1", "cont state") + + -- 8) 字节原样:不做 UTF-8 编码 + warn("\\255") + warn("pre\\255post") + warn("\\255", "\\0", "\\1") + print("H1", "raw bytes") + + -- 9) 警告状态是全局的:协程里关掉,主线程也跟着关 + warn("@on") + warn("before co") + local co = coroutine.create(function() warn("@off") end) + coroutine.resume(co) + warn("after co") -- 不该出现 + warn("@on") + warn("reopened") + warn("@off") + print("I1", "shared across threads") + """); + } + + /** + * 库函数的返回元数矩阵:把每个库函数零参调一遍,比对 + * 「成功还是失败 + 每个返回值的类型 + 出错时的消息」。 + *

+ * 挑零参不是偷懒:零参是唯一一个能对所有 ~130 个库函数统一施加的输入, + * 于是「返回几个值」这件事可以被一次性铺开。而返回元数恰恰是最容易写错的 + * 一类 —— {@code f:write("")} 那次就是红在这里(参考给 1 个值、我们给 3 个), + * 而且只有「只读文件 × 空串」那一格能逼出来。 + *

+ * 三条必须遵守的排除/归一化,每一条都是踩过的: + *

    + *
  • {@code os.exit} 会真的杀掉进程(参考实现那边的进程)。
  • + *
  • {@code io.*}、{@code dofile}、{@code loadfile} 会动默认流或读 + * stdin —— 参考实现的 stdin 与我们的不一样。
  • + *
  • {@code debug.debug()} 会进交互式提示符({@code lua_debug>}), + * 从 stdin 读命令。
  • + *
  • {@code math.atan} 与 {@code math.atan2} 是同一个 C 函数 + * ({@code lmathlib.c} 里 {@code {"atan", math_atan}} 与 + * {@code {"atan2", math_atan}}),报错里报哪个名字取决于 + * {@code findfield} 用 {@code lua_next} 先撞见谁,而字符串哈希种子是 + * 每次进程随机的。实测参考实现连跑六次:三次 {@code atan}、 + * 三次 {@code atan2}。它自己都不确定,所以统一成 {@code atan} 再比。
  • + *
+ *

+ * 只打印类型不打印值:{@code os.time()} / {@code os.date()} / + * {@code math.random()} 的值每次都不同,打了就是一条永远红的用例。 + */ + @Test + void libraryArityMatchesReference() { + Diff.assertFullMatches(""" + local out = {} + local function probe(name, f) + local r = table.pack(pcall(f)) + local s = name + for i = 1, r.n do s = s .. " " .. type(r[i]) end + if not r[1] then + s = s .. " | " .. tostring(r[2]):gsub("'math%.atan2'", "'math.atan'") + end + out[#out + 1] = s + end + + local function sweep(libname, t, skip) + local names = {} + for k in pairs(t) do + if type(t[k]) == "function" and not (skip and skip[k]) then + names[#names + 1] = k + end + end + table.sort(names) + for _, k in ipairs(names) do probe(libname .. "." .. k, t[k]) end + end + + sweep("string", string) + sweep("table", table) + sweep("math", math) + sweep("utf8", utf8) + sweep("coroutine", coroutine) + -- debug.debug 会进交互式提示符并读 stdin + sweep("debug", debug, { debug = true }) + -- os.exit 会杀进程;os.clock 的值不可比 + sweep("os", os, { exit = true, clock = true }) + + for _, g in ipairs({ "assert", "collectgarbage", "error", + "getmetatable", "ipairs", "load", "next", "pairs", "pcall", + "print", "rawequal", "rawget", "rawlen", "rawset", "require", + "select", "setmetatable", "tonumber", "tostring", "type", + "warn", "xpcall" }) do + probe(g, _G[g]) + end + + print(table.concat(out, "\\n")) + """); + } + + /** + * 与 {@link #libraryArityMatchesReference} 同一思路的另一半:每个库函数各喂 + * 一个错类型的实参({@code {}} / {@code nil} / {@code 1}),比对 + * 「成功还是失败 + 返回值的类型 + 出错时的消息」。 + *

+ * 零参那条铺的是「参数缺失」这条路,这条铺的是「类型不对」那条路 —— + * 两者在 {@code lauxlib.c} 里是两个不同的函数 + * ({@code luaL_check*} 与 {@code luaL_opt*} 对「缺失」的处理不一样), + * 只铺一条会漏掉另一条的报错文本。 + *

+ * 这一片铺下来是干净的(359 行 × 3 类参数,逐字节一致)。留着的理由是 + * 它把 ~370 个调用形状一次性钉住:以后改任何一个库的报错文本、返回值个数, + * 或者少一个参数校验,这里立刻红 —— 而这类改动在手写矩阵里往往是没人碰的角落。 + */ + @Test + void libraryWrongArgMatchesReference() { + Diff.assertFullMatches(""" + local out = {} + local function probe(name, f, ...) + local r = table.pack(pcall(f, ...)) + local s = name + for i = 1, r.n do s = s .. " " .. type(r[i]) end + if not r[1] then + s = s .. " | " .. tostring(r[2]):gsub("'math%.atan2'", "'math.atan'") + end + out[#out + 1] = s + end + + local function sweep(lib, t, skip, tag, arg) + local names = {} + for k in pairs(t) do + if type(t[k]) == "function" and not (skip and skip[k]) then + names[#names + 1] = k + end + end + table.sort(names) + for _, k in ipairs(names) do + probe(lib .. "." .. k .. "/" .. tag, t[k], arg) + end + end + + local G = { "assert", "collectgarbage", "error", "getmetatable", + "ipairs", "load", "next", "pairs", "pcall", "print", + "rawequal", "rawget", "rawlen", "rawset", "require", + "select", "setmetatable", "tonumber", "tostring", "type", + "warn", "xpcall" } + + local ARGS = { { {}, "table" }, { nil, "nil" }, { 1, "num" } } + for _, p in ipairs(ARGS) do + local a, tag = p[1], p[2] + sweep("string", string, nil, tag, a) + sweep("table", table, nil, tag, a) + sweep("math", math, nil, tag, a) + sweep("utf8", utf8, nil, tag, a) + sweep("coroutine", coroutine, nil, tag, a) + sweep("debug", debug, { debug = true }, tag, a) + sweep("os", os, { exit = true, clock = true }, tag, a) + for _, g in ipairs(G) do probe(g .. "/" .. tag, _G[g], a) end + end + + print(table.concat(out, "\\n")) + """); + } + + /** + * 整数 / 浮点子类型网格。 + *

+ * 5.4 里「数字」分成 integer 与 float 两个子类型,{@code math.type} 看得见 + * 差别 —— 而这是移植 5.4 最容易错的一类:写成「一律 double」的脚本大多能跑, + * 只是 {@code //}、{@code %}、{@code string.format("%d")}、 + * {@code math.tointeger} 这些地方悄悄不同。所以把标准库里所有会返回数字 + * 的门口过一遍,只比子类型(不比值:{@code os.time} / {@code math.random} + * 的值每次都不同)。 + *

+ * 铺两格:常用形状(数学库「整型进整型出、浮点进浮点出」、{@code tonumber} 的 + * 四种写法、位运算、{@code string.unpack} 各宽度、{@code io} 的 seek、 + * {@code os.date} 各字段),以及边界({@code maxinteger + 1}、 + * {@code mininteger // -1}、{@code math.fmod(1, 0)} 与 {@code 1 // 0} 是 + * 抛错而不是给 nan、{@code math.floor(-0.0)} 给整数、 + * {@code math.tointeger(2^63)} 给 nil)。 + *

+ * 每条都套在 {@code pcall} 里 —— 上面那两个抛错的就是这么发现的:不套的话 + * 脚本在第一条就死了,后面几十行一条也不会比,而用例照样是绿的。 + */ + @Test + void numberSubtypeMatchesReference() { + Diff.assertFullMatches(""" + local out = {} + local function n(tag, f, ...) + local ok, v = pcall(f, ...) + out[#out + 1] = tag .. " " .. tostring(ok) .. " " .. tostring(math.type(v)) + end + local function s(tag, v) out[#out + 1] = tag .. " " .. tostring(math.type(v)) end + -- 数学库:整型进整型出,浮点进浮点出 + n("floor3.7", math.floor, 3.7) n("floor3", math.floor, 3) + n("floor_3.7", math.floor, -3.7) n("floorstr", math.floor, "3.7") + n("ceil3.2", math.ceil, 3.2) n("ceil3", math.ceil, 3) + n("abs_i", math.abs, -3) n("abs_f", math.abs, -3.5) + n("abs_h", math.abs, -math.maxinteger - 1) + n("maxii", math.max, 1, 2) n("maxif", math.max, 1, 2.5) + n("maxff", math.max, 1.0, 2.0) n("minii", math.min, 1, 2) + n("minfi", math.min, 1.5, 2) + n("fmodii", math.fmod, 7, 3) n("fmodif", math.fmod, 7.5, 2) + n("fmodfi", math.fmod, 7, 2.5) + n("sqrt4", math.sqrt, 4) n("sin1", math.sin, 1) + n("exp1", math.exp, 1) n("log8_2", math.log, 8, 2) + n("log8", math.log, 8) n("pow", math.pow, 2, 3) + n("toint3.0", math.tointeger, 3.0) n("toint3.5", math.tointeger, 3.5) + n("toint_i", math.tointeger, 3) n("toint_s", math.tointeger, "4") + n("ult", math.ult, 1, 2) n("rand", math.random, 1, 10) + n("rand0", math.random, 0) n("deg", math.deg, 90) + n("ldexp", math.ldexp, 3, 2) n("cosh", math.cosh, 1) + s("huge", math.huge) s("maxint", math.maxinteger) + s("minint", math.mininteger) s("pi", math.pi) + -- 运算符 + s("idiv_ii", 10 // 3) s("idiv_fi", 10.0 // 3) + s("idiv_if", 10 // 3.0) s("mod_ii", 10 % 3) + s("mod_fi", 10.5 % 3) s("pow_ii", 2 ^ 2) + s("pow_if", 2 ^ 2.0) s("div", 6 / 3) + s("str_arith", "10" + 1) s("str_arith2", "10.5" + 1) + s("neg_i", -3) s("neg_f", -3.0) + -- 字符串 + n("len", string.len, "abc") n("byte", string.byte, "A") + s("hash", #("abc")) n("rep_n", function() return ("ab"):rep(2) end) + n("find", string.find, "abc", "b") + n("find2", string.find, "abc", "z") + local ok, a, b = pcall(string.find, "abc", "b") + out[#out + 1] = "findpair " .. tostring(ok) .. " " .. tostring(math.type(a)) + .. " " .. tostring(math.type(b)) + n("gsub", string.gsub, "abc", "b", "X") + local ok2, g1, g2 = pcall(string.gsub, "abc", "b", "X") + out[#out + 1] = "gsubpair " .. tostring(ok2) .. " " .. tostring(math.type(g2)) + -- tonumber + n("tonum1", tonumber, "1") n("tonum1.0", tonumber, "1.0") + n("tonum1e2", tonumber, "1e2") n("tonum0x10", tonumber, "0x10") + n("tonum_sp", tonumber, " 1 ") n("tonum_hex", tonumber, "ff", 16) + n("tonum_d5", tonumber, "1.5", 10) + n("tonum_i", tonumber, 3) n("tonum_f", tonumber, 3.5) + -- utf8 + n("u8len", utf8.len, "abc") n("u8cp", utf8.codepoint, "A") + n("u8off", utf8.offset, "abc", 2) + -- os + n("ostime", os.time) n("osdifftime", os.difftime, 10, 3) + n("osclock", os.clock) + s("osdate_year", os.date("!*t", 0).year) + s("osdate_hour", os.date("!*t", 0).hour) + -- string.pack / unpack + local pk = string.pack("i4", 7) + n("unpack_i4", string.unpack, "i4", pk) + n("unpack_I4", string.unpack, "I4", pk) + n("unpack_f", string.unpack, "n", string.pack("n", 1.5)) + n("packsize", string.packsize, "i4") + -- io + local p = "/tmp/it_tmp.txt" + os.remove(p) + local w = io.open(p, "w"); w:write("abcd"); w:close() + local f = io.open(p) + n("seek_end", f.seek, f, "end") + f:close(); os.remove(p) + -- debug / 其他 + s("getinfo_line", debug.getinfo(1).currentline) + s("select_n", select("#", 1, 2, 3)) + s("pack_n", table.pack(1, 2).n) + s("tostring_i", tonumber(tostring(3))) + out[#out + 1] = "fmt_d_i " .. string.format("%d", 3) + out[#out + 1] = "fmt_d_f " .. string.format("%d", 3.0) + out[#out + 1] = "fmt_g " .. string.format("%.14g", 3) + out[#out + 1] = "tos_i " .. tostring(3) + out[#out + 1] = "tos_f " .. tostring(3.0) + out[#out + 1] = "tos_h " .. tostring(math.huge) + local function s2(tag, f) + local ok, v = pcall(f) + out[#out + 1] = tag .. " " .. tostring(ok) .. " " .. tostring(math.type(v)) + end + -- 整数边界 + s2("maxint1", function() return math.maxinteger + 1 end) + s2("minint1", function() return math.mininteger - 1 end) + s2("abs_minint", function() return math.abs(math.mininteger) end) + s2("floor_2_53", function() return math.floor(2 ^ 53) end) + s2("floor_n0", function() return math.floor(-0.0) end) + s2("floor_0", function() return math.floor(0.0) end) + s2("floor_nan", function() return math.floor(0 / 0) end) + s2("floor_inf", function() return math.floor(1 / 0) end) + s2("ceil_inf", function() return math.ceil(-1 / 0) end) + s2("fmod_0", function() return math.fmod(1, 0) end) + s2("fmod_hh", function() return math.fmod(math.huge, 2) end) + s2("toint_2_63", function() return math.tointeger(2 ^ 63) end) + s2("toint_huge", function() return math.tointeger(math.huge) end) + s2("toint_n0", function() return math.tointeger(-0.0) end) + s2("toint_max", function() return math.tointeger(3.0) end) + s2("toint_bool", function() return math.tointeger(true) end) + s2("sqrt_minint", function() return math.sqrt(math.mininteger) end) + s2("pow_0_0", function() return 0 ^ 0 end) + s2("pow_neg", function() return (-8) ^ (1 / 3) end) + -- 整除与取模 + s2("idiv_by0i", function() return 1 // 0 end) + s2("idiv_by0f", function() return 1.0 // 0 end) + s2("mod_by0i", function() return 1 % 0 end) + s2("mod_by0f", function() return 1.0 % 0 end) + s2("idiv_min_1", function() return math.mininteger // -1 end) + s2("mod_min_1", function() return math.mininteger % -1 end) + s2("idiv_neg", function() return -7 // 2 end) + s2("mod_neg", function() return -7 % 2 end) + s2("idiv_fneg", function() return -7.0 // 2 end) + s2("mod_fneg", function() return -7.0 % 2 end) + -- 字符串转数字参与运算 + s2("str_idiv", function() return "10" // "3" end) + s2("str_mod", function() return "10" % "3" end) + s2("str_max", function() return math.max("1", 2) end) + s2("str_abs", function() return math.abs2("-3.5") end) + s2("str_concat", function() return 1 .. 2 end) + s2("str_concat2", function() return 1.0 .. "" end) + -- random 的形状 + s2("rand_none", function() return math.random() end) + s2("rand_m", function() return math.random(0) end) + s2("rand_nm", function() return math.random(3, 3) end) + s2("rand_max", function() return math.random(math.maxinteger) end) + -- randomseed 返回两个整数 + s2("rseed1", function() return (math.randomseed(42, 7)) end) + s2("rseed2", function() return select(2, math.randomseed(42, 7)) end) + -- 多返回值里的数字 + s2("byte3_1", function() return string.byte("ABC", 1, 3) end) + s2("byte3_2", function() return select(2, string.byte("ABC", 1, 3)) end) + s2("cp3_1", function() return utf8.codepoint("ABC", 1, 3) end) + s2("cp3_2", function() return select(2, utf8.codepoint("ABC", 1, 3)) end) + -- os + s2("ostime_t", function() return os.time({ year = 2000, month = 1, day = 1, hour = 12 }) end) + s2("date_year", function() return os.date("!*t", 0).year end) + s2("date_yday", function() return os.date("!*t", 0).yday end) + s2("date_isdst", function() return os.date("!*t", 0).isdst end) + -- 位运算 + s2("band", function() return 7 & 3 end) + s2("bor", function() return 7 | 8 end) + s2("bxor", function() return 7 ~ 3 end) + s2("bnot", function() return ~0 end) + s2("shl", function() return 1 << 62 end) + s2("shr", function() return -1 >> 1 end) + -- string.unpack 的各整数宽度 + s2("up_i1", function() return string.unpack("i1", string.pack("i1", 1)) end) + s2("up_i2", function() return string.unpack("i2", string.pack("i2", 1)) end) + s2("up_i4", function() return string.unpack("i4", string.pack("i4", 1)) end) + s2("up_i8", function() return string.unpack("i8", string.pack("i8", 1)) end) + s2("up_I8", function() return string.unpack("I8", string.pack("I8", 1)) end) + s2("up_i", function() return string.unpack("i", string.pack("i", 1)) end) + s2("up_j", function() return string.unpack("j", string.pack("j", 1)) end) + s2("up_f", function() return string.unpack("f", string.pack("f", 1.5)) end) + s2("up_d", function() return string.unpack("d", string.pack("d", 1.5)) end) + s2("up_b", function() return string.unpack("b", string.pack("b", 65)) end) + s2("up_sz", function() return select(2, string.unpack("s", string.pack("s", "hi"))) end) + print(table.concat(out, "\n")) +"""); + } + + /** + * 全局符号审计:本实现与参考实现暴露的符号集必须一模一样。 + *

+ * 存在的理由就是 {@code warn} 那次教训 —— 一整个函数没实现,828 个用例 + * 全绿,因为没有任何脚本用过它。行为矩阵再密也只能在「已经写到的函数」里 + * 找分歧,漏掉的函数一个也发现不了;符号集对撞补的正是这一格: + * 以后再少一个库函数、少一个常量,这里立刻红。 + *

+ * 不比对值,只比对「名字 + 类型」:值里有地址({@code function: 0x...}), + * 归一化抹掉之后什么也不剩,比了等于没比,还会让用例变成空用例。 + *

+ * 文件句柄那一格是后来补的:第一版只查 {@code io} 表,于是七个文件方法 + * 整片漏掉({@code close} / {@code flush} / {@code lines} / {@code read} / + * {@code seek} / {@code setvbuf} / {@code write})—— 它们住在 + * {@code getmetatable(io.stdout).__index} 里,不在 {@code io} 表上。 + * 符号审计必须顺着 {@code __index} 往下挖才有用。 + */ + @Test + void globalSymbolsMatchReference() { + Diff.assertFullMatches(""" + local function dump(t, out) + local ks = {} + for k, v in pairs(t) do + ks[#ks + 1] = tostring(k) .. " " .. type(v) + end + table.sort(ks) + for _, s in ipairs(ks) do out[#out + 1] = s end + end + + local out = {} + out[#out + 1] = "== _G" + dump(_G, out) + for _, n in ipairs({ "coroutine", "debug", "io", "math", "os", + "package", "string", "table", "utf8" }) do + out[#out + 1] = "== " .. n + local t = _G[n] + if type(t) == "table" then + dump(t, out) + else + out[#out + 1] = "MISSING " .. type(t) + end + end + + -- 字符串元表:("x"):upper() 与 "10" + 1 都走它 + out[#out + 1] = "== string metatable" + local smt = getmetatable("") + if smt == nil then + out[#out + 1] = "NONE" + else + dump(smt, out) + end + + -- 文件句柄的元表,以及元表里的 __index —— 文件方法住在那里, + -- 不在 io 表里(close/flush/lines/read/seek/setvbuf/write 七个)。 + -- 只查 io 表的键会整片漏掉它们。 + out[#out + 1] = "== file metatable" + local fmt = getmetatable(io.stdout) + if fmt == nil then + out[#out + 1] = "NONE" + else + dump(fmt, out) + out[#out + 1] = "== file methods" + if type(fmt.__index) == "table" then + dump(fmt.__index, out) + else + out[#out + 1] = "NO __index " .. type(fmt.__index) + end + end + + -- 每个库都该在 package.loaded 里占一格 + out[#out + 1] = "== package.loaded" + dump(package.loaded, out) + + print(table.concat(out, "\\n")) + """); + } + + /** + * 成功路径矩阵:string 与 math 两个库:每个函数喂一组正常的实参,比的是返回值本身(不是类型)。 + *

+ * 前面那两片({@link #libraryArityMatchesReference} 与 + * {@link #libraryWrongArgMatchesReference})铺的是「函数在不在」与 + * 「喂错参数怎么报错」,这一片铺的是第三件事:喂对参数时返回值对不对。 + * 三件事覆盖的代码路径几乎不重叠 —— 报错走的是 {@code luaL_check*} / + * {@code luaL_opt*} 的检查分支,成功走的是后面的计算本体,只铺前两片的话 + * 一个算错的 {@code string.rep} 会一直绿。 + *

+ * 返回值用 {@code d()} 描述:数字带 {@code math.type} 子类型、字符串带长度、 + * 表递归展开两层并按键排序(避开 {@code pairs} 顺序与地址)。少数几处 + * 只比类型:{@code os.time} / {@code os.clock} / + * {@code collectgarbage("count")} 的值本来就随实现变,表的 {@code tostring} + * 是地址(已由 {@code Diff} 的 {@code 0xADDR} 归一化接住)。 + *

+ * 敏感性是量过的,不是假设的:把 {@code string.upper} 改坏一格 + * ({@code 'a'..'z'} → {@code 'B'..'Z'}),这一片立刻红在 + * {@code s.upper [2] q(3)[Abc]} —— 见 README「成功路径矩阵」一节。 + */ + @Test + void stringMathSuccessMatchesReference() { + Diff.assertFullMatches(""" + -- 成功路径矩阵:每个库函数喂一组「正常实参」,比返回值本身 + local out = {} + local function d(v, depth) + depth = depth or 0 + local t = type(v) + if t == "nil" then return "nil" end + if t == "boolean" then return tostring(v) end + if t == "number" then return math.type(v) .. ":" .. tostring(v) end + if t == "string" then return string.format("q(%d)[%s]", #v, v) end + if t == "function" then return "function" end + if t == "thread" then return "thread:" .. tostring(coroutine.status(v)) end + if t == "table" then + if depth >= 2 then return "table..." end + local keys, ks = {}, {} + for k in pairs(v) do keys[#keys + 1] = k end + table.sort(keys, function(a, b) + local ta, tb = type(a), type(b) + if ta ~= tb then return ta < tb end + if ta == "number" or ta == "string" then return a < b end + return false + end) + for _, k in ipairs(keys) do + ks[#ks + 1] = (type(k) == "table" and "table" or d(k, 9)) + .. "=" .. d(v[k], depth + 1) + end + return "{" .. table.concat(ks, ",") .. "}" + end + return t + end + local function probe(name, f, ...) + local r = table.pack(pcall(f, ...)) + local s = name .. " [" .. r.n .. "]" + for i = 2, r.n do s = s .. " " .. d(r[i]) end + if not r[1] then s = s .. " ERR " .. tostring(r[2]) end + out[#out + 1] = s + end + local function say(s) out[#out + 1] = s end + + -- string + probe("s.byte", string.byte, "ABC", 2) + probe("s.byte0", string.byte, "ABC", 2, 3) + probe("s.char", string.char, 65, 66, 67) + probe("s.find", string.find, "hello world", "o w") + probe("s.findinit", string.find, "hello", "l", 4) + probe("s.findplain", string.find, "a.c", ".", 1, true) + probe("s.findcap", string.find, "hello", "(l)(l)") + probe("s.format_d", string.format, "%d", 42) + probe("s.format_5d", string.format, "%5.2f", 3.14159) + probe("s.format_s", string.format, "%s-%s", "a", "b") + probe("s.format_q", string.format, "%q", 'a"b\\n') + probe("s.format_x", string.format, "%x %X", 255, 255) + probe("s.format_pct", string.format, "100%%") + probe("s.format_c", string.format, "%c", 65) + probe("s.format_e", string.format, "%.3e", 1234.5) + probe("s.format_g", string.format, "%g", 0.0001) + probe("s.format_a", string.format, "%a", 1.0) + probe("s.format_i", string.format, "%i", -7) + probe("s.gmatch", function() + local r = {} + for w in ("a,b,c"):gmatch("%a") do r[#r + 1] = w end + return table.concat(r, "|") + end) + probe("s.gsub", string.gsub, "hello world", "o", "0") + probe("s.gsubn", string.gsub, "hello", "l", "L", 1) + probe("s.gsubfn", string.gsub, "a1b2", "%d", function(x) return x .. x end) + probe("s.gsubtbl", string.gsub, "abc", "%a", { a = "X" }) + probe("s.len", string.len, "abc") + probe("s.len0", string.len, "") + probe("s.lower", string.lower, "AbC") + probe("s.upper", string.upper, "AbC") + probe("s.rep", string.rep, "ab", 3) + probe("s.rep0", string.rep, "ab", 0) + probe("s.repsep", string.rep, "ab", 2, "-") + probe("s.reverse", string.reverse, "abc") + probe("s.sub", string.sub, "hello", 2, 4) + probe("s.subneg", string.sub, "hello", -3) + probe("s.subnoend", string.sub, "hello", 2) + probe("s.pack", function() return (string.pack("i4i2", 7, 8):gsub(".", function(c) return string.format("%02x", string.byte(c)) end)) end) + probe("s.packsize", string.packsize, "i4i2") + probe("s.unpack", string.unpack, "i4i2", string.pack("i4i2", 7, 8)) + + -- math + probe("m.abs_i", math.abs, -3) + probe("m.abs_f", math.abs, -3.5) + probe("m.ceil", math.ceil, 3.2) + probe("m.ceilneg", math.ceil, -3.2) + probe("m.floor", math.floor, 3.7) + probe("m.fmod", math.fmod, 7, 3) + probe("m.fmodf", math.fmod, 7.5, 2) + probe("m.max", math.max, 1, 5, 3) + probe("m.maxf", math.max, 1.5, 2) + probe("m.min", math.min, 1, 5, 3) + probe("m.modf", math.modf, 3.75) + probe("m.sqrt", math.sqrt, 16) + probe("m.sqrt2", math.sqrt, 2) + probe("m.toint", math.tointeger, 3.0) + probe("m.tointf", math.tointeger, 3.5) + probe("m.type", math.type, 3.0) + probe("m.ult", math.ult, 1, 2) + probe("m.exp", math.exp, 1) + probe("m.log", math.log, 8, 2) + probe("m.log10", math.log, 100, 10) + probe("m.sin", math.sin, 0) + probe("m.cos", math.cos, 0) + probe("m.tan", math.tan, 1) + probe("m.asin", math.asin, 0.5) + probe("m.acos", math.acos, 0.5) + probe("m.atan1", math.atan, 1) + probe("m.atan2", math.atan, 1, 2) + math.randomseed(42) + probe("m.random", math.random) + probe("m.random_n", math.random, 100) + probe("m.random_r", math.random, 10, 20) + + print(table.concat(out, "\\n")) + """); + } + + /** + * 成功路径矩阵:table / utf8 / os / coroutine。 + *

+ * 前面那两片({@link #libraryArityMatchesReference} 与 + * {@link #libraryWrongArgMatchesReference})铺的是「函数在不在」与 + * 「喂错参数怎么报错」,这一片铺的是第三件事:喂对参数时返回值对不对。 + * 三件事覆盖的代码路径几乎不重叠 —— 报错走的是 {@code luaL_check*} / + * {@code luaL_opt*} 的检查分支,成功走的是后面的计算本体,只铺前两片的话 + * 一个算错的 {@code string.rep} 会一直绿。 + *

+ * 返回值用 {@code d()} 描述:数字带 {@code math.type} 子类型、字符串带长度、 + * 表递归展开两层并按键排序(避开 {@code pairs} 顺序与地址)。少数几处 + * 只比类型:{@code os.time} / {@code os.clock} / + * {@code collectgarbage("count")} 的值本来就随实现变,表的 {@code tostring} + * 是地址(已由 {@code Diff} 的 {@code 0xADDR} 归一化接住)。 + *

+ * 敏感性是量过的,不是假设的:把 {@code string.upper} 改坏一格 + * ({@code 'a'..'z'} → {@code 'B'..'Z'}),这一片立刻红在 + * {@code s.upper [2] q(3)[Abc]} —— 见 README「成功路径矩阵」一节。 + */ + @Test + void tableOsCoroutineSuccessMatchesReference() { + Diff.assertFullMatches(""" + -- 成功路径矩阵第二片:table / utf8 / os / coroutine / io / debug / 全局 + local out = {} + local function d(v, depth) + depth = depth or 0 + local t = type(v) + if t == "nil" then return "nil" end + if t == "boolean" then return tostring(v) end + if t == "number" then return math.type(v) .. ":" .. tostring(v) end + if t == "string" then return string.format("q(%d)[%s]", #v, v) end + if t == "function" then return "function" end + if t == "thread" then return "thread:" .. tostring(coroutine.status(v)) end + if t == "table" then + if depth >= 2 then return "table..." end + local keys, ks = {}, {} + for k in pairs(v) do keys[#keys + 1] = k end + table.sort(keys, function(a, b) + local ta, tb = type(a), type(b) + if ta ~= tb then return ta < tb end + if ta == "number" or ta == "string" then return a < b end + return false + end) + for _, k in ipairs(keys) do + ks[#ks + 1] = (type(k) == "table" and "table" or d(k, 9)) + .. "=" .. d(v[k], depth + 1) + end + return "{" .. table.concat(ks, ",") .. "}" + end + return t + end + local function probe(name, f, ...) + local r = table.pack(pcall(f, ...)) + local s = name .. " [" .. r.n .. "]" + for i = 2, r.n do s = s .. " " .. d(r[i]) end + if not r[1] then s = s .. " ERR " .. tostring(r[2]) end + out[#out + 1] = s + end + local function say(s) out[#out + 1] = s end + + -- table + local t = { 10, 20, 30 } + probe("t.concat", table.concat, t) + probe("t.concatsep", table.concat, t, "-") + probe("t.conatrange", table.concat, t, ",", 2, 3) + probe("t.insert", function() local a = { 1, 2 }; table.insert(a, 3); return a end) + probe("t.insertpos", function() local a = { 1, 3 }; table.insert(a, 2, 2); return a end) + probe("t.move", function() local a = { 1, 2, 3, 4 }; return table.move(a, 2, 3, 1) end) + probe("t.movedst", function() local a = { 1, 2 }; local b = { 9, 9, 9 }; return table.move(a, 1, 2, 2, b) end) + probe("t.pack", table.pack, 1, nil, 3) + probe("t.remove", function() local a = { 1, 2, 3 }; return table.remove(a, 2) end) + probe("t.removelast", function() local a = { 1, 2 }; return table.remove(a) end) + probe("t.sort", function() local a = { 3, 1, 2 }; table.sort(a); return a end) + probe("t.sortcmp", function() local a = { 3, 1, 2 }; table.sort(a, function(x, y) return x > y end); return a end) + probe("t.unpack", table.unpack, { 1, 2, 3 }) + probe("t.unpackr", table.unpack, { 1, 2, 3 }, 2, 3) + + -- utf8 + probe("u.char", utf8.char, 0x41, 0x4E2D) + probe("u.codepoint", utf8.codepoint, "中", 1) + probe("u.codepoint2", utf8.codepoint, "中文", 1, 2) + probe("u.len", utf8.len, "中文a") + probe("u.lenrange", utf8.len, "中文", 2) + probe("u.offset", utf8.offset, "中文", 2) + probe("u.offsetneg", utf8.offset, "中文", -1) + probe("u.offsetn", utf8.offset, "中文", 1, 2) + say("u.pattern " .. utf8.charpattern) + + -- os + probe("o.date_s", os.date, "!%Y-%m-%d %H:%M:%S", 0) + probe("o.date_t", function() local x = os.date("!*t", 0); x.isdst = nil; return x end) + probe("o.date_now", function() return type(os.date("!%Y")) end) + probe("o.difftime", os.difftime, 100, 50) + probe("o.time_tbl", os.time, { year = 2000, month = 1, day = 1, hour = 12 }) + probe("o.time_now", function() return math.type(os.time()) end) + probe("o.getenv", function() return type(os.getenv("HOME")) end) + probe("o.getenv_miss", os.getenv, "NO_SUCH_ENV_VAR_XYZ_9") + probe("o.setlocale", function() return type(os.setlocale()) end) + probe("o.setlocale_c", function() return type(os.setlocale("C")) end) + probe("o.remove", function() + local p = "/tmp/fkok_rm.txt" + local f = io.open(p, "w"); f:write("x"); f:close() + return os.remove(p), io.open(p) == nil or "still" + end) + probe("o.rename", function() + local a, b = "/tmp/fkok_a.txt", "/tmp/fkok_b.txt" + os.remove(a); os.remove(b) + local f = io.open(a, "w"); f:write("y"); f:close() + os.rename(a, b) + local g = io.open(b) + local r = g and g:read("a") or "none" + if g then g:close() end + os.remove(a); os.remove(b) + return r + end) + probe("o.remove_missing", os.remove, "/tmp/fkok_no_such_file_xyz") + + -- coroutine + probe("c.create", function() return type(coroutine.create(function() end)) end) + probe("c.resume", function() local co = coroutine.create(function(a, b) return a + b end); return coroutine.resume(co, 1, 2) end) + probe("c.status", function() local co = coroutine.create(function() end); return coroutine.status(co), (coroutine.resume(co)), coroutine.status(co) end) + probe("c.wrap", function() local f = coroutine.wrap(function() coroutine.yield(1); return 2 end); return f(), f() end) + probe("c.close", function() local co = coroutine.create(function() coroutine.yield() end); coroutine.resume(co); return coroutine.close(co) end) + probe("c.isyieldable", function() return coroutine.isyieldable(), (coroutine.wrap(function() return coroutine.isyieldable() end))() end) + probe("c.running", function() local a, b = coroutine.running(); return type(a), b end) + probe("c.wraperr", function() local f = coroutine.wrap(function() error("boom") end); return f() end) + + print(table.concat(out, "\\n")) + """); + } + + /** + * 成功路径矩阵:io / debug / 全局函数。 + *

+ * 前面那两片({@link #libraryArityMatchesReference} 与 + * {@link #libraryWrongArgMatchesReference})铺的是「函数在不在」与 + * 「喂错参数怎么报错」,这一片铺的是第三件事:喂对参数时返回值对不对。 + * 三件事覆盖的代码路径几乎不重叠 —— 报错走的是 {@code luaL_check*} / + * {@code luaL_opt*} 的检查分支,成功走的是后面的计算本体,只铺前两片的话 + * 一个算错的 {@code string.rep} 会一直绿。 + *

+ * 返回值用 {@code d()} 描述:数字带 {@code math.type} 子类型、字符串带长度、 + * 表递归展开两层并按键排序(避开 {@code pairs} 顺序与地址)。少数几处 + * 只比类型:{@code os.time} / {@code os.clock} / + * {@code collectgarbage("count")} 的值本来就随实现变,表的 {@code tostring} + * 是地址(已由 {@code Diff} 的 {@code 0xADDR} 归一化接住)。 + *

+ * 敏感性是量过的,不是假设的:把 {@code string.upper} 改坏一格 + * ({@code 'a'..'z'} → {@code 'B'..'Z'}),这一片立刻红在 + * {@code s.upper [2] q(3)[Abc]} —— 见 README「成功路径矩阵」一节。 + */ + @Test + void ioDebugGlobalsSuccessMatchesReference() { + Diff.assertFullMatches(""" + -- 成功路径矩阵第三片:io / debug / 全局函数 + local out = {} + local function d(v, depth) + depth = depth or 0 + local t = type(v) + if t == "nil" then return "nil" end + if t == "boolean" then return tostring(v) end + if t == "number" then return math.type(v) .. ":" .. tostring(v) end + if t == "string" then return string.format("q(%d)[%s]", #v, v) end + if t == "function" then return "function" end + if t == "thread" then return "thread:" .. tostring(coroutine.status(v)) end + if t == "table" then + if depth >= 2 then return "table..." end + local keys, ks = {}, {} + for k in pairs(v) do keys[#keys + 1] = k end + table.sort(keys, function(a, b) + local ta, tb = type(a), type(b) + if ta ~= tb then return ta < tb end + if ta == "number" or ta == "string" then return a < b end + return false + end) + for _, k in ipairs(keys) do + ks[#ks + 1] = (type(k) == "table" and "table" or d(k, 9)) + .. "=" .. d(v[k], depth + 1) + end + return "{" .. table.concat(ks, ",") .. "}" + end + return t + end + local function probe(name, f, ...) + local r = table.pack(pcall(f, ...)) + local s = name .. " [" .. r.n .. "]" + for i = 2, r.n do s = s .. " " .. d(r[i]) end + if not r[1] then s = s .. " ERR " .. tostring(r[2]) end + out[#out + 1] = s + end + local function say(s) out[#out + 1] = s end + + local P = "/tmp/fkok3.txt" + + -- io + probe("i.open_r_missing", io.open, "/tmp/fkok_no_such_xyz", "r") + probe("i.open_badmode", io.open, P, "q") + probe("i.write_read", function() + local f = assert(io.open(P, "w")) + local w = f:write("line1\\nline2\\n") + f:close() + local g = assert(io.open(P, "r")) + return w, g:read("l"), g:read("l"), g:read("l"), g:close() + end) + probe("i.read_a", function() + local g = assert(io.open(P, "r")) + local a = g:read("a") + g:close() + return a + end) + probe("i.read_L", function() + local g = assert(io.open(P, "r")) + local a = g:read("L") + g:close() + return a + end) + probe("i.read_n", function() + local f = assert(io.open(P, "w")); f:write(" 42 3.5e1 -7"); f:close() + local g = assert(io.open(P, "r")) + local a, b, c, e = g:read("n", "n", "n", "n") + g:close() + return a, b, c, e + end) + probe("i.read_num", function() + local f = assert(io.open(P, "w")); f:write("abcdef"); f:close() + local g = assert(io.open(P, "r")) + local a = g:read(3) + local b = g:read(0) + local c = g:read(10) + g:close() + return a, b, c + end) + probe("i.read_multi", function() + local f = assert(io.open(P, "w")); f:write("a\\nb\\n"); f:close() + local g = assert(io.open(P, "r")) + local a, b = g:read("l", "l") + g:close() + return a, b + end) + probe("i.seek", function() + local g = assert(io.open(P, "r")) + local a = g:seek("set", 2) + local b = g:seek("cur", 0) + local c = g:seek("end", 0) + g:close() + return a, b, c + end) + probe("i.lines", function() + local f = assert(io.open(P, "w")); f:write("x\\ny\\n"); f:close() + local r = {} + for l in io.lines(P) do r[#r + 1] = l end + return table.concat(r, ",") + end) + probe("i.lines_fmt", function() + local f = assert(io.open(P, "w")); f:write("ab"); f:close() + local r = {} + for l in io.lines(P, "l") do r[#r + 1] = l end + return table.concat(r, ",") + end) + probe("i.type", function() + local g = assert(io.open(P, "r")) + local a = io.type(g) + g:close() + return a, io.type(g), io.type(io.stdout), io.type(42) + end) + probe("i.append", function() + local f = assert(io.open(P, "w")); f:write("A"); f:close() + local h = assert(io.open(P, "a")); h:write("B"); h:close() + local g = assert(io.open(P, "r")); local a = g:read("a"); g:close() + return a + end) + probe("i.flush_out", function() + local f = assert(io.open(P, "w")) + local a = f:flush() + f:close() + return a + end) + probe("i.output_input", function() + local f = assert(io.open(P, "w")) + local a = io.output(f) + io.write("z") + io.output(io.stdout) + f:close() + local g = assert(io.open(P, "r")); local b = g:read("a"); g:close() + return a == f, b + end) + probe("i.write_num", function() + local f = assert(io.open(P, "w")) + f:write(1, 2.5, "x") + f:close() + local g = assert(io.open(P, "r")); local a = g:read("a"); g:close() + return a + end) + probe("i.close_again", function() + local g = assert(io.open(P, "r")) + g:close() + return g:close() + end) + probe("i.popen_read", function() + local f = io.popen("echo hi") + local a = f:read("l") + return a, f:close() + end) + -- 非零退出码是 nil 不是 false(luaL_pushfail 在 5.4 里就是 lua_pushnil) + probe("i.popen_exit0", function() return io.popen("exit 0"):close() end) + probe("i.popen_exit3", function() return io.popen("exit 3"):close() end) + probe("i.popen_exit7", function() return io.popen("exit 7"):close() end) + probe("i.popen_write", function() + local f = io.popen("cat > /dev/null", "w") + local a = (f:write("abc") == f) + return a, f:close() + end) + os.remove(P) + + -- debug + local function named(a, b) return a + b end + probe("d.getinfo_n", function() local i = debug.getinfo(named, "n"); return i.name, i.namewhat end) + probe("d.getinfo_S", function() local i = debug.getinfo(named, "S"); return i.what, i.source == nil, type(i.short_src), i.linedefined, i.lastlinedefined end) + probe("d.getinfo_u", function() local up = 1; local f = function() return up end; local i = debug.getinfo(f, "u"); return i.nups, i.nparams end) + probe("d.getinfo_l", function() local i = debug.getinfo(1, "l"); return type(i.currentline) end) + probe("d.getlocal", function() local x = 7; return debug.getlocal(1, 1) end) + probe("d.getlocal_name", function() local zz = 1; local n, v = debug.getlocal(1, 1); return type(n), type(v) end) + probe("d.setlocal", function() local q = 1; debug.setlocal(1, 1, 9); return q end) + probe("d.getupvalue", function() local up = 5; local f = function() return up end; return debug.getupvalue(f, 1) end) + probe("d.setupvalue", function() local up = 5; local f = function() return up end; debug.setupvalue(f, 1, 6); return f() end) + probe("d.getupvalue_bad", function() local f = function() end; return debug.getupvalue(f, 1) end) + probe("d.traceback", function() local s = debug.traceback("MSG"); return type(s), s:sub(1, 3) end) + probe("d.traceback_lvl", function() return type(debug.traceback("M", 1)) end) + probe("d.getmetatable", debug.getmetatable, {}) + probe("d.setmetatable", function() local t = {}; return debug.setmetatable(t, { __index = { a = 1 } }) == t, t.a end) + probe("d.getregistry", function() return type(debug.getregistry()) end) + probe("d.uservalue", function() + local a = debug.getuservalue(io.stdout) + local b = debug.setuservalue(io.stdout, 1) + return a, b + end) + probe("d.sethook_count", function() + local n = 0 + debug.sethook(function() n = n + 1 end, "", 10) + local s = 0 + for i = 1, 100 do s = s + i end + debug.sethook() + return n > 0, s + end) + probe("d.gethook", function() + debug.sethook(function() end, "c") + local f = debug.gethook() + debug.sethook() + return type(f), type(debug.gethook()) + end) + + -- 全局 + probe("g.assert", assert, 1, "msg") + probe("g.assert_multi", function() return assert(1, 2, 3) end) + probe("g.error0", function() return error("E", 0) end) + probe("g.error2", function() return error("E", 2) end) + probe("g.errortbl", function() return error({ code = 1 }) end) + probe("g.getmetatable", getmetatable, "") + probe("g.setmetatable", function() local t = setmetatable({}, { __len = function() return 5 end }); return #t end) + probe("g.ipairs", function() local s = 0; for i, v in ipairs({ 10, 20 }) do s = s + i * v end; return s end) + probe("g.pairs", function() local n = 0; for k in pairs({ a = 1, b = 2 }) do n = n + 1 end; return n end) + probe("g.next", next, { a = 1 }) + probe("g.rawequal", rawequal, {}, {}) + probe("g.rawget", rawget, { a = 1 }, "a") + probe("g.rawlen", rawlen, { 1, 2, 3 }) + probe("g.rawlen_str", rawlen, "abc") + probe("g.rawset", function() local t = {}; rawset(t, "k", "v"); return t.k end) + probe("g.select_hash", select, "#", 1, 2, 3) + probe("g.select_i", select, 2, "a", "b", "c") + probe("g.select_neg", select, -1, "a", "b") + probe("g.tonumber", tonumber, "10", 2) + probe("g.tonumber_bad", tonumber, "zz") + probe("g.tonumber_hex", tonumber, "0xff") + probe("g.tostring", tostring, 1.5) + probe("g.type", type, {}) + probe("g.load", function() return load("return 1+1")() end) + probe("g.load_name", function() local f, e = load("x=", "=CHUNK"); return f, e end) + probe("g.load_reader", function() + local i = 0 + local parts = { "return ", "7" } + return load(function() i = i + 1; return parts[i] end)() + end) + probe("g.load_binary", function() return type(load(string.dump(function() return 1 end))) end) + probe("g.xpcall", xpcall, function() error("x") end, function(e) return "H:" .. e end) + probe("g.xpcall_ok", xpcall, function() return 1, 2 end, print) + probe("g.pcall_nested", function() return pcall(pcall, error, "in") end) + probe("g.collect_count", function() return type(collectgarbage("count")) end) + probe("g.collect_step", function() return type(collectgarbage("step")) end) + probe("g.require_math", function() return require("math") == math, type(require("string")) end) + probe("g.require_missing", function() return require("no_such_module_xyz_1") end) + + print(table.concat(out, "\\n")) + """); + } + + /** + * 目录当文件用(18 行)—— 从 PackageLib 的未覆盖行里点名打出来的 + * 其中一条,结果是一处横跨五个门口的真 bug。 + *

+ * 根因只有一条:POSIX 的 {@code fopen(dir, "r")} 是成功的 + * ({@code open(dir, O_RDONLY)} 允许),要写的模式才给 {@code EISDIR}, + * 而读的失败发生在第一次 {@code fgetc} / {@code fread} 上。Java 的 + * {@code RandomAccessFile} 在目录上开都开不了,于是本实现在五处同时错: + *

+     * loadfile(dir)        cannot read <p>: Is a directory   |  cannot open <p>: Input/output error
+     * io.open(dir,"r")     file 句柄                          |  nil, "<p>: No such file or directory", 2
+     * io.open(dir,"w")     nil, "<p>: Is a directory", 21     |  nil, "<p>: No such file or directory", 2
+     * package.searchpath   找到(返回路径)                    |  nil, "no file '<p>'"
+     * require              error loading module ... cannot read|  module not found ...
+     * 
+ * 修在四处:{@code IoLib.openInto} 分模式放行只读 / 报 {@code EISDIR}、 + * {@code LuaFile.getc} 与 {@code read} 报 21、{@code seek}/{@code tell} + * 按零长文件算(目录 fd 上的 {@code lseek} 是允许的)、 + * {@code PackageLib.readable} 认目录、{@code BaseLib.loadfile} 与 + * {@code PackageLib.searcherLua} 改走 {@code cannot read} 那条分支。 + *

+ * 一处不比:{@code seek("end", …)} 在目录上给的是目录自身的大小 + * (本机 64),那是文件系统定的,本实现按零长文件给 0。 + *

+ * 值得记下的是它是怎么被发现的:不是猜的,是把 JaCoCo HTML 报告里 + * {@code class="nc"} 的行号抄出来逐条问「Lua 能不能走到」问出来的。 + */ + @Test + void directoryAsFileMatchesReference() { + Diff.assertFullMatches(""" + -- 目录当文件用:POSIX 的 fopen(dir,"r") 是成功的,读才失败(EISDIR) + local out = {} + local function d(v) + local t = type(v) + if t == "nil" then return "nil" end + if t == "boolean" then return tostring(v) end + if t == "number" then return math.type(v) .. ":" .. tostring(v) end + if t == "string" then return string.format("q(%d)[%s]", #v, (v:gsub("\\n", "\\\\n"))) end + return t + end + local function probe(name, f, ...) + local r = table.pack(pcall(f, ...)) + local s = name .. " [" .. r.n .. "]" + for i = 2, r.n do s = s .. " " .. d(r[i]) end + if not r[1] then s = s .. " ERR " .. d(r[2]) end + out[#out + 1] = s + end + + local D = "/tmp/fkdir" + os.execute("rm -rf " .. D .. " && mkdir -p " .. D .. "/adir.lua " .. D .. "/adir") + local DIR = D .. "/adir.lua" + + -- 1. 加载:fopen 成功、读失败 → "cannot read ...: Is a directory" + probe("lf_dir", loadfile, DIR) + probe("load_dir", load, function() return nil end, "=x", nil, nil) -- 对照:load 不吃文件 + probe("req_dir", function() + local old = package.path + package.path = D .. "/?.lua" + local ok, e = pcall(require, "adir") + package.path = old + return ok, e + end) + probe("sp_dir", package.searchpath, "adir", D .. "/?.lua") + + -- 2. io.open:只读开得开,要写的模式一律 EISDIR(21) + for _, m in ipairs({ "r", "r+", "w", "w+", "a", "a+", "rb" }) do + probe("open[" .. m .. "]", function() + local f, e, en = io.open(DIR, m) + if f == nil then return f, e, en end + local r = { io.type(f) } + local ok1, v1, e1 = pcall(function() return f:read("l") end) + r[#r + 1] = tostring(ok1) .. "/" .. tostring(v1) .. "/" .. tostring(e1) + local ok2, v2, e2 = pcall(function() return f:read("a") end) + r[#r + 1] = tostring(ok2) .. "/" .. tostring(v2) .. "/" .. tostring(e2) + local ok3, v3, e3 = pcall(function() return f:read(1) end) + r[#r + 1] = tostring(ok3) .. "/" .. tostring(v3) .. "/" .. tostring(e3) + local ok4, v4, e4 = pcall(function() return f:read(0) end) + r[#r + 1] = tostring(ok4) .. "/" .. tostring(v4) .. "/" .. tostring(e4) + -- 目录 fd 上的 lseek 是允许的;SEEK_END 给的是目录自身的大小(本机 64), + -- 那是文件系统定的,不比 + local ok5, v5, e5 = pcall(function() return f:seek("cur", 0) end) + r[#r + 1] = tostring(ok5) .. "/" .. tostring(v5) .. "/" .. tostring(e5) + local ok6, v6, e6 = pcall(function() return f:seek("set", 5) end) + r[#r + 1] = tostring(ok6) .. "/" .. tostring(v6) .. "/" .. tostring(e6) + local ok7, v7, e7 = pcall(function() return f:seek("set", -1) end) + r[#r + 1] = tostring(ok7) .. "/" .. tostring(v7) .. "/" .. tostring(e7) + local ok8, v8 = pcall(function() return f:lines() end) + r[#r + 1] = tostring(ok8) .. "/" .. type(v8) + local ok9, v9 = pcall(function() return (f:write("x") == f) end) + r[#r + 1] = tostring(ok9) .. "/" .. tostring(v9) + local ok10, v10 = pcall(function() return f:flush() end) + r[#r + 1] = tostring(ok10) .. "/" .. tostring(v10) + local ok11, v11 = pcall(function() return f:close() end) + r[#r + 1] = tostring(ok11) .. "/" .. tostring(v11) .. "/" .. tostring(io.type(f)) + return table.concat(r, ";") + end) + end + + -- 3. 迭代与数字格式 + probe("lines_iter", function() + local f = io.open(DIR, "r") + local ok, e = pcall(function() for l in f:lines() do end end) + f:close() + return ok, e + end) + probe("read_n", function() + local f = io.open(DIR, "r") + local ok, v, e = pcall(function() return f:read("n") end) + f:close() + return ok, v, e + end) + probe("read_L", function() + local f = io.open(DIR, "r") + local ok, v, e = pcall(function() return f:read("L") end) + f:close() + return ok, v, e + end) + probe("tostring_dir", function() + local f = io.open(DIR, "r") + local s = tostring(f) + f:close() + return s:match("^file %(0x%x+%)$") ~= nil -- 只比形状:%p 是地址 + end) + probe("setvbuf_dir", function() + local f = io.open(DIR, "r") + local ok, v = pcall(function() return f:setvbuf("no") end) + f:close() + return ok, v + end) + probe("remove_dir", os.remove, DIR) + probe("open_missing_dir", io.open, D .. "/adir/nope.lua", "r") + + os.execute("rm -rf " .. D) + print(table.concat(out, "\\n")) + """); + } + + /** + * 表键语义矩阵:键 × 键 的读写、rehash、表构造、NaN/inf,以及 + * {@code math.maxinteger} 附近那个区间。 + *

+ * 这个是覆盖扫描指过来的:{@code LuaTable} 有 16 行没被走到,其中 + * {@code hashFloat} 的 {@code -0.0} 归一化、{@code landsInArray} 的浮点 + * 分支、rehash 时哈希槽不够那三条,都是「只在很窄的键区间上才走」的路径。 + * 手挑几个键是碰不到它们的,所以做成键 × 键的方阵:20 个键各写一遍、 + * 再用全部 20 个键各读一遍。 + *

+ * 扫出来的真分歧只有一处,但它不在 {@code LuaTable} 里: + * {@code rawequal(math.maxinteger, math.maxinteger + 1.0)} 应当是 + * {@code false},本实现给 {@code true}。两个数确实不等—— + * {@code (double)(2^63-1)} 舍入成 {@code 2^63} 才撞上的——而表拿 + * {@code rawequal} 当键的比较用,于是 {@code t[math.maxinteger]} 与 + * {@code t[2^63]} 被当成了同一项。修的是 + * {@code LuaValue.numberEqual}:变体不同的两个数要按 F2Ieq + * 两边都转整数再比,不是统一转 double。 + *

+ * 两条规则只在 {@code math.maxinteger} 往前约一千个整数上分岔,所以脚本末尾 + * 专门按步长扫了那 2048 个整数——去掉这一段,修与不修只差 6 行;带上它, + * 改回旧写法会红 77 行。 + *

+ * {@code next} 的顺序依赖哈希函数,两边不必相同,所以键集合一律排序 + * 后再打印;只比集合,不比顺序。 + */ + @Test + void tableKeySemanticsMatchesReference() { + Diff.assertMatches(""" +-- 表键语义矩阵:整数/浮点的键等价、rehash、表构造、NaN/inf、以及 +-- math.maxinteger 附近那个「整数转 double 会舍入」的分岔区间 +-- 整数与浮点在「变体不同」时的相等比较边界 +local MAXI = math.maxinteger +local MINI = math.mininteger +local C = { + { "MAXI", MAXI, MAXI + 1.0 }, + { "MAXI2", MAXI, MAXI + 0.0 }, + { "MAXI3", MAXI, 2.0 ^ 63 }, + { "MAXI4", MAXI, -2.0 ^ 63 }, + { "MINI", MINI, MINI - 1.0 }, + { "MINI2", MINI, MINI + 0.0 }, + { "MINI3", MINI, -2.0 ^ 63 }, + { "OK1", 1, 1.0 }, + { "OK2", 0, -0.0 }, + { "OK3", 2 ^ 53, 2 ^ 53 + 1.0 }, + { "OK4", 2 ^ 53 + 1, 2 ^ 53 + 2.0 }, + { "BIG", 2 ^ 62, 2 ^ 62 + 1024.0 }, + { "BIG2", 2 ^ 62, 2 ^ 62 + 512.0 }, +} +for _, c in ipairs(C) do + local a, b = c[2], c[3] + print(string.format("%-6s %-22s == %-22s : %s rawequal=%s < = > : %s %s %s", c[1], + tostring(a), tostring(b), tostring(a == b), tostring(rawequal(a, b)), + tostring(a < b), tostring(a <= b), tostring(a > b))) +end +-- 表键:这两类键是不是同一项 +for _, c in ipairs(C) do + local t = {} + t[c[2]] = "A" + print(string.format("key %-6s get(%s)=%s type=%s", c[1], tostring(c[3]), tostring(t[c[3]]), + math.type(c[3]))) +end + +-- LuaTable 键语义矩阵:键 × 键 的读写,外加 rehash / 表构造 / 边界键 +-- next 的顺序依赖哈希函数、两边不必相同,所以键集合一律排序后再打印 + +local function keys(t) + local ks = {} + for k in next, t do + ks[#ks + 1] = tostring(k) .. "/" .. tostring(math.type(k)) + end + table.sort(ks) + return table.concat(ks, ",") +end + +local KEYS = { + "0", "-0.0", "0.0", "1", "1.0", "-1.0", "2", "2.0", "0.5", "-0.5", + "3", "2^53", "2^53+1.0", "math.maxinteger", "math.maxinteger+1.0", + "math.mininteger", "1e300", "-1e300", "math.pi", "1/3", +} + +-- 1) 键 × 键:用 k 写、用 q 读 +for _, ks in ipairs(KEYS) do + local k = load("return " .. ks)() + local t = {} + local ok, err = pcall(function() + t[k] = "V" + end) + if not ok then + print(string.format("set %-18s ERR %s", ks, tostring(err))) + else + local rd = {} + for _, qs in ipairs(KEYS) do + local q = load("return " .. qs)() + rd[#rd + 1] = qs .. "=" .. tostring(t[q]) + end + print(string.format("set %-18s ok #=%s keys={%s} %s", ks, tostring(#t), keys(t), + table.concat(rd, " "))) + end +end + +-- 2) 先写整数键再写同一个值的浮点键(触发 landsInArray 的浮点分支与 rehash) +for n = 0, 12 do + local t = {} + for i = 1, n do + t[i] = tostring(i) + end + t[2.0] = "F" + print(string.format("mix n=%-3d #=%s keys={%s}", n, tostring(#t), keys(t))) +end + +-- 3) 多条浮点键挤进哈希部分,逼哈希槽不够(hashSize = needSlots 那条) +for n = 0, 10 do + local t = {} + for i = 1, n do + t[i] = true + end + for i = 1, 8 do + t[i + 0.5] = true -- 全是浮点、落不进数组部分 + end + print(string.format("hash n=%-3d #=%s keys={%s}", n, tostring(#t), keys(t))) +end + +-- 4) 表构造里的边界键 +local c1 = { [0] = "zero", [1] = "one" } +print("ctor0", #c1, keys(c1)) +local c2 = { [1.0] = "f1", [2.0] = "f2" } +print("ctorf", #c2, keys(c2)) +local c3 = { [-0.0] = "nz", [0.0] = "pz" } +print("ctorz", #c3, keys(c3)) +local c4 = { [1] = "a", [2] = "b", [3] = "c" } +print("ctorseq", #c4, keys(c4)) +local w = setmetatable({}, { __mode = "k" }) +w[1] = "x" +print("weak", type(w), keys(w)) + +-- 5) NaN / inf 键:hashFloat 的归一化与「索引是 NaN」那条错 +for _, ks in ipairs({ "0/0", "-(0/0)", "math.huge", "-math.huge", "1e400" }) do + local k = load("return " .. ks)() + local t = {} + local ok, err = pcall(function() + t[k] = "V" + end) + print(string.format("nan %-12s set=%s %s", ks, tostring(ok), tostring(err))) + local ok2, v2 = pcall(function() + return t[k] + end) + print(string.format("nan %-12s get=%s %s", ks, tostring(ok2), tostring(v2))) +end + +-- 6) 负浮点键大批扫(hashFloat 里 ni<0 取反那条) +local NEG = { "-1.0", "-2.0", "-0.5", "-1e300", "-math.pi", "-2^53", "-1/3", "-1e-300" } +for _, ks in ipairs(NEG) do + local k = load("return " .. ks)() + local t = {} + t[k] = "V" + print(string.format("neg %-10s keys={%s} reread=%s", ks, keys(t), tostring(t[k + 0.0]))) +end + +-- 7) 删掉之后再写:浮点键与整数键互相覆盖 +local d = {} +d[1] = "int" +d[1.0] = "flt" +print("ovr1", keys(d), d[1], d[1.0], #d) +d[1.0] = nil +print("ovr2", keys(d), d[1], #d) +d[1] = nil +print("ovr3", keys(d), #d) + +-- 8) rawget / rawset / rawlen 走的是同一套键归一化 +local r = {} +rawset(r, 2.0, "two") +print("raw", rawget(r, 2), rawget(r, 2.0), rawlen(r), keys(r)) +rawset(r, -0.0, "nz") +print("rawz", rawget(r, 0), rawget(r, 0.0), rawlen(r), keys(r)) + +-- 9) 把浮点键混进序列,看长度怎么算 +for n = 1, 8 do + local t = {} + for i = 1, n do + t[i] = true + end + t[n + 1.0] = true + print(string.format("seq n=%-3d #=%s", n, tostring(#t))) + t[n + 1.0] = nil + print(string.format("seqd n=%-3d #=%s", n, tostring(#t))) +end + +-- 10) 精确扫 math.maxinteger 附近:整数转 double 会舍入到 2^63,而 2^63 转不回整数 +-- 这个区间是「都转 double 比」与「F2Ieq 比」唯一分岔的地方 +for d = 0, 2048, 64 do + local i = math.maxinteger - d + local f = i + 0.0 + print(string.format("near d=%-5d i=%s f=%s eq=%s raw=%s int=%s", d, tostring(i), + tostring(f), tostring(i == f), tostring(rawequal(i, f)), + tostring(math.tointeger(f)))) +end +for d = 0, 2048, 64 do + local i = math.mininteger + d + local f = i + 0.0 + print(string.format("nearlo d=%-5d i=%s f=%s eq=%s raw=%s int=%s", d, tostring(i), + tostring(f), tostring(i == f), tostring(rawequal(i, f)), + tostring(math.tointeger(f)))) +end +-- 这些整数作键:写进去之后用对应的浮点去读 +for d = 0, 2048, 128 do + local i = math.maxinteger - d + local t = {} + t[i] = "A" + print(string.format("nearkey d=%-5d get=%s keys={%s}", d, tostring(t[i + 0.0]), + tostring(#t))) +end + """); + } + + /** + * {@code debug} 库的帧形态矩阵:hook 帧、C 帧、协程帧、尾调用帧、 + * 传函数(而不是层号)的 {@code getinfo},共 106 行。 + *

+ * 覆盖扫描指过来的是 {@code DebugLib} 的 22 行:{@code currentLine} 对非 Lua + * 帧返回 {@code -1}、{@code funcNameFromCallInfo} 的 {@code ci.hooked} 分支、 + * {@code getinfo} 传函数+线程的另一条取帧路径、{@code traceback} 的 + * {@code (...tail calls...)}。它们全都是「栈长得不一样时才走」的路径,手挑两 + * 个例子碰不到。 + *

+ * hook 帧要取到 level 3:hook 里调 {@code getinfo} 时,level 1 是包在外 + * 面的 {@code pcall}、level 2 是打印用的 helper,hook 自己的帧在第 3 层 + * —— 只取到 2 层的话那条 {@code namewhat = "hook"} 永远走不到,而报告上它看 + * 起来和别的未覆盖行一样。 + *

+ * 有一处不比:最外层那个 C 帧的 {@code "(C temporary)"},值正是宿主调用 + * {@code pmain} 时压的实参——参考实现压的是 {@code (argc, argv)},所以取出来是 + * {@code 2} 和一个 userdata;本实现的驱动压的是脚本文本。那不是 Lua 语义, + * 是宿主自己的调用约定,比不出东西,脚本里只取到 level 2。 + *

+ * 用 {@code assertFullMatches}:脚本第 8 段打印的 {@code debug.traceback} + * 本身就是回溯,而后面还有四段输出——走 {@code assertMatches} 会把它们 + * 连着错误信息一起砍掉(这也是 {@code Diff} 直接判失败而不是默默砍的原因)。 + */ + @Test + void debugFrameShapesMatchReference() { + Diff.assertFullMatches(""" +-- debug 库帧形态矩阵:hook 帧 / C 帧 / 协程帧 / 尾调用帧 / 传函数的 getinfo +-- 输出里不能出现地址:func 一律替换成 ,activelines 只打条数 + +local function dump(tag, ...) + local ok, info = pcall(debug.getinfo, ...) + if not ok then + print(tag, "ERR " .. tostring(info)) + return + end + if info == nil then + print(tag, "nil") + return + end + local keys = {} + for k in pairs(info) do + keys[#keys + 1] = k + end + table.sort(keys) + local out = {} + for _, k in ipairs(keys) do + local v = info[k] + local tv = type(v) + if tv == "function" then + v = "" + elseif tv == "table" then + local c = 0 + for _ in pairs(v) do + c = c + 1 + end + v = "L(" .. c .. ")" + end + out[#out + 1] = k .. "=" .. tostring(v) + end + print(tag, table.concat(out, " ")) +end + +-- 1) 顶层各层号 +for l = 0, 4 do + dump("top" .. l, l, "nSlufLt") +end +dump("top99", 99, "nS") +dump("topneg", -1, "nS") + +-- 2) Lua 函数里的层号 +local function inner() + dump("in1", 1, "nSlufLt") + dump("in2", 2, "nSlufLt") + dump("in3", 3, "nSlufLt") +end +local function outer() + inner() +end +outer() + +-- 3) C 帧:table.sort 的回调里往上看,level 2 是 C 函数 +local t = { 3, 1, 2 } +local seen = 0 +table.sort(t, function(a, b) + seen = seen + 1 + if seen == 1 then + dump("csort1", 1, "nSluf") + dump("csort2", 2, "nSluf") + dump("csort3", 3, "nSluf") + end + return a < b +end) +-- pcall 的 C 帧 +pcall(function() + dump("pcall1", 1, "nS") + dump("pcall2", 2, "nS") +end) + +-- 4) 协程:外部取、内部取、传函数取 +local co = coroutine.create(function(x) + local loc = x + 1 + dump("co_in1", 1, "nSluf") + dump("co_in2", 2, "nSluf") + coroutine.yield(loc) + dump("co_in1b", 1, "nSl") + return loc +end) +coroutine.resume(co, 10) +dump("co_ext0", co, 0, "nSluf") +dump("co_ext1", co, 1, "nSluf") +dump("co_ext2", co, 2, "nSluf") +coroutine.resume(co) +dump("co_dead", co, 1, "nSl") +-- 传函数 + 线程(走的是另一条取帧的路) +local function plain() + return 1 +end +dump("cofn1", co, plain, "nSluf") +dump("cofn2", co, print, "nSluf") +dump("fn1", plain, "nSluf") +dump("fn2", print, "nSluf") + +-- 5) getlocal:层号与下标越界 +-- 只到 l=2:l=3 是最外层那个 C 帧,它的「C temporary」正是宿主调用 pmain 时 +-- 压的实参 —— 参考实现压的是 (argc, argv),本实现的驱动压的是脚本文本, +-- 内容由宿主定,不是 Lua 语义,比不出东西。 +for l = 1, 2 do + for n = -2, 3 do + local ok, name, val = pcall(debug.getlocal, l, n) + print(string.format("gl l=%d n=%-3d ok=%s name=%s val=%s", l, n, tostring(ok), + tostring(name), tostring(val))) + end +end +-- getlocal 对协程 +local co2 = coroutine.create(function() + local a, b = 1, 2 + coroutine.yield() +end) +coroutine.resume(co2) +for n = -1, 3 do + local ok, name, val = pcall(debug.getlocal, co2, 1, n) + print(string.format("glco n=%-3d ok=%s name=%s val=%s", n, tostring(ok), tostring(name), + tostring(val))) +end + +-- 6) getupvalue:Lua 闭包与 C 函数 +local up1, up2 = "u1", "u2" +local function lf() + return up1, up2 +end +for n = -1, 4 do + local ok, name, val = pcall(debug.getupvalue, lf, n) + print(string.format("gup lua n=%-3d ok=%s name=%s val=%s", n, tostring(ok), tostring(name), + tostring(val))) +end +for _, fn in ipairs({ "print", "type", "tostring", "string.rep", "table.sort", "load", + "require", "pcall", "select" }) do + local f = load("return " .. fn)() + for n = 0, 2 do + local ok, name, val = pcall(debug.getupvalue, f, n) + print(string.format("gup %-12s n=%d ok=%s name=%s val=%s", fn, n, tostring(ok), + tostring(name), tostring(val))) + end +end + +-- 7) hook 内部取帧:hooked 帧的名字是 "hook" +local count = 0 +debug.sethook(function(what, line) + count = count + 1 + if count > 2 then + debug.sethook() + return + end + print("hook", what, tostring(line)) + for lv = 1, 5 do + dump(" h" .. lv .. "n", lv, "n") + dump(" h" .. lv .. "Sl", lv, "Sl") + end +end, "l") +local aa = 1 +local bb = 2 +local cc = aa + bb +debug.sethook() +print("afterhook", aa, bb, cc) + +-- 8) 尾调用链 + traceback +local function tb3() + return debug.traceback("msg3") +end +local function tb2() + return tb3() +end +local function tb1() + return tb2() +end +print("tb", (tb1():gsub("\\n", " | "))) +-- 尾调用帧上取 info +local function ti3() + dump("ti1", 1, "nSlt") + dump("ti2", 2, "nSlt") + dump("ti3", 3, "nSlt") + return 1 +end +local function ti2() + return ti3() +end +local function ti1() + return ti2() +end +ti1() + +-- 9) 长函数 + hook:逼行号搜索走到 lineinfo 末尾 +local function big() + local s = 0 + s = s + 1 + s = s + 2 + s = s + 3 + s = s + 4 + s = s + 5 + s = s + 6 + s = s + 7 + s = s + 8 + s = s + 9 + s = s + 10 + s = s + 11 + s = s + 12 + s = s + 13 + s = s + 14 + s = s + 15 + s = s + 16 + s = s + 17 + s = s + 18 + s = s + 19 + s = s + 20 + s = s + 21 + s = s + 22 + s = s + 23 + s = s + 24 + s = s + 25 + s = s + 26 + s = s + 27 + s = s + 28 + s = s + 29 + s = s + 30 + s = s + 31 + s = s + 32 + s = s + 33 + s = s + 34 + s = s + 35 + s = s + 36 + s = s + 37 + s = s + 38 + s = s + 39 + s = s + 40 + return s +end +local last = 0 +debug.sethook(function(what, line) + last = line +end, "l") +print("big", big()) +debug.sethook() +print("lasthookline", last) +dump("bigfn", big, "SL") + """); + } + + /** + * 「协程 × C 边界」矩阵:在各种由 C 发起的回调里 {@code yield} 会怎样。 + *

+ * 每条都同时问 {@code coroutine.isyieldable()},把「能不能」与「真做了会怎样」 + * 分开记 —— 前者是纯查询,后者才牵扯 {@code nny}(C 版的 {@code nyci})计数 + * 在哪些入口被加一。这两件事被同一处缺陷同时答错时,只看后者会漏掉前者。 + *

+ * 覆盖的入口:{@code pcall} / {@code xpcall} / {@code xpcall} 的消息处理 + * 函数 / {@code table.sort} 比较函数 / {@code string.gsub} 替换函数 / + * {@code load} 的 reader / 八个元方法(含 {@code __close})/ 四种泛型 for 的 + * 迭代器 / 调试 hook / {@code coroutine.wrap} / 嵌套协程 / resume 已死与已 + * 出错的协程 / 挂起的协程 resume 自己。 + *

+ * 咬出来的那一处真分歧:{@code xpcall} 的消息处理函数里 yield。 + * 参考实现给 {@code false, "error in error handling"} 且协程 dead; + * 本实现给 {@code true, "in msgh"} 且停在 suspended。原因是 + * {@code LuaState.applyErrMsg} 用的是可 yield 的 {@code pcall},而 C 版 + * {@code ldo.c: luaD_callnoyield}。道理:能在错误处理的续体里 yield,就等于 + * 承认「错误已交出去一半」这种中间态可以被挂起,而 C 版没有这种状态。 + * 反向验证:把 {@code callNoYield} 改回 {@code call},这里正好红 2 行。 + *

+ * 有一处不能比:{@code pairs} 的键顺序。字符串键走 {@code luaS_hash}, + * seed 来自 {@code lstate.c: luai_makeseed} —— {@code time(NULL)} 加三个 + * ASLR 地址,每次启动都不一样。实测同一份脚本在参考实现上连跑 5 次, + * {@code pairs} 的第一个键在 {@code a}/{@code b} 之间跳。所以本用例的 + * {@code for_pairs} 走 {@code drain}:把协程跑到死、把全部 yield 值排序 + * 再打印。任何打印字符串键先后的探针都是假红发生器。 + *

+ * 另一处:{@code load} 的 reader 必须有终止条件(要 {@code return nil}), + * 否则 {@code load} 会一直读下去——第一版漏了这句,把参考实现二进制挂住了, + * 只能 {@code pkill}。 + */ + @Test + void coroutineYieldAcrossCBoundaryMatchesReference() { + Diff.assertMatches(""" +-- 协程 × C 边界矩阵:在各种「由 C 发起的回调」里 yield 会发生什么 +-- 每条都同时问 coroutine.isyieldable(),把「能不能」与「真做了会怎样」分开记 + +local function show(name, ...) + local a = { ... } + local out = {} + for i = 1, select("#", ...) do + out[#out + 1] = tostring(a[i]) + end + print(name, table.concat(out, " ")) +end + +-- 在协程里跑 fn,打印 resume 的全部返回值 +local function run(name, fn) + local co = coroutine.create(fn) + show("run " .. name, coroutine.resume(co)) + show(" status " .. name, coroutine.status(co)) +end + +-- 只问能不能 yield,不真 yield +local function ask(name, fn) + local co = coroutine.create(fn) + show("ask " .. name, coroutine.resume(co)) +end + +-- 1) 基线:Lua 函数里直接 yield +run("plain", function() + coroutine.yield(1, 2) + return "done" +end) +ask("isyieldable_plain", function() + return coroutine.isyieldable() +end) + +-- 2) pcall / xpcall 里(Lua 的 pcall 是可 yield 的) +run("pcall", function() + return pcall(function() + coroutine.yield("in pcall") + return 1 + end) +end) +ask("isyieldable_pcall", function() + return pcall(function() + return coroutine.isyieldable() + end) +end) +run("xpcall", function() + return xpcall(function() + coroutine.yield("in xpcall") + return 1 + end, function(m) + return "H:" .. tostring(m) + end) +end) +ask("isyieldable_xpcall", function() + return xpcall(function() + return coroutine.isyieldable() + end, tostring) +end) +-- 消息处理函数里 yield +run("xpcall_msgh", function() + return xpcall(function() + error("boom") + end, function(m) + coroutine.yield("in msgh") + return "H" .. m + end) +end) + +-- 3) C 发起的回调:sort / gsub / gmatch / load 的 reader +run("sort", function() + local t = { 3, 1, 2 } + table.sort(t, function(a, b) + coroutine.yield("in sort") + return a < b + end) + return table.concat(t, ",") +end) +ask("isyieldable_sort", function() + local t = { 3, 1, 2 } + table.sort(t, function(a, b) + y = coroutine.isyieldable() + return a < b + end) + return y +end) + +run("gsub_fn", function() + return string.gsub("abc", "%a", function(c) + coroutine.yield("in gsub") + return c:upper() + end) +end) +run("gsub_table", function() + return string.gsub("abc", "%a", { a = "A" }) +end) +-- 注意:reader 必须有终止条件,否则 load 会一直读下去(无限循环) +run("load_reader", function() + local n = 0 + local f, err = load(function() + n = n + 1 + if n == 1 then + coroutine.yield("in reader") + return "return " + elseif n == 2 then + return "42" + end + return nil + end) + return tostring(f), tostring(err) +end) +ask("isyieldable_load_reader", function() + local n = 0 + local f, err = load(function() + n = n + 1 + if n == 1 then + y = coroutine.isyieldable() + return "return " + elseif n == 2 then + return "42" + end + return nil + end) + return y, tostring(f), tostring(err) +end) + +-- 4) 元方法里 yield +local function meta(name, key, body) + run(name, function() + local t = setmetatable({}, { [key] = function(...) + coroutine.yield("in " .. key) + return body(...) + end }) + return body2 and body2(t) or t + end) +end +run("meta_index", function() + local t = setmetatable({}, { + __index = function(_, k) + coroutine.yield("in __index") + return "V" .. tostring(k) + end, + }) + return t.x +end) +run("meta_newindex", function() + local t = setmetatable({}, { + __newindex = function(_, k, v) + coroutine.yield("in __newindex") + rawset(_, k, v) + end, + }) + t.x = 1 + return t.x +end) +run("meta_call", function() + local t = setmetatable({}, { + __call = function(_, a) + coroutine.yield("in __call") + return a * 2 + end, + }) + return t(21) +end) +run("meta_concat", function() + local t = setmetatable({}, { + __concat = function(a, b) + coroutine.yield("in __concat") + return tostring(a) .. tostring(b) + end, + }) + return t .. "x" +end) +run("meta_tostring", function() + local t = setmetatable({}, { + __tostring = function() + coroutine.yield("in __tostring") + return "TS" + end, + }) + return tostring(t) +end) +run("meta_add", function() + local t = setmetatable({}, { + __add = function(a, b) + coroutine.yield("in __add") + return 7 + end, + }) + return t + 1 +end) +run("meta_eq", function() + local mt = { + __eq = function() + coroutine.yield("in __eq") + return true + end, + } + local a, b = setmetatable({}, mt), setmetatable({}, mt) + return a == b +end) +run("meta_len", function() + local t = setmetatable({}, { + __len = function() + coroutine.yield("in __len") + return 9 + end, + }) + return #t +end) +run("meta_close", function() + local closed = false + do + local t = setmetatable({}, { + __close = function() + coroutine.yield("in __close") + closed = true + end, + }) + end + return closed +end) + +-- 把协程跑到死,把所有 yield 值排序后一起打印。 +-- 排序是必须的:字符串键的遍历顺序由 luaS_hash 的 seed 决定,而参考实现的 +-- seed 是 lstate.c: luai_makeseed —— time(NULL) 加三个 ASLR 地址, +-- 每次启动都不一样。实测同一份脚本连跑 5 次,pairs 首个键在 a/b 之间跳。 +-- 任何打印字符串键先后的探针都是「假红发生器」。 +local function drain(name, fn) + local co = coroutine.create(fn) + local all, res = {}, nil + for _ = 1, 50 do + res = { coroutine.resume(co) } + if coroutine.status(co) == "dead" then + break + end + all[#all + 1] = tostring(res[2]) + end + table.sort(all) + show("drain " .. name, table.concat(all, "|")) + show(" drain_ret " .. name, table.unpack(res)) + show(" drain_status " .. name, coroutine.status(co)) +end + +-- 5) 泛型 for:next/pairs(C 迭代器)与自定义(Lua 迭代器) +drain("for_pairs", function() + local t = { a = 1, b = 2, c = 3 } + for k, v in pairs(t) do + coroutine.yield("p:" .. k .. "=" .. v) + end + return "ok" +end) +run("for_ipairs", function() + local t = { 10, 20 } + for i, v in ipairs(t) do + coroutine.yield("in ipairs:" .. i) + end + return "ok" +end) +run("for_next", function() + local t = { a = 1 } + for k, v in next, t do + coroutine.yield("in next:" .. k) + end + return "ok" +end) +run("for_custom", function() + local function iter(s, c) + if c >= 2 then + return nil + end + return c + 1, (c + 1) * 10 + end + for i, v in iter, nil, 0 do + coroutine.yield("in custom:" .. i) + end + return "ok" +end) +ask("isyieldable_for_pairs", function() + local t = { a = 1 } + for k in pairs(t) do + return coroutine.isyieldable() + end +end) + +-- 6) hook 里 yield +run("hook_yield", function() + debug.sethook(function() + debug.sethook() + coroutine.yield("in hook") + end, "l") + local x = 1 + return x +end) +ask("isyieldable_hook", function() + debug.sethook(function() + debug.sethook() + y = coroutine.isyieldable() + end, "l") + local x = 1 + return y +end) + +-- 7) 主协程里 yield +show("main_yield", pcall(coroutine.yield, 1)) +show("main_isyieldable", coroutine.isyieldable()) + +-- 8) coroutine.wrap 包起来再 yield +local w = coroutine.wrap(function() + coroutine.yield("w1") + return "w2" +end) +show("wrap1", w()) +show("wrap2", w()) +show("wrap3", pcall(w)) + +-- 9) 嵌套:协程里再开协程 +run("nested", function() + local inner = coroutine.create(function() + coroutine.yield("inner") + return "innerdone" + end) + show(" nested1", coroutine.resume(inner)) + coroutine.yield("outer") + show(" nested2", coroutine.resume(inner)) + return "outerdone" +end) + +-- 10) resume 一个已经跑完的协程 / 出错的协程 +local c1 = coroutine.create(function() + return 1 +end) +show("dead1", coroutine.resume(c1)) +show("dead2", coroutine.resume(c1)) +local c2 = coroutine.create(function() + error("inner error") +end) +show("errored1", coroutine.resume(c2)) +show("errored2", coroutine.resume(c2)) +-- 在挂起的协程里 resume 自己 +local c3 +c3 = coroutine.create(function() + show("self_resume", coroutine.resume(c3)) + coroutine.yield() +end) +show("c3a", coroutine.resume(c3)) +show("c3b", coroutine.resume(c3)) + """); + } + + /** + * to-be-closed 语义矩阵:{@code } 在各种「退出块」的时机里怎么被调、 + * 传什么、自己出错/yield 会怎样。 + *

+ * {@code } 是 5.4 唯一的新语法,边界也最多。按退出方式铺开:正常到块尾、 + * 嵌套、{@code return} 提前、{@code break}、{@code goto} 跳出、{@code error} + * 展开(字符串与非字符串两种错误对象)、多个 tbc 的关闭次序;再按 + * {@code __close} 自己的行为铺开:记一笔、抛错、抛非字符串、返回多值、 + * {@code yield}、不是函数、是可调用表、压根没有;然后是它与 + * {@code pcall} / {@code xpcall} / 协程 / {@code coroutine.close} 的交叠, + * 以及一批声明期的语法边界。56 行。 + *

+ * 咬出两处真分歧,都是同一个根因的两面:{@code __close} 抛出的错误 + * 也要过消息处理函数。 + *

    + *
  • {@code xpcall} + 带 {@code } 的函数体里 {@code error}, + * 且 {@code __close} 也 {@code error}:参考实现给 + * {@code msgh orig | close a err=H | msgh E:a | false H},本实现给 + * {@code ... | false E:a} —— 最终结果是处理函数的返回值, + * 不是 {@code __close} 那个原始错。原因是 C 的 {@code luaD_pcall} + * 到最后一句才 {@code L->errfunc = old_errfunc},而 + * {@code luaD_closeprotected} 就在那句之前,所以跑 + * {@code __close} 的整个过程里处理函数仍然生效。
  • + *
  • 协程里 {@code xpcall}:处理函数要在关 {@code __close} 之前跑。 + * 参考给 {@code msgh h2 after | close a err=H-h2},本实现(旧)给 + * {@code close a err=after | msgh h2 after} —— {@code __close} + * 收到的是没处理过的原始错,顺序也反了。
  • + *
+ * 连带修好的是「{@code error in error handling}」那一条:处理函数自己抛错时, + * {@code __close} 的错再过一次它就被兜成那句话(旧实现给的是原始错)。 + *

+ * 改这两处时踩到的第二个坑:处理函数必须在把 {@code top} 压回 + * {@code fIdx + 1} 之前跑。{@code luaG_errormsg} 是在出错那一刻的 + * 栈顶压调用帧的,那会儿被保护函数的局部变量都还在;先收 {@code top} + * 再跑,它就从 {@code fIdx + 2} 开始写 —— 而那正是第一个局部变量(也就是那个 + * {@code } 变量)的槽,于是 {@code __close} 被覆盖成处理函数自己, + * 报 {@code attempt to call a nil value}。 + *

+ * 有一条从矩阵里删掉了:「消息处理函数自己抛错」的那条组合。C 的 + * {@code luaG_errormsg} 会在那里无限递归到 C 栈溢出,输出几百行、 + * 行数随栈深度而变,没法逐字节比。它的结果由 G 段单独钉住。 + * 另外 {@code coroutine.close(coroutine.running())} 必须包 {@code pcall}: + * 参考实现在那里直接抛,不包的话脚本当场死在中间,后面整段一条也不会比。 + */ + @Test + void toBeClosedSemanticsMatchesReference() { + Diff.assertMatches(""" +-- to-be-closed 语义矩阵:__close 在各种「退出块」的时机里怎么被调、传什么、 +-- 自己出错/yield 会怎样。 是 5.4 唯一的新语法,边界最多。 + +local LOG + +local function show(...) + local a = { ... } + for i = 1, select("#", ...) do + a[i] = tostring(a[i]) + end + LOG[#LOG + 1] = table.concat(a, " ") +end + +-- 造一个带 __close 的值。kind 决定 __close 做什么: +-- "log" 记一笔就返回 +-- "err" 抛一个错 +-- "err2" 抛非字符串对象 +-- "multi" 返回多个值 +-- "yield" yield 出去 +-- "noclose" 没有 __close 元方法 +local function mk(tag, kind, extra) + local mt = {} + if kind == "log" then + mt.__close = function(self, err) + show("close", tag, "err=" .. tostring(err)) + return extra + end + elseif kind == "err" then + mt.__close = function(self, err) + show("close", tag, "err=" .. tostring(err)) + error("E:" .. tag, 0) + end + elseif kind == "err2" then + mt.__close = function(self, err) + show("close", tag, "err=" .. tostring(err)) + error({ code = tag }, 0) + end + elseif kind == "multi" then + mt.__close = function(self, err) + show("close", tag, "err=" .. tostring(err)) + return 1, 2, 3 + end + elseif kind == "yield" then + mt.__close = function(self, err) + show("close", tag, "err=" .. tostring(err)) + coroutine.yield("Y:" .. tag) + end + elseif kind == "notfn" then + mt.__close = 42 + elseif kind == "callable" then + mt.__close = setmetatable({}, { + __call = function() + show("close", tag, "callable") + end, + }) + elseif kind == "noclose" then + -- 什么都不放: 但没有 __close + end + mt.__tostring = function() + return "<" .. tag .. ">" + end + return setmetatable({}, mt) +end + +local function reset() + LOG = {} +end + +local function dump(name) + print(name, table.concat(LOG, " | ")) +end + +-- ==================== A. 基本时机 ==================== + +reset() +do + local a = mk("a", "log") + local b = mk("b", "log") + show("body") +end +dump("A1_normal") + +reset() +do + local a = mk("a", "log") + do + local b = mk("b", "log") + show("inner") + end + show("outer") +end +dump("A2_nested") + +reset() +local function early() + local a = mk("a", "log") + show("before return") + return "R" +end +show("A3_ret", early()) +dump("A3_return") + +reset() +for i = 1, 3 do + local a = mk("a" .. i, "log") + if i == 2 then + break + end + show("iter", i) +end +dump("A4_break") + +reset() +do + local a = mk("a", "log") + for i = 1, 2 do + local b = mk("b" .. i, "log") + if i == 1 then + goto done + end + end + ::done:: + show("after goto") +end +dump("A5_goto") + +reset() +show("A6_err", pcall(function() + local a = mk("a", "log") + error("boom", 0) +end)) +dump("A6_error") + +reset() +show("A7_err_obj", pcall(function() + local a = mk("a", "log") + error({ c = 1 }, 0) +end)) +dump("A7_error_obj") + +-- 多个 tbc 的关闭顺序(应是从里到外、后声明的先关) +reset() +do + local a = mk("a", "log") + local b = mk("b", "log") + local c = mk("c", "log") +end +dump("A8_order") + +-- ==================== B. __close 自己出错 ==================== + +reset() +show("B1_close_err", pcall(function() + local a = mk("a", "err") + return "body-ok" +end)) +dump("B1") + +reset() +show("B2_close_err_while_unwinding", pcall(function() + local a = mk("a", "err") + error("orig", 0) +end)) +dump("B2") + +reset() +show("B3_two_tbc_first_err", pcall(function() + local a = mk("a", "err") + local b = mk("b", "log") +end)) +dump("B3") + +reset() +show("B4_two_tbc_second_err", pcall(function() + local a = mk("a", "log") + local b = mk("b", "err") +end)) +dump("B4") + +reset() +show("B5_nonstring_err", pcall(function() + local a = mk("a", "err2") +end)) +dump("B5") + +reset() +show("B6_nonstring_err_unwinding", pcall(function() + local a = mk("a", "err2") + error("orig", 0) +end)) +dump("B6") + +reset() +show("B7_where_prefix", pcall(function() + local a = mk("a", "err") +end)) +dump("B7") + +-- __close 抛的错带位置前缀(level 1,函数内) +reset() +local function inner() + local a = mk("a", "err") + return 1 +end +show("B8_lvl1", pcall(inner)) +dump("B8") + +reset() +local function inner2() + local a = mk("a", "err") + error("orig", 2) +end +show("B9_unwind_lvl1", pcall(inner2)) +dump("B9") + +-- __close 返回多个值:C 的 callclosemethod 只取一个(nresults = 0) +reset() +show("B10_multi", pcall(function() + local a = mk("a", "multi") +end)) +dump("B10") + +-- __close 不是函数 / 是可调用表 / 没有 +reset() +show("B11_notfn", pcall(function() + local a = mk("a", "notfn") +end)) +dump("B11") + +reset() +show("B12_callable", pcall(function() + local a = mk("a", "callable") +end)) +dump("B12") + +reset() +show("B13_noclose", pcall(function() + local a = mk("a", "noclose") +end)) +dump("B13") + +reset() +show("B14_noclose_unwinding", pcall(function() + local a = mk("a", "noclose") + error("orig", 0) +end)) +dump("B14") + +-- 元表上的 __close 在关闭前被改掉 +reset() +show("B15_late_mt", pcall(function() + local t = setmetatable({}, { __close = function() show("close", "orig") end }) + local a = t + setmetatable(t, {}) +end)) +dump("B15") + +-- ==================== C. 与 pcall / xpcall 交互 ==================== + +reset() +show("C1_xpcall", xpcall(function() + local a = mk("a", "err") + return 1 +end, function(m) + show("msgh", m) + return "H" +end)) +dump("C1") + +reset() +show("C2_xpcall_unwind", xpcall(function() + local a = mk("a", "err") + error("orig", 0) +end, function(m) + show("msgh", m) + return "H" +end)) +dump("C2") + +reset() +show("C3_pcall_nested", pcall(function() + local a = mk("a", "log") + return pcall(function() + local b = mk("b", "err") + return 1 + end) +end)) +dump("C3") + +-- ==================== D. 协程 ==================== + +-- __close 里 yield +reset() +local co = coroutine.create(function() + local a = mk("a", "yield") + show("after yield point") + return "done" +end) +show("D1a", coroutine.resume(co)) +show("D1b", coroutine.resume(co)) +dump("D1") + +-- 协程被 close:挂起状态的协程里的 tbc 怎么关 +reset() +local co2 = coroutine.create(function() + local a = mk("a", "log") + coroutine.yield("mid") + return "done" +end) +show("D2a", coroutine.resume(co2)) +show("D2b", coroutine.close(co2)) +dump("D2") + +-- 挂起 + __close 抛错 +reset() +local co3 = coroutine.create(function() + local a = mk("a", "err") + coroutine.yield("mid") + return "done" +end) +show("D3a", coroutine.resume(co3)) +show("D3b", coroutine.close(co3)) +dump("D3") + +-- 出错的协程被 close +reset() +local co4 = coroutine.create(function() + local a = mk("a", "log") + error("coerr", 0) +end) +show("D4a", coroutine.resume(co4)) +show("D4b", coroutine.close(co4)) +show("D4c", coroutine.status(co4)) +dump("D4") + +-- 跑完的协程被 close(没有 tbc 要关) +reset() +local co5 = coroutine.create(function() + local a = mk("a", "log") + return "done" +end) +show("D5a", coroutine.resume(co5)) +show("D5b", coroutine.close(co5)) +dump("D5") + +-- 协程里 __close 抛错,协程结束后 close +reset() +local co6 = coroutine.create(function() + local a = mk("a", "err") + return "done" +end) +show("D6a", coroutine.resume(co6)) +show("D6b", coroutine.close(co6)) +show("D6c", coroutine.status(co6)) +dump("D6") + +-- 主协程里 coroutine.close(coroutine.running()):参考实现在这里直接抛, +-- 不包 pcall 的话脚本当场死在中间,后面整段 E 一条也不会比(用例照样绿) +reset() +show("D7", pcall(coroutine.close, coroutine.running())) +dump("D7") + +-- ==================== E. 语法/声明边界 ==================== + +local srcs = { + "local a ", + "local a = 1, 2", + "for i = 1, 2 do local a = nil end", + "local function f() end local a = f", + "local a , b = nil, 1", + "local a = setmetatable({}, {__close=function() end})", + "do local a = nil end", + "t = {} local a = t", + "local a = nil local b = a", +} +for i, s in ipairs(srcs) do + local f, e = load(s) + print("E" .. i, tostring(f) ~= "nil", tostring(e)) +end + +-- tbc 变量被闭包捕获:关的是 upvalue +reset() +local escaped +do + local a = setmetatable({}, { + __close = function(self, err) + show("close", "esc", "err=" .. tostring(err)) + escaped = self + end, + }) + local function cap() + return a + end + cap() +end +dump("E10_capture") + +-- 同一个值既是 tbc 又被赋给别处 +reset() +do + local t = mk("t", "log") + local a = t + show("same", tostring(a) == tostring(t)) +end +dump("E11") + +-- ==================== F. 协程里的 xpcall × __close ==================== +-- C 版 ldo.c: lua_pcallk 的可 yield 那条分支同样会写 L->errfunc,而且 +-- luaD_call 是 longjmp 出去的,errfunc 在 yield 期间一直是设着的 —— +-- 所以 finishpcallk 里跑 __close 时,消息处理函数依然生效。 + +reset() +local function cofn(msgh) + local co = coroutine.create(function() + return xpcall(function() + local a = mk("a", "log") + error("orig", 0) + end, msgh) + end) + show("F_a", coroutine.resume(co)) +end +cofn(function(m) + show("msgh", m) + return "H" +end) +dump("F1_coro_xpcall_log") + +reset() +local co = coroutine.create(function() + return xpcall(function() + local a = mk("a", "err") + error("orig", 0) + end, function(m) + show("msgh", m) + return "H" + end) +end) +show("F_a", coroutine.resume(co)) +dump("F2_coro_xpcall_closeerr") + +-- 协程里 xpcall 且 __close 里 yield +reset() +local co3 = coroutine.create(function() + return xpcall(function() + local a = mk("a", "yield") + error("orig", 0) + end, function(m) + show("msgh", m) + return "H" + end) +end) +show("F3a", coroutine.resume(co3)) +show("F3b", coroutine.resume(co3)) +show("F3c", coroutine.status(co3)) +dump("F3") + +-- 协程里 pcall(无 handler)+ __close 抛错 +reset() +local co4 = coroutine.create(function() + return pcall(function() + local a = mk("a", "err") + error("orig", 0) + end) +end) +show("F4", coroutine.resume(co4)) +dump("F4") + +-- 嵌套 xpcall:内层的 __close 抛错,外层的 handler 会不会看到 +reset() +show("F5", xpcall(function() + return xpcall(function() + local a = mk("a", "err") + error("inner", 0) + end, function(m) + show("inner msgh", m) + return "IH" + end) +end, function(m) + show("outer msgh", m) + return "OH" +end)) +dump("F5") + +-- (删掉「消息处理函数自己抛错」这一条:C 版会在 luaG_errormsg 里无限递归 +-- 到 C stack overflow,输出几百行、行数随栈深度而变,不可逐字节比。 +-- 那条路径由「error in error handling」的最终结果单独钉住,见下面的 G 段。) + +-- 两个 tbc,第一个的 __close 抛错、第二个的 __close 看到的 err 是什么 +reset() +show("F7", xpcall(function() + local a = mk("a", "err") + local b = mk("b", "log") + error("orig", 0) +end, function(m) + show("msgh", m) + return "H" +end)) +dump("F7") + +-- 两个 tbc 都在 __close 里抛错 +reset() +show("F8", pcall(function() + local a = mk("a", "err") + local b = mk("b", "err") + error("orig", 0) +end)) +dump("F8") +-- 消息处理函数抛错 → 最终结果是不是 "error in error handling"(不比过程) +reset() +show("G1", xpcall(function() + local a = mk("a", "err") + error("orig", 0) +end, function(m) + error("in msgh", 0) +end)) +dump("G1") + +-- ==================== H. 协程里「先 yield 再出错」:msgh 由谁调、调几次 ============ +local function mh(tag) + return function(m) + show("msgh", tag, m) + return "H-" .. tag + end +end + +reset() +local h1 = coroutine.create(function() + return xpcall(function() + coroutine.yield("Y1") + error("after", 0) + end, mh("h1")) +end) +show("H1a", coroutine.resume(h1)) +show("H1b", coroutine.resume(h1)) +dump("H1_yield_then_error") + +reset() +local h2 = coroutine.create(function() + return xpcall(function() + local a = mk("a", "log") + coroutine.yield("Y1") + error("after", 0) + end, mh("h2")) +end) +show("H2a", coroutine.resume(h2)) +show("H2b", coroutine.resume(h2)) +dump("H2_yield_then_error_tbc") + +reset() +local h3 = coroutine.create(function() + return pcall(function() + coroutine.yield("Y1") + error("after", 0) + end) +end) +show("H3a", coroutine.resume(h3)) +show("H3b", coroutine.resume(h3)) +dump("H3_pcall_yield_then_error") + +-- 出错之后 resume 回来看 __close:__close 收到的 err 是原始错还是 msgh 的结果 +reset() +local h4 = coroutine.create(function() + return xpcall(function() + local a = mk("a", "err") + coroutine.yield("Y1") + error("after", 0) + end, mh("h4")) +end) +show("H4a", coroutine.resume(h4)) +show("H4b", coroutine.resume(h4)) +show("H4c", coroutine.status(h4)) +dump("H4_close_err_after_resume") + """); + } + + /** + * 调试钩子矩阵:mask 的八种组合 × {@code count} × 各类帧(Lua / C / 尾调用 / + * 协程)× 钩子自身的各种行为(关自己、换自己、抛错、yield、读 {@code gethook})。 + *

+ * 钩子是本实现唯一「用 Java 异常打断 VM 正常流程」的地方,形状最容易走偏: + * 事件在 {@code luaD_precall} / {@code luaD_poscall} / {@code luaV_execute} + * 三处各发一次,而 {@code allowhook}、{@code nCcalls}、{@code ci} 在这些点上的 + * 状态都不一样。56 行可比输出(末尾那段回溯被 {@code assertMatches} 剥掉)。 + *

+ * 结果是逐字节一致,没有真分歧。但用例仍然收进来:反向验证过它敏感 + * —— 把事件名 {@code "tail call"} 改成 {@code "tailcall"},14 行立刻变红。 + * 从没红过的用例等于没证明任何东西,所以这条破坏是安装前必做的一步。 + *

+ * 两条踩出来、必须在写同类探针时记住的: + *

    + *
  • 「钩子抛错能不能被 {@code pcall} 接住」取决于钩子设在哪。 + * {@code sethook} 之后下一个 {@code call} 事件就是 + * {@code pcall} 自己({@code "c"} 掩码连 C 函数也算),而那一下发生在 + * {@code pcall} 建立保护之前,错会直接逃出去把脚本打死。实测 + * 参考实现报 {@code lua: hk1} + 回溯里 {@code [C]: in function 'pcall'}, + * 脚本到此为止。要测「接住」就必须把 {@code sethook} 放在 {@code pcall} + * 里面(第 6 段);「逃出去」那条只能放在脚本最末(第 14 段), + * 否则后面几十行一条也不会比 —— 而用例照样是绿的。
  • + *
  • 钩子里取栈要取到第 3 层。{@code who(2)} 拿到的是打印用的 + * helper,{@code who(3)} 才是 {@code namewhat = "hook"} 那一层, + * 与 {@code debugFrameShapesMatchReference} 的结论对得上。
  • + *
+ */ + @Test + void debugHookMatrixMatchesReference() { + Diff.assertMatches(""" +-- 调试钩子矩阵:mask 的八种组合 × count × 各类帧(Lua / C / 尾调用 / 协程) +-- × 钩子自身的各种行为(关自己、抛错、yield、再 getinfo)。 +-- 钩子是本实现唯一「用 Java 异常打断 VM 正常流程」的地方,形状最容易走偏。 + +local function show(...) + local a = { ... } + for i = 1, select("#", ...) do + a[i] = tostring(a[i]) + end + print(table.concat(a, " ")) +end + +-- 在钩子里取调用者那一层的信息。只打不带地址、不带顺序的字段。 +-- 层号:1 = getinfo 自己那层(是 C),2 = 钩子,3 = 被钩的那一帧 +local function who(lvl) + local ok, info = pcall(debug.getinfo, lvl, "nSlf") + if not ok then + return "ERR" + end + if info == nil then + return "nil" + end + local f = info.func + return string.format("%s/%s/%s/%s/%s", + tostring(info.name), tostring(info.namewhat), tostring(info.what), + tostring(info.currentline), type(f)) +end + +-- ==================== 1. 初始状态与 sethook 的返回值 ==================== +show("1a", debug.gethook()) +local h0 = function() end +debug.sethook(h0, "crl", 17) +do + local a, b, c = debug.gethook() + show("1b", type(a), b, c) +end +debug.sethook() +show("1c", debug.gethook()) + +-- 参数校验 +show("1d", pcall(debug.sethook)) +show("1e", pcall(debug.sethook, nil, "c")) +show("1f", pcall(debug.sethook, 42, "c")) +show("1g", pcall(debug.sethook, print)) +show("1h", pcall(debug.sethook, print, 42)) +show("1i", pcall(debug.sethook, print, "c", "x")) +show("1j", pcall(debug.sethook, print, "Q")) +do + local ok, err = pcall(debug.sethook, print, "c", -1) + show("1k", ok, type(err)) +end +debug.sethook() +show("1l", debug.gethook()) + +-- ==================== 2. 八种 mask 组合 × 一小段调用 ==================== +local function target(n) + if n <= 0 then + return "end" + end + local x = n * 2 + return target(n - 1) +end + +local function runMask(mask, count) + local log = {} + local function h(what, line) + log[#log + 1] = tostring(what) .. "@" .. tostring(line) + end + debug.sethook(h, mask, count or 0) + local ok, res = pcall(target, 2) + debug.sethook() + show("2 mask=" .. mask .. " count=" .. tostring(count), ok, res, table.concat(log, ",")) +end + +for _, mask in ipairs({ "", "c", "r", "l", "cr", "cl", "rl", "crl" }) do + runMask(mask, 0) +end +runMask("", 1) +runMask("", 2) +runMask("l", 3) +runMask("cr", 5) + +-- ==================== 3. 尾调用:有没有 "tail call" 事件 ==================== +local function tail2() + return 1 +end +local function tail1() + return tail2() +end +do + local log = {} + debug.sethook(function(what) + log[#log + 1] = tostring(what) + end, "cr") + local r = tail1() + debug.sethook() + show("3_tail", r, table.concat(log, ",")) +end + +-- ==================== 4. C 函数的 call / return ==================== +do + local log = {} + debug.sethook(function(what) + log[#log + 1] = tostring(what) + end, "cr") + local r = string.rep("a", 3) + debug.sethook() + show("4_cfunc", r, table.concat(log, ",")) +end + +-- ==================== 5. 钩子里再关钩子 / 再设钩子 ==================== +do + local log, n = {}, 0 + debug.sethook(function(what) + n = n + 1 + log[#log + 1] = tostring(what) + debug.sethook() -- 第一个事件之后就关掉 + end, "c") + target(1) + debug.sethook() + show("5a_disable_in_hook", n, #log > 0) +end +do + local n = 0 + debug.sethook(function() + n = n + 1 + if n == 1 then + debug.sethook(function() end, "c") -- 换一个空钩子 + end + end, "cr") + target(1) + debug.sethook() + show("5b_swap_in_hook", n > 0) +end + +-- ==================== 6. 钩子里抛错 ==================== +-- 钩子必须设在 pcall 内部才能测「接住」:sethook 之后下一个 call +-- 事件就是 pcall 自己("c" 掩码连 C 函数也算),那一下发生在 pcall 建立保护 +-- 之前,错会直接逃出去把脚本打死。实测参考实现就是这样: +-- "lua: hookerr" + 回溯里 [C]: in function 'pcall',脚本到此为止。 +-- 所以「钩子在第一个事件上抛错」那条单独放到脚本最末(第 14 段)。 +show("6a_inside_pcall", pcall(function() + debug.sethook(function(what, line) + debug.sethook() + error("inside", 0) + end, "l") + local a = 1 + local b = 2 + return a + b +end)) +show("6b_cleared", debug.gethook()) +show("6c_nonstring", pcall(function() + debug.sethook(function() + debug.sethook() + error({ c = 1 }, 0) + end, "l") + local a = 1 + return a +end)) +-- 钩子抛错后 debug.gethook 还在不在(钩子自己关掉了自己) +show("6d", debug.gethook()) + +-- ==================== 7. 钩子里 yield(已在协程矩阵里量过,这里补「行钩子」)======== +do + local co = coroutine.create(function() + debug.sethook(function() + debug.sethook() + coroutine.yield("from hook") + end, "l") + local x = 1 + return x + end) + show("7a", coroutine.resume(co)) + show("7b", coroutine.resume(co)) +end + +-- ==================== 8. 协程的钩子是各自的 ==================== +do + local log = {} + local function h(what) + log[#log + 1] = "main:" .. tostring(what) + end + debug.sethook(h, "c") + local co = coroutine.create(function() + return target(1) + end) + local ok, res = coroutine.resume(co) + debug.sethook() + show("8a_main_hook_not_inherited", ok, res, table.concat(log, ",")) +end +-- 给指定线程设钩子 +do + local log = {} + local co = coroutine.create(function() + return target(1) + end) + debug.sethook(co, function(what) + log[#log + 1] = tostring(what) + end, "cr") + local ok, res = coroutine.resume(co) + local a, b, c = debug.gethook(co) + debug.sethook(co) + show("8b_thread_hook", ok, res, table.concat(log, ","), type(a), b, c) + show("8c_after_clear", debug.gethook(co)) +end +-- 主线程的 gethook(coroutine.running()) +show("8d", pcall(debug.gethook, coroutine.running())) + +-- ==================== 9. 行钩子报的行号序列 ==================== +do + local log = {} + local function f() + local a = 1 + local b = 2 + local c = a + b + return c + end + debug.sethook(function(what, line) + log[#log + 1] = tostring(line) + end, "l") + local r = f() + debug.sethook() + show("9_lines", r, table.concat(log, ",")) +end + +-- ==================== 10. 钩子里取各层信息 ==================== +do + local log = {} + debug.sethook(function(what) + log[#log + 1] = tostring(what) .. ":" .. who(2) .. ":" .. who(3) + end, "c") + local r = target(1) + debug.sethook() + for i, s in ipairs(log) do + show("10_" .. i, s) + end +end + +-- ==================== 11. count 钩子在循环里 ==================== +do + local n = 0 + debug.sethook(function() + n = n + 1 + end, "", 1) + local s = 0 + for i = 1, 20 do + s = s + i + end + debug.sethook() + show("11_count1", s, n > 0) +end + +-- ==================== 12. 钩子开着的开销之外:钩子期间不再触发钩子 ============ +do + local n = 0 + debug.sethook(function() + n = n + 1 + -- 钩子内再调一个会触发钩子的调用,不应再进来 + local _ = string.rep("x", 1) + end, "c") + local _ = string.rep("y", 1) + debug.sethook() + show("12_no_reentrance", n) +end + +-- ==================== 13. 钩子里改 mask / 读 gethook ==================== +do + local log = {} + debug.sethook(function() + local f, m, c = debug.gethook() + log[#log + 1] = type(f) .. "/" .. tostring(m) .. "/" .. tostring(c) + end, "c") + target(1) + debug.sethook() + show("13", table.concat(log, ",")) +end + +-- ==================== 14. 钩子在第一个事件上抛错:逃出 pcall,脚本到此为止 === +-- 必须放在最后:这一条会让参考实现直接死掉,后面任何输出都不会有。 +show("14_before") +debug.sethook(function(what) + debug.sethook() + error("hk1", 0) +end, "c") +show("14_unreachable", pcall(function() + return 1 +end)) + """); + } + + /** + * 类型元表矩阵:{@code debug.setmetatable} / {@code debug.getmetatable} + * 对每种类型怎么分派。 + *

+ * {@code lapi.c: lua_setmetatable} 按类型分派到三处:表 → 自己的字段、 + * userdata → 自己的字段、其余 → {@code global_State} 的 {@code mt[]} + * (整个类型共享一张)。三条路都要量,而且要用八种类型逐个量: + * 之前 {@code LuaTm.setMetatable} 的 userdata 分支一直是 {@code nc}, + * 因为脚本里根本碰不到——{@code setmetatable} 的第一个参数必须是表 + * ({@code luaL_checktype(L, 1, LUA_TTABLE)}),userdata 只能走 + * {@code debug.setmetatable},而套件里从来没人那么写过。48 行。 + *

+ * 逐字节一致,没有真分歧。敏感性量过:把 userdata 那个分支删掉一句,6 行变红。 + *

+ * 顺带把两条容易被写错的东西钉住: + *

    + *
  • 只有表与完整 userdata 认 {@code __name}。给数字 / nil / + * 布尔 / 函数 / 线程 / 字符串设了带 {@code __name} 的类型元表,报错里 + * 依然是 {@code number} / {@code nil} / {@code boolean} 等原名; + * 只有 userdata 变成 {@code attempt to index a M_ud value}。
  • + *
  • 字符串的类型元表就是 string 库那张,换掉它 {@code ("x"):upper()} + * 立刻失效。所以第 9 段必须在脚本最前面把原表存下来再还原—— + * 第 3 段一上来就给每种类型都设了一张空元表,那时再存就已经晚了。
  • + *
+ */ + @Test + void typeMetatableDispatchMatchesReference() { + Diff.assertMatches(""" +-- 类型元表矩阵:debug.setmetatable / debug.getmetatable 对每种类型怎么分派。 +-- lapi.c: lua_setmetatable 按类型分派到三处:表 -> 自己的字段、userdata -> 自己的 +-- 字段、其余 -> global_State 的 mt[](整个类型共享一张)。这三条路都要量。 +-- __name 是唯一能在报错里看见类型元表的窗口,所以每条都顺带看一眼报错名。 + +local function show(...) + local a = { ... } + for i = 1, select("#", ...) do + a[i] = tostring(a[i]) + end + print(table.concat(a, " ")) +end + +-- 字符串的类型元表(就是 string 库那张)必须在最前面存好:第 3 段会给 +-- 每种类型都设一张带 __name 的空元表,那时它已经被顶掉了,再存就存了个空的。 +local savedStrMt = debug.getmetatable("") + +-- 每个类型取一个代表值。名字里带 NUL 的、地址类的都不打。 +local f = io.open("/dev/null", "r") +local th = coroutine.create(function() end) +local vals = { + { "nil", nil }, + { "bool", true }, + { "num", 42 }, + { "str", "s" }, + { "tbl", {} }, + { "fn", print }, + { "ud", f }, + { "thr", th }, +} + +-- ==================== 1. 初始 getmetatable ==================== +for _, v in ipairs(vals) do + local mt = debug.getmetatable(v[2]) + show("1_" .. v[1], type(mt), mt ~= nil and (rawget(mt, "__name") or "-") or "-") +end + +-- ==================== 2. setmetatable 的参数校验 ==================== +show("2a", pcall(debug.setmetatable)) +show("2b", pcall(debug.setmetatable, 1)) +show("2c", pcall(debug.setmetatable, 1, 5)) +show("2d", pcall(debug.setmetatable, 1, "x")) +show("2e", pcall(debug.setmetatable, 1, nil)) +show("2f", pcall(debug.setmetatable, nil, nil)) + +-- ==================== 3. 给每个类型设元表,再看 getmetatable ==================== +for _, v in ipairs(vals) do + local ok, res = pcall(debug.setmetatable, v[2], { __name = "M_" .. v[1] }) + local mt = debug.getmetatable(v[2]) + show("3_" .. v[1], ok, type(res), type(mt), mt ~= nil and tostring(rawget(mt, "__name")) or "-") +end + +-- ==================== 4. __name 能不能改报错里的类型名 ==================== +-- 注意:只有「表与完整 userdata 认 __name」(lobject.c: luaT_objtypename 的判据), +-- 其余类型连类型元表都不看。这里逐类型实测。 +local function probe(name, v, body) + local ok, err = pcall(body) + show("4_" .. name, ok, err) +end +probe("num", 42, function() + local x = 42 + return x.y +end) +probe("nil", nil, function() + local x + return x.y +end) +probe("bool", true, function() + return (true).y +end) +probe("fn", print, function() + return (print).y +end) +probe("thr", th, function() + return th.y +end) +probe("ud", f, function() + return f.y +end) +probe("str", "s", function() + return ("s").y +end) + +-- ==================== 5. 类型元表真的被共享 ==================== +do + debug.setmetatable(1, { __name = "SHARED" }) + local a = debug.getmetatable(1) + local b = debug.getmetatable(2) + local c = debug.getmetatable(3.5) + show("5_shared", a == b, tostring(rawget(a, "__name")), tostring(b ~= nil and rawget(b, "__name") or "-"), + tostring(c ~= nil and rawget(c, "__name") or "-")) + debug.setmetatable(1, nil) + show("5_cleared", debug.getmetatable(1)) +end + +-- ==================== 6. 整数与浮点是同一个类型 ==================== +do + debug.setmetatable(1, { __name = "NUMX" }) + show("6a", debug.getmetatable(1) == debug.getmetatable(1.0)) + debug.setmetatable(1, nil) +end + +-- ==================== 7. userdata 的元表是「对象自己的」 ==================== +do + local g = io.open("/dev/null", "r") + debug.setmetatable(f, { __name = "F1" }) + debug.setmetatable(g, { __name = "F2" }) + show("7a", tostring(rawget(debug.getmetatable(f), "__name")), + tostring(rawget(debug.getmetatable(g), "__name"))) + -- io.type 还认不认(认的是注册表里 FILEHANDLE* 那张,不是对象自己的) + show("7b", io.type(f), io.type(g)) + -- 换成完全空的元表:__index 没了,方法也就没了 + debug.setmetatable(f, {}) + show("7c", io.type(f), pcall(function() return f.close end)) + -- 还原 + debug.setmetatable(f, nil) + -- 元表清掉之后 :close 也没了(__index 跟着没了),所以这里不能再关; + -- 句柄指向 /dev/null,漏掉无妨。实测参考实现在这里报 + -- "attempt to index a userdata value (local 'f')" 并终止脚本。 + show("7d", io.type(f), type(debug.getmetatable(f))) +end + +-- ==================== 8. 表的元表不受 debug.setmetatable 影响分派 ============ +do + local t = {} + show("8a", debug.getmetatable(t)) + debug.setmetatable(t, { __name = "TT" }) + show("8b", tostring(rawget(debug.getmetatable(t), "__name"))) + show("8c", setmetatable(t, { __name = "TT2" }) == t) + show("8d", tostring(rawget(debug.getmetatable(t), "__name"))) + debug.setmetatable(t, nil) + show("8e", debug.getmetatable(t)) +end + +-- ==================== 9. 字符串类型元表(会顶掉 string 库,放最后)============ +do + local saved = savedStrMt + show("9a", type(saved), tostring(saved ~= nil and rawget(saved, "__index") ~= nil)) + debug.setmetatable("", { __name = "STRX" }) + show("9b", pcall(function() return ("x").y end)) + show("9c", pcall(function() return ("x"):upper() end)) + -- 还原,后面的代码才不会炸 + debug.setmetatable("", saved) + show("9d", ("x"):upper()) +end + +-- ==================== 10. 协程的元表 ==================== +do + local co = coroutine.create(function() end) + debug.setmetatable(co, { __name = "COX" }) + show("10a", tostring(rawget(debug.getmetatable(co), "__name"))) + show("10b", pcall(function() return co.y end)) + debug.setmetatable(co, {}) + show("10c", tostring(rawget(debug.getmetatable(co), "__name"))) +end + """); + } + + /** + * {@code package} 库的路径搜索/搜索器/{@code loadlib} 矩阵,与参考实现逐行比对。 + *

+ * 覆盖的是 {@code loadlib.c} 里一段先前从来没跟参考实现比过的逻辑: + * {@code searchpath} 怎么切模板、{@code require} 怎么把四个搜索器的消息 + * 拼成一条、{@code loadlib} 的第三个返回值怎么定。 + *

+ * 注意它没有抬覆盖率(跑完 {@code PackageLib} 仍是 0.891、miss 仍是 + * 117):这些行本来就被别的用例走到了,只是走完没有比对。覆盖率 + * 报告看不出这件事——它是这个矩阵存在的理由,也是「覆盖率涨了才算补上了」 + * 这个直觉的反例。{@code PackageLib} 剩下的 18 行里,8 行是 + * {@code setpath}({@code LUA_PATH} 里的 {@code ";;"}),只能靠环境变量喂, + * 塞不进这套差分驱动,已另用 {@code env} 逐个量过(九种形状逐字节一致); + * 其余 10 行要真 {@code dlopen},本实现没有。 + *

+ * 三条容易被踩的: + *

    + *
  • 模板的切分看不见,但消息看得见。错误消息是拿未切分的 + * 整条 {@code path} 拼的({@code pusherrornotfound}),不是拿切出来的 + * 模板表拼的——所以末尾分号多出的那一项空模板在消息里照样出现, + * 而它 {@code readable("")} 必为假,两边都找得到。
  • + *
  • 四个参数全是 {@code const char*},在第一个 {@code NUL} 处砍断。 + * {@code searcher_C} / {@code searcher_Croot} 原先漏了这一步,实测 + * (参考二进制)四处对不上。
  • + *
  • {@code loadlib} 的第三个返回值有两个常量,不是一个。库打不开是 + * {@code LIB_FAIL}(启用 dlopen 的构建里是 {@code "open"}),库能打开 + * 而符号找不到是写死的 {@code "init"}——兜底分支只重定义了前者。
  • + *
+ * 比对里不含 {@code dlerror()} 的文本:它带地址,逐字节比不了。 + * K 节因此只比 {@code "\n\t"} 之前那一截。 + */ + @Test + void packageSearchPathAndSearchersMatchReference() { + Diff.assertMatches(""" +-- package 库的路径搜索 / 搜索器 / loadlib 矩阵 +-- 靠 "no file 'x'\\n\\tno file 'y'" 这条错误消息把切出来的模板逐个暴露出来 +local Z = string.char(0) -- 不用字面量 NUL,免得在 Java 文本块里被当成转义 + +local function sp(name, path, sep, dirsep) + local ok, r, e = pcall(package.searchpath, name, path, sep, dirsep) + if not ok then + return "ERR " .. tostring(r) + end + if r then + return "FOUND " .. tostring(r) + end + -- e 是 "no file 'a'\\n\\tno file 'b'",压成一行好看 + return (tostring(e):gsub("\\n\\t", " | ")) +end + +-- A. 纯模板切分:name 用不含 '?' 也不存在的 N +local paths = { + "", ";", ";;", ";;;", ";;;;", + "a", "a;", ";a", ";a;", "a;b", "a;b;", "a;;b", "a;;", + ";;b", "a;;b;;c", "a;;b;c", "a;b;;c", ";;a;;", +} +for i, p in ipairs(paths) do + print(string.format("A%02d [%s] -> %s", i, p, sp("N", p))) +end + +-- B. '?' 替换 +local qs = { "?", "?;?", "?-?", "a?b", "?", "x/?/y" } +for i, p in ipairs(qs) do + print(string.format("B%02d [%s] -> %s", i, p, sp("N", p))) +end + +-- C. name 里带 '?':是被当成字面量填进模板 +print("C01 " .. sp("a?b", "?")) +print("C02 " .. sp("a?b", "?-?")) + +-- D. sep / dirsep +print("D01 " .. sp("a.b", "?", ".", "/")) +print("D02 " .. sp("a.b", "?", "", "/")) -- sep 空:不替换 +print("D03 " .. sp("a.b", "?", ".", "")) -- dirsep 空:'.' 被删掉 +print("D04 " .. sp("a.b", "?", ".b", "-")) -- sep 多字符:整段替换 +print("D05 " .. sp("a.b", "?", "b", "Z")) -- sep 命中 name 尾部 +print("D06 " .. sp("a.b", "?", "z", "Z")) -- sep 没命中:不动 +print("D07 " .. sp("a.b", "?", "..", "+")) -- sep 比 name 长 +print("D08 " .. sp("a..b", "?", ".", "-")) -- 两个都换 + +-- E. NUL:四个参数全都是 const char*,在第一个 NUL 处砍断 +print("E01 " .. sp("a" .. Z .. "b", "?")) +print("E02 " .. sp("a" .. Z .. "b", "?-?")) +print("E03 " .. sp("N", "a" .. Z .. "b;c")) +print("E04 " .. sp("N", "?", "." .. Z .. "x", "-")) + +-- F. searchpath 参数检查 +local function r2(tag, f, ...) + local ok, a, b = pcall(f, ...) + if ok then + print(string.format("%s OK %s | %s", tag, tostring(a), tostring(b))) + else + print(string.format("%s ERR %s", tag, tostring(a))) + end +end +r2("F01", package.searchpath) -- 缺 1 +r2("F02", package.searchpath, "N") -- 缺 2 +r2("F03", package.searchpath, 42, "?") -- 数字当 name +r2("F04", package.searchpath, "N", 42) -- 数字当 path +r2("F05", package.searchpath, true, "?") -- boolean name +r2("F06", package.searchpath, "N", true) -- boolean path +r2("F07", package.searchpath, "N", "?", 42) -- 数字 sep +r2("F08", package.searchpath, "N", "?", ".", 42) -- 数字 dirsep +r2("F09", package.searchpath, "N", nil) -- nil path +r2("F10", package.searchpath, nil, "?") -- nil name + +-- G. 找到的情况:真存在的文件 +local f = io.open("pk_found.txt", "w") +f:write("x") +f:close() +print("G01 " .. sp("pk_found", "?.txt")) +print("G02 " .. sp("pk_found", "nope;?.txt")) +print("G03 " .. sp("pk_found", "nope;?.txt;more")) +print("G04 " .. sp("pk_found", "?.txt;?.txt")) +-- 目录也算"能打开":C 的 fopen 在目录上是成功的,读的时候才失败 +-- 用绝对路径,免得两边 cwd 不一致变成假红 +print("G05 " .. sp("usr", "/?")) +print("G06 " .. sp("etc", "/?")) +print("G07 " .. sp("nosuchdirxyz", "/?")) + +-- H. require 的消息拼装:四个搜索器都失败 +local SAVE_SEARCHERS = package.searchers -- J、K 两节要用真搜索器 +package.path = "P1/?.lua;P2/?.lua" +package.cpath = "C1/?.so;C2/?.so" +r2("H01", require, "nomod") +r2("H02", require, "nomod.sub") +r2("H03", require, "nomod" .. Z .. "zzz") -- NUL 在 require 那一层就砍断 +r2("H04", require, 42) -- 数字模块名 +package.searchers = { function(n) return 42 end } -- 返回数字也算消息 +r2("H05", require, "nomod") +package.searchers = { function(n) return true end } -- 不是字符串:整条丢掉 +r2("H06", require, "nomod") +package.searchers = { function(n) return nil end } +r2("H07", require, "nomod") +package.searchers = { function(n) return "boom" end } +r2("H08", require, "nomod") +package.searchers = {} +r2("H09", require, "nomod") +package.searchers = 42 +r2("H10", require, "nomod") +package.searchers = { 42 } +r2("H11", require, "nomod") +-- 搜索器返回 (f, data):data 会原样传给 loader +package.searchers = { + function(n) return function(m, d) return "got:" .. m .. ":" .. tostring(d) end, "DATA" end +} +r2("H12", require, "m1") +-- 返回值 nil 也算"已加载",true 会被塞进 package.loaded +package.searchers = { function(n) return function() return nil end end } +r2("H13", require, "m2") +print("H14 " .. tostring(package.loaded.m2)) +-- 加载两次:第二次直接取缓存,搜索器不该再被调用 +package.searchers = { function(n) return function() return "fresh" end end } +r2("H15", require, "m3") +package.searchers = { function(n) error("should not be called", 0) end } +r2("H16", require, "m3") +-- package.loaded 里预先塞值:false 不算已加载(判的是 toboolean) +package.loaded["m4"] = "PRESET" +r2("H17", require, "m4") +package.loaded["m5"] = false +r2("H18", require, "m5") + +-- I. package.loadlib:第二个返回值的文本是 dlerror() 的,平台相关, +-- 所以只比"返回值个数"和第三个值(判据是失败发生在哪一步) +local function ll(tag, ...) + local ok, a, b, c = pcall(package.loadlib, ...) + if not ok then + print(string.format("%s ERR %s", tag, tostring(a))) + else + print(string.format("%s OK [%s] [%s]", tag, tostring(a), tostring(c))) + end +end +ll("I01", "/nope/x.so", "luaopen_y") +ll("I02", "x.so", "luaopen_y") +ll("I03", "/nope/x.so", "*") +ll("I04", 42, "luaopen_y") +ll("I05", "/nope/x.so", 42) +ll("I06", true, "x") +ll("I07", "/nope/x.so", true) +ll("I08", {}, "x") +ll("I09", "/nope/x.so", {}) +ll("I10") +ll("I11", "/nope/x.so") +ll("I12", nil, "x") +print("I13 " .. tostring(select('#', package.loadlib("/nope/x.so", "luaopen_y")))) +print("I14 " .. tostring(select(3, package.loadlib("/nope/x.so", "luaopen_y")))) +print("I15 " .. tostring(select(3, package.loadlib("/nope/x.so", "*")))) +print("I16 " .. tostring(select(1, package.loadlib("/nope/x.so", "luaopen_y")))) + +-- J. 直接调四个搜索器 +local jn = 0 +local function se(...) + jn = jn + 1 + local args = { ... } + local i = table.remove(args, 1) + local ok, a, b = pcall(package.searchers[i], table.unpack(args)) + local nm = tostring(args[1]):gsub("%z", "") + if not ok then + print(string.format("J%02d s%d [%s] ERR %s", jn, i, nm, tostring(a))) + else + print(string.format("J%02d s%d [%s] OK [%s] [%s]", jn, i, nm, + tostring(a), tostring(b):gsub("%z", ""))) + end +end +package.searchers = SAVE_SEARCHERS +package.path = "zz/?.lua" +package.cpath = "C/?.so" +for _, n in ipairs({ "x", "a.b", "a-b", 42, + "a" .. Z .. "b", "a" .. Z .. "b.c", "a.b" .. Z .. "c" }) do + for i = 1, 4 do + se(i, n) + end +end +se(1) -- 缺参数 +se(2, true) -- boolean +se(3, true) +se(4, true) +se(3, "x", "extra") -- 多余参数 +package.cpath = true -- cpath 不是字符串 +se(3, "x") +package.cpath = 42 -- 数字算字符串 +se(3, "x") +package.cpath = {} +se(3, "x") +package.cpath = nil +se(3, "x") +package.cpath = "C/?.so" +package.path = true +se(2, "x") +package.path = 42 +se(2, "x") + +-- K. C 搜索器真找到文件(文本文件,不是 dylib):"\\n\\t" 之后是 dlerror 的文本, +-- 平台相关,只比前面那一截 +package.searchers = SAVE_SEARCHERS +package.path = "zz/?.lua" +package.cpath = "?.so" +local cf = io.open("pk_c.so", "w") +cf:write("not a dylib\\n") +cf:close() +local function req(tag, n) + local ok, r = pcall(require, n) + if ok then + print(string.format("%s OK %s", tag, tostring(r))) + else + local head = tostring(r):match("^(.-)\\n\\t") + print(string.format("%s ERR %s", tag, head or tostring(r))) + end +end +req("K01", "pk_c") +req("K02", "pk_c.sub") +req("K03", "pk_c-sub") +req("K04", "pk_nosuch") + +os.remove("pk_found.txt") +os.remove("pk_c.so") + """); + } + + /** + * {@code io} 的边角矩阵:{@code read(0)}、大块写、管道上定位、目录、 + * {@code io.lines()} 无参。与参考实现逐行比对。 + *

+ * 挑的都是 {@code IoLib.LuaFile} 里覆盖率最薄的那几条(整类 0.801), + * 而且每一条都对应一条 stdio 的规则,不是随手挑的: + *

    + *
  • {@code f:read(0)}:{@code fread} 要 0 个字节返回 0,算读到了 + * —— 非 EOF 处给空串,EOF 处给 {@code nil}。写那边同理(空串往只读 + * 文件里写是成功的),两边是同一条规则的两半。
  • + *
  • 大块写绕开缓冲:写缓冲是 8192,超过它的整块直接落盘。所以 + * 10000 / 8192 / 8191 三种长度要各写一次,再读回来验位置。
  • + *
  • 管道上的 {@code seek} 是 {@code ESPIPE}(29),不是随便一个 + * IO 错。{@code io.popen} 出来的文件走这一条。
  • + *
  • 目录:{@code fopen} 成功、读失败({@code EISDIR} 21)、 + * {@code seek} 又是另一套(位置照算,负位置才是 {@code EINVAL} 22)。
  • + *
  • {@code io.lines()} 无参取的是注册表里的默认输入;把它关掉之后 + * 再要,报的是 {@code attempt to use a closed file} 而不是 + * {@code no input file} —— 注册表里那一项还在,只是已经关了。
  • + *
+ * 不要在这个矩阵里对 {@code io.stdout} / {@code io.stderr} 做 {@code seek}。 + * 输出被重定向成文件时那条 {@code seek("set", 0)} 是成功的, + * 于是写指针回到 0,后面每一行都从头覆盖 —— 参考实现的输出会变成一坨 + * 乱序的残片,而它看起来像是「实现有 bug」。重定向到管道时才是 + * {@code ESPIPE}。这条依赖的是运行环境,不是 Lua 语义。 + */ + @Test + void ioFileCornerCasesMatchReference() { + Diff.assertMatches(""" +-- io 的边角:read(0) / 大块写 / 管道上定位 / 目录 / io.lines 无参 +-- 注意:不要对 io.stdout / io.stderr 做 seek —— 输出被重定向成文件时那条 +-- seek 会成功并把写指针归零,把已经打印的内容整片覆盖掉(实测踩过)。 +local function p(tag, f, ...) + local ok, a, b, c = pcall(f, ...) + if not ok then + print(string.format("%s ERR %s", tag, tostring(a))) + else + local t = { tostring(a), tostring(b), tostring(c) } + print(string.format("%s %s | %s | %s", tag, t[1], t[2], t[3])) + end +end + +-- A. read(0):C 的 fread 要 0 个字节,返回 0 +local f = io.open("io_t.txt", "w") +f:write("hello\\nworld\\n") +f:close() +local g = io.open("io_t.txt", "r") +p("A01", g.read, g, 0) +print("A02 [" .. tostring(g:read(0)) .. "]") +print("A03 [" .. tostring(g:read(3)) .. "]") +p("A04", g.read, g, 0) -- 非 EOF 处再读 0 +print("A05 [" .. tostring(g:read("a")) .. "]") +p("A06", g.read, g, 0) -- EOF 处读 0 +p("A07", g.read, g, 1) -- EOF 处读 1 +p("A08", g.read, g, "l") +p("A09", g.read, g, "n") +g:close() + +-- B. 大块写:超过写缓冲(8192)就绕开缓冲直接落盘 +local w = io.open("io_big.txt", "w") +p("B01", w.write, w, string.rep("x", 10000)) +p("B02", w.write, w, string.rep("y", 8192)) +p("B03", w.write, w, string.rep("z", 8191)) +p("B04", w.write, w, "", "tail") +p("B05", w.write, w, "") +w:close() +local rb = io.open("io_big.txt", "rb") +local all = rb:read("a") +print("B06 " .. #all .. " " .. tostring(all:sub(1, 1)) .. tostring(all:sub(10000, 10000)) + .. tostring(all:sub(10001, 10001)) .. tostring(all:sub(18192, 18192)) + .. tostring(all:sub(18193, 18193)) .. tostring(all:sub(26387, 26387))) +rb:close() + +-- C. 管道:seek / tell 走 ESPIPE(29) +local pp = io.popen("echo hi") +print("C01 [" .. tostring(pp:read("a")) .. "]") +p("C02", pp.seek, pp, "set", 0) +p("C03", pp.seek, pp, "cur") +p("C04", pp.close, pp) +local pp2 = io.popen("nosuchcmd_xyz 2>/dev/null; exit 3") +print("C05 [" .. tostring(pp2:read("a")) .. "]") +p("C06", pp2.close, pp2) + +-- D. 目录:打开成功、读失败、seek 走另外一套 +local d, e1, e2 = io.open("/usr", "r") +print("D01 " .. type(d) .. " | " .. tostring(e1) .. " | " .. tostring(e2)) +if d then + p("D02", d.read, d, 1) + p("D03", d.read, d, "a") + p("D04", d.read, d, 0) + p("D05", d.read, d, "l") + p("D06", d.seek, d, "set", 0) + p("D07", d.seek, d, "cur", 0) + p("D08", d.seek, d, "set", -1) + p("D09", d.write, d, "x") + p("D10", d.flush, d) + p("D11", d.close, d) +end + +-- E. io.lines() 无参:用的是注册表里的默认输入 +io.input("io_t.txt") +print("E01 " .. tostring(type(io.lines()))) +local n = 0 +for _ in io.lines() do + n = n + 1 +end +print("E02 " .. n) +local inp = io.input() +print("E03 " .. tostring(io.close(inp))) +local function q(tag, f, ...) + local ok, a = pcall(f, ...) + print(string.format("%s %s | %s", tag, tostring(ok), tostring(a))) +end +q("E04", io.lines) +q("E05", function() for _ in io.lines() do end end) +q("E06", io.lines, nil) +q("E07", io.input, inp) +q("E10", io.read, "l") +q("E11", io.input, "nosuch_file_xyz") +-- 标准文件不许关 +p("E08", io.close, io.stdin) +p("E09", io.close, io.stdout) + +os.remove("io_t.txt") +os.remove("io_big.txt") + """); + } + + /** + * errno 形状表:{@code io.open} 的各种失败 × {@code os.rename} × + * {@code os.remove}。与参考实现逐行比对。 + *

+ * 这个文件操作的 errno 不是「存在不存在」一个维度,而是由目标的形状 + * 决定的,一共五个值:{@code ENOENT}(2)、{@code EACCES}(13)、 + * {@code ENOTDIR}(20)、{@code EISDIR}(21)、{@code EINVAL}(22),外加 + * {@code ENOTEMPTY} 和 {@code EPERM}(1)。本实现原先只按异常类型判, + * 认不出的全落进 {@code ENOENT} / {@code EIO} 的兜底 —— 五条全错。 + *

+ * 修法是反查而不是另列一张表:errno 与 {@code strerror} 是一对逆函数, + * 而 Java 的 IO 异常把 libc 的 {@code strerror} 原文带在消息里 + * ({@code FileNotFoundException} 是 {@code " (Permission denied)"}, + * {@code FileSystemException} 的 {@code getReason()} 直接是 + * {@code "Not a directory"}),所以拿 {@code LuaAux.strError} 那张表反查即可。 + *

+ * 两个写探针时的坑,都写在这儿的注释里免得再踩: + *

    + *
  • 素材放子目录,收尾整个删掉,别在仓库根目录建临时文件;
  • + *
  • 不要用 {@code /etc/hosts} 这种真系统文件试 {@code os.remove} + * —— 一旦以 root 跑就真删掉了。只读权限那条用自己建的 + * {@code chmod 555} 目录。
  • + *
+ */ + @Test + void errnoShapesMatchReference() { + Diff.assertMatches(""" +-- errno 形状表:io.open 的各种失败 × os.rename × os.remove +-- 素材全放在子目录里,收尾整个删掉,不在仓库根目录留东西 +local D = "au_tmp/" +local function p(tag, f, ...) + local ok, a, b, c = pcall(f, ...) + if not ok then + print(string.format("%s ERR %s", tag, tostring(a))) + else + print(string.format("%s %s | %s | %s", tag, tostring(a), tostring(b), tostring(c))) + end +end + +os.execute("rm -rf au_tmp && mkdir -p au_tmp/d au_tmp/e") +local w = io.open(D .. "f.txt", "w") +w:write("f") +w:close() +local w2 = io.open(D .. "g.txt", "w") +w2:write("g") +w2:close() +local w3 = io.open(D .. "d/inner", "w") +w3:write("i") +w3:close() + +-- A. io.open:目标的「形状」决定 errno,不只是"存在不存在" +for _, m in ipairs({ "r", "w", "a", "r+", "w+", "a+", "rb", "wb" }) do + p("A[" .. m .. "]", io.open, D .. "d", m) -- 目标是目录 +end +p("A01", io.open, D .. "nodir/x.txt", "r") -- 父目录不存在 +p("A02", io.open, D .. "nodir/x.txt", "w") +p("A03", io.open, D .. "f.txt/y", "r") -- 父是文件 -> ENOTDIR +p("A04", io.open, D .. "f.txt/y", "w") +p("A05", io.open, "/etc/hosts", "w") -- 没写权限 -> EACCES +p("A06", io.open, "/etc/hosts", "r") +p("A07", io.open, "", "r") -- 空路径 +p("A08", io.open, D .. "f.txt", "rw") -- 非法模式 +p("A09", io.open, D .. "f.txt", "x") +p("A10", io.open, "/etc", "w") -- 目录 + 写 -> EISDIR +-- A11 原来是 io.open("/dev/null", "w"):那是设备文件,行为由内核 + libc +-- 定,Linux 与 macOS 不一样(而且 JVM 在这上面给出的 errno 也不跟着 C 走), +-- 不进差分表。 +p("A12", io.open, "/etc/hosts", "a") -- 追加也要写权限 +p("A13", io.open, "/etc/hosts", "r+") + +-- B. os.rename:C 的 rename 有一张形状表,Java 的 Files.move 只对上一半 +p("B01", os.rename, D .. "f.txt", D .. "g.txt") -- 文件 -> 已存在的文件:替换 +-- 类型冲突那一支只比「失败了」:errno 是内核定的,不由 POSIX 统一 —— +-- macOS 给 "Is a directory" 21,Linux 的「文件 -> 非空目录」给 +-- "Directory not empty" 39。两边都是各自内核的答案(约定 #98)。 +--(文件 -> 空目录 两支一致,走普通的 p。) +local function px(tag, f, ...) + local r = table.pack(pcall(f, ...)) + print(string.format("%s %s [%d] %s", tag, tostring(r[1]), r.n, tostring(r[2]))) +end +px("B02", os.rename, D .. "g.txt", D .. "d") -- 文件 -> 非空目录 +p("B03", os.rename, D .. "g.txt", D .. "e") -- 文件 -> 空目录:EISDIR +p("B04", os.rename, D .. "d", D .. "e") -- 目录 -> 空目录:替换 +p("B05", os.rename, D .. "nosuch_a", D .. "nosuch_b") -- 源不存在 +p("B06", os.rename, D .. "e", D .. "nodir/x") -- 目标的父不存在 +p("B07", os.rename, D .. "e", D .. "e") -- 自己到自己 +p("B08", os.rename, D .. "e", "") -- 空目标 +p("B09", os.rename, D .. "e", D .. "e/x") -- 目标是自己下面的:ENOTDIR +p("B10", os.rename, "", D .. "x") + +-- C. os.remove +p("C01", os.remove, D .. "d") -- 非空目录 -> ENOTEMPTY +p("C02", os.remove, D .. "e") -- 空目录 +p("C03", os.remove, D .. "nosuch_zzz") -- 不存在 +p("C04", os.remove, D .. "f.txt") +p("C05", os.remove, "") -- 空路径 +p("C06", os.remove, D .. "d/inner") -- 目录里的文件 +p("C07", os.remove, ".") -- 当前目录 -> EINVAL +p("C08", os.remove, "/") -- 根 -> EISDIR +p("C09", os.remove, "/etc") -- 权限 -> EPERM +-- 只读目录里的文件 -> EACCES。不要拿 /etc/hosts 试:真删掉了就出事了 +os.execute("mkdir -p au_tmp/ro && touch au_tmp/ro/x && chmod 555 au_tmp/ro") +p("C10", os.remove, D .. "ro/x") -- 目录只读 -> EACCES + +os.execute("chmod -R u+w au_tmp && rm -rf au_tmp") + """); + } + + /* ==================== varinfo:错误消息里的变量后缀 ==================== */ + + /** + * {@code varinfo} 网格:运行时错误里那个 {@code " (kind 'name')"} 后缀。 + *

+ * 后缀是反推出来的,不是存下来的。{@code ldebug.c: varinfo} 先查 + * {@code getupvalname}(值是不是某个 upvalue),再看值落在第几个寄存器 + * {@code instack},最后交给 {@code getobjname} 去猜——猜的依据是 + * 当前帧上一次改写那个寄存器的指令。所以 kind 只有这几种: + *

    + *
  • {@code local} / {@code upvalue} / {@code constant}:值来自局部量、 + * upvalue、字符串常量;
  • + *
  • {@code global} / {@code field}:值来自 {@code t[k]},而 + * {@code isenv} 看被索引的是不是 {@code _ENV};
  • + *
  • {@code method}:{@code OP_SELF}({@code t:m()});
  • + *
  • {@code field 'integer index'}:{@code OP_GETI}({@code t[1]}), + * 注意 kind 是 {@code field}、名字是字面串 {@code integer index};
  • + *
  • {@code field '?'}:键推不出来(寄存器/浮点/布尔/表做键);
  • + *
  • {@code (for iterator 'for iterator')}:{@code OP_TFORCALL}。
  • + *
+ *

+ * 空后缀也是答案的一部分:{@code attempt to call a number value} 后面 + * 不能多出 {@code (constant '42')}——{@code kname} 只对字符串 + * 常量给名字,数字/布尔一律返回 NULL(矩阵里 {@code k_callnum} / + * {@code k_callbool} 两行钉住这条)。 + *

+ * 敏感度(破坏一处,矩阵变红的行数):{@code "integer index"\u2192"?"} 3、 + * {@code method\u2192field} 2、{@code isenv} 的 {@code global} 3、 + * {@code upvalue\u2192local} 1、{@code kname} 只认字符串 131、 + * {@code upvalname} 的 {@code "?"} 211、{@code instack} 435。 + */ + @Test + void varInfoKindGridMatchesReference() { + Diff.assertMatches(""" + + -- varinfo:运行时错误消息里那个 " (kind 'name')" 后缀 + local function s(tag, f, ...) + local ok, err = pcall(f, ...) + print(tag, ok and "OK" or err) + end + + -- 1. local + s("l_idx", function() local x = nil return x.y end) + s("l_newidx", function() local x = nil x.y = 1 end) + s("l_call", function() local x = nil return x() end) + s("l_arith", function() local x = {} return x + 1 end) + s("l_concat", function() local x = nil return x .. "a" end) + s("l_len", function() local x = 1 return #x end) + s("l_band", function() local x = {} return x & 1 end) + s("l_cmp", function() local x = {} return x < 1 end) + s("l_toint", function() local x = 1.5 return x | 0 end) + + -- 2. global(_ENV 上的字段) + gn = nil + gs = "str" + gt = {} + s("g_idx", function() return gn.y end) + s("g_call", function() return gn() end) + s("g_arith", function() return gs + 1 end) + s("g_len", function() return #gs end) + s("g_cmp", function() return gt < 1 end) + s("g_toint", function() return gs | 0 end) + + -- 3. upvalue + do + local u = nil + local uv = "uv" + local ut = {} + s("u_idx", function() return u.y end) + s("u_call", function() return u() end) + s("u_arith", function() return uv + 1 end) + s("u_band", function() return ut & 1 end) + end + + -- 4. constant + s("k_call", function() return ("cs")() end) + s("k_arith", function() return 1 + "cs" end) + s("k_concat", function() return 1 .. {} end) + s("k_idx", function() return ("cs").y end) + s("k_callnum", function() return (42)() end) + s("k_callbool", function() return (true)() end) + s("k_len", function() return #"cs" end) + + -- 5. field(局部变量表上的字符串键) + local t = { x = nil, ["a b"] = nil, y = "ys" } + s("f_idx", function() return t.x.y end) + s("f_idxsp", function() return t["a b"].y end) + s("f_call", function() return t.x() end) + s("f_arith", function() return t.y + 1 end) + -- 6. field(全局表上的字符串键) + G = { x = nil, y = "ys" } + s("gf_idx", function() return G.x.y end) + s("gf_call", function() return G.x() end) + s("gf_arith", function() return G.y + 1 end) + + -- 7. integer index(OP_GETI / OP_GETTABLE 的整数常量键) + local ti = { nil, "is" } + s("i_idx", function() return ti[1].y end) + s("i_call", function() return ti[1]() end) + s("i_arith", function() return ti[2] + 1 end) + s("i_idx0", function() return ti[0].y end) + s("i_idxneg", function() return ti[-1].y end) + + -- 8. 键是寄存器 / 浮点常量:名字推不出来,给 "?" + local tr = {} + local kr = "x" + s("r_idx", function() return tr[kr].y end) + s("r_call", function() return tr[kr]() end) + s("r_float", function() return tr[1.5].y end) + s("r_bool", function() return tr[true].y end) + s("r_self", function() return tr[{}].y end) + + -- 9. method(OP_SELF) + local tm = {} + s("m_call", function() return tm:m() end) + s("m_callsp", function() return tm["m n"]() end) + local Gm = {} + s("gm_call", function() return Gm:m() end) + + -- 10. 没有名字的临时值:后缀是空串 + s("n_tmp", function() local a = 1 return (a + 1)() end) + s("n_tmp2", function() local a = {} return (a and 1).y end) + s("n_vararg", function(...) return (...).y end, nil) + s("n_ret", function() local function g() return nil end return g().y end) + s("n_multi", function() local a = { nil } return (a[1] or 1)() end) + + -- 11. 走 OP_MOVE:名字从源寄存器反推 + s("mv", function() local x = nil local y = x return y.z end) + s("mv2", function() local x = nil local y = x local z = y return z.z end) + + -- 12. for 循环与 tointeger + s("for_init", function() for i = "a", 10 do end end) + s("for_limit", function() for i = 1, "b" do end end) + s("for_step", function() for i = 1, 10, "c" do end end) + s("for_var", function() for i = 1, 2 do local q = nil q.x = 1 end end) + + -- 13. 元方法里再报错 + local mt = { __index = function(t, k) local z = nil return z.w end } + local mo = setmetatable({}, mt) + s("mm_idx", function() return mo.anything end) + + -- 14. 数字与字符串之间 + s("num_idx", function() local n = 5 return n.x end) + s("str_idx", function() local s2 = "s" return s2.x end) + s("bool_arith", function() local b = true return b + 1 end) + print("done") + """); + } + + /** + * 符号执行网格:名字只在临时寄存器上才需要反推。 + *

+ * {@code ldebug.c: basicgetobjname} 的第一步是 + * {@code luaF_getlocalname(p, reg + 1, pc)}——只要那个寄存器在出错那一点 + * 是活着的局部量,就直接答 {@code "local"} 收工, + * 根本不进 {@code findsetreg}。这有个反直觉的后果: + * {@code local x = nil if c then x = nil end return x.y} 里那次条件赋值 + * 压根不影响结果,答案永远是 {@code (local 'x')}。本矩阵前 21 行就是按 + * 「条件赋值会让名字消失」这个错误直觉写的,实测全绿——把 + * {@code filterpc} 改成无条件的 {@code return pc},这 21 行一行都不红。 + *

+ * 真正能走到 {@code filterpc} 的是 {@code and} / {@code or}: + * {@code (c and t.a or t.b).z} 两份分支写的是同一个临时寄存器, + * 而且都写在跳转目标之前,于是最后一条改写记录被判为「可能没执行」而作废, + * 答案退化成空后缀。加上这几行之后,同一个破坏立刻红 234 行。 + *

+ * 同理 {@code OP_LOADNIL} 那条分支(「置 nil 覆盖 a..a+b 全部寄存器」) + * 红 10 行、{@code OP_MOVE} 的 {@code b < a} 红 22 行; + * 只有 {@code OP_TFORCALL} 的 {@code reg >= a + 2} 在这 1032 行里 + * 一次都没被走到(插桩实测,上游 33 个用例里也是 0)—— + * {@code TFORCALL} 在循环体末尾,而循环控制寄存器在循环内都是活局部量、 + * 循环外又必然被重新写过,于是它对错误消息不可观测。这一点是量出来的, + * 不是推出来的。 + */ + @Test + void varInfoSymbolicExecutionGridMatchesReference() { + Diff.assertMatches(""" + local T, F = true, false + local function s(tag, f, ...) local ok, e = pcall(f, ...) print(tag, ok and "OK" or e) end + + -- 15. 条件赋值:最后一次改写寄存器发生在跳转目标之前 -> filterpc 判为 -1 + s("c_if", function(c) local x = nil if c then x = nil end return x.y end, T) + s("c_ifF", function(c) local x = nil if c then x = nil end return x.y end, F) + s("c_else", function(c) local x = nil if c then x = nil else x = nil end return x.y end, T) + s("c_elseF", function(c) local x = nil if c then x = nil else x = nil end return x.y end, F) + s("c_elseif", function(c) local x = nil if c then x = nil elseif c then x = nil end return x.y end, F) + s("c_while", function(c) local x = nil local i = 0 while i < 1 do x = nil i = i + 1 end return x.y end) + s("c_repeat", function() local x = nil local i = 0 repeat x = nil i = i + 1 until i >= 1 return x.y end) + s("c_for", function() local x = nil for i = 1, 1 do x = nil end return x.y end) + s("c_gfor", function() local x = nil for _, v in ipairs({ 1 }) do x = nil end return x.y end) + s("c_and", function(c) local x = nil local y = c and x return y.z end, T) + s("c_or", function(c) local x = nil local y = c or x return y.z end, F) + s("c_not", function(c) local x = nil if not c then x = nil end return x.y end, T) + s("c_goto", function(c) local x = nil if c then goto done end x = nil ::done:: return x.y end, T) + s("c_twoif", function(c) local x = nil if c then x = nil end if c then x = nil end return x.y end, T) + s("c_ifcall", function(c) local x = nil if c then x = nil end return x() end, T) + s("c_ifarith", function(c) local x = nil if c then x = {} end return x + 1 end, T) + s("c_ifband", function(c) local x = nil if c then x = {} end return x & 1 end, T) + s("c_ifconcat", function(c) local x = nil if c then x = nil end return x .. "a" end, T) + s("c_ifidx", function(c) local x = nil if c then x = nil end x.k = 1 end, T) + s("c_deep", function(c) local x = nil local y = nil if c then y = nil end local z = y return z.w end, T) + s("c_ok", function(c) local x = nil if c then end return x.y end, T) + + -- 16. MMBIN:落空的元方法指令不参与反推 + local mtn = setmetatable({}, { __add = function() return 0 end, __index = function() return nil end }) + s("mm_add", function() local x = {} return x + mtn end) + s("mm_add2", function() local x = {} return mtn + x end) + s("mm_concat", function() local x = {} return x .. mtn end) + s("mm_len", function() local x = {} return #mtn end) + s("mm_idx", function() local x = nil return mtn[x] end) + s("mm_eq", function() local x = {} return x == mtn end) + s("mm_lt", function() local x = {} return x < mtn end) + s("mm_call", function() local x = nil return x(mtn) end) + s("mm_self", function() local x = nil return x:m(mtn) end) + print("done") + + local T, F = true, false + local function s(tag, f, ...) local ok, e = pcall(f, ...) print(tag, ok and "OK" or e) end + local t = { a = nil, b = nil, c = nil } + local gn = nil + do + local u = nil + -- and/or:两条分支各写一次同一个临时寄存器,且都在跳转目标之前 + s("ao_andor_T", function(c) return (c and t.a or t.b).z end, T) + s("ao_andor_F", function(c) return (c and t.a or t.b).z end, F) + s("ao_or", function() return (t.a or t.b).z end) + s("ao_orv", function(c) return (c or t.a).z end, F) + s("ao_and", function(c) return (c and t.a).z end, T) + s("ao_andv", function(c) return (c and t.a).z end, F) + s("ao_nest", function(c) return ((c and t.a or t.b) or t.c).z end, T) + s("ao_nest2", function(c) return (c and (t.a or t.b) or t.c).z end, F) + s("ao_3", function(c) return (c and t.a or c and t.b or t.c).z end, T) + s("ao_3b", function(c) return (c and t.a or c and t.b or t.c).z end, F) + s("ao_str", function(c) return (c and "s" or "s")() end, T) + s("ao_strF", function(c) return (c and "s" or "s")() end, F) + s("ao_num", function(c) return (c and 1 or 2)() end, T) + s("ao_up", function(c) return (c and u or u).z end, T) + s("ao_upF", function(c) return (c and u or u).z end, F) + s("ao_glob", function(c) return (c and gn or gn).z end, T) + s("ao_call", function(c) return (c and t.a or t.b)() end, T) + s("ao_arith", function(c) return (c and t.a or 1) + 1 end, T) + s("ao_concat", function(c) return (c and t.a or nil) .. "x" end, T) + s("ao_len", function(c) return #(c and t.a or nil) end, T) + s("ao_idx", function(c) (c and t.a or t.b).k = 1 end, T) + s("ao_band", function(c) return (c and t.a or 1) & 1 end, T) + -- 循环里的 break / goto 制造的跳转目标 + s("lp_while", function() local r for i = 1, 3 do if i == 2 then r = t.a break end r = t.b end return r.z end) + s("lp_repeat", function() local r repeat r = t.a until true return r.z end) + s("lp_gfor", function() local r for _, v in ipairs({ 1, 2 }) do if v == 2 then r = t.a break end r = t.b end return r.z end) + s("lp_goto", function(c) local r if c then r = t.a goto out end r = t.b ::out:: return r.z end, T) + s("lp_gotoF", function(c) local r if c then r = t.a goto out end r = t.b ::out:: return r.z end, F) + end + print("done") + """); + } + + /** + * 泛型 {@code for} 网格:迭代器自己报错时的 kind。 + *

+ * {@code ldebug.c} 在 {@code funcnamefromcall} 里给 {@code OP_TFORCALL} + * 单独留了一句:kind 和名字都叫 {@code for iterator},于是完整后缀是 + * {@code (for iterator 'for iterator')}——两个词一样,不是笔误。 + *

+ * 另外 {@code for k, v in ...} 的控制寄存器在 {@code luaF_getlocalname} + * 眼里是局部量,名字是 {@code (for gen)} / {@code (for state)} 这类带 + * 括号的内部名;{@code luaF_getlocalname} 不过滤它们,所以循环里的 + * {@code k} / {@code v} 照常报 {@code (local 'k')}。 + */ + @Test + void varInfoGenericForGridMatchesReference() { + Diff.assertMatches(""" + local T, F = true, false + local function s(tag, f, ...) local ok, e = pcall(f, ...) print(tag, ok and "OK" or e) end + local gn = nil + do + local u = nil + local t = { a = nil } + s("it_001", function(c) for a in (nil) do end end, T) + s("it_002", function(c) for a in ((nil)) do end end, F) + s("it_003", function(c) for a in (u) do end end, T) + s("it_004", function(c) for a in ((u)) do end end, F) + s("it_005", function(c) for a in (gn) do end end, T) + s("it_006", function(c) for a in ((gn)) do end end, F) + s("it_007", function(c) for a in ("s") do end end, T) + s("it_008", function(c) for a in (1) do end end, F) + s("it_009", function(c) for a in ({}) do end end, T) + s("it_010", function(c) for a in (t.a) do end end, F) + s("it_011", function(c) for a in ((t.a)) do end end, T) + s("it_012", function(c) for a in ((c and u or u)) do end end, F) + s("it_013", function(c) for a in ((c and nil or u)) do end end, T) + s("inb_014", function(c) for a in pairs({}) do return (nil).z end end, F) + s("inb_015", function(c) for a in pairs({}) do return ((nil)).z end end, T) + s("inb_016", function(c) for a in pairs({}) do return (u).z end end, F) + s("inb_017", function(c) for a in pairs({}) do return ((u)).z end end, T) + s("inb_018", function(c) for a in pairs({}) do return (gn).z end end, F) + s("inb_019", function(c) for a in pairs({}) do return ((gn)).z end end, T) + s("inb_020", function(c) for a in pairs({}) do return ("s").z end end, F) + s("inb_021", function(c) for a in pairs({}) do return (1).z end end, T) + s("inb_022", function(c) for a in pairs({}) do return ({}).z end end, F) + s("inb_023", function(c) for a in pairs({}) do return (t.a).z end end, T) + s("inb_024", function(c) for a in pairs({}) do return ((t.a)).z end end, F) + s("inb_025", function(c) for a in pairs({}) do return ((c and u or u)).z end end, T) + s("inb_026", function(c) for a in pairs({}) do return ((c and nil or u)).z end end, F) + s("aft_027", function(c) for a in pairs({}) do end return (nil).z end, T) + s("aft2_028", function(c) for a in pairs({}) do end return (nil)() end, F) + s("aft_029", function(c) for a in pairs({}) do end return ((nil)).z end, T) + s("aft2_030", function(c) for a in pairs({}) do end return ((nil))() end, F) + s("aft_031", function(c) for a in pairs({}) do end return (u).z end, T) + s("aft2_032", function(c) for a in pairs({}) do end return (u)() end, F) + s("aft_033", function(c) for a in pairs({}) do end return ((u)).z end, T) + s("aft2_034", function(c) for a in pairs({}) do end return ((u))() end, F) + s("aft_035", function(c) for a in pairs({}) do end return (gn).z end, T) + s("aft2_036", function(c) for a in pairs({}) do end return (gn)() end, F) + s("aft_037", function(c) for a in pairs({}) do end return ((gn)).z end, T) + s("aft2_038", function(c) for a in pairs({}) do end return ((gn))() end, F) + s("aft_039", function(c) for a in pairs({}) do end return ("s").z end, T) + s("aft2_040", function(c) for a in pairs({}) do end return ("s")() end, F) + s("aft_041", function(c) for a in pairs({}) do end return (1).z end, T) + s("aft2_042", function(c) for a in pairs({}) do end return (1)() end, F) + s("aft_043", function(c) for a in pairs({}) do end return ({}).z end, T) + s("aft2_044", function(c) for a in pairs({}) do end return ({})() end, F) + s("aft_045", function(c) for a in pairs({}) do end return (t.a).z end, T) + s("aft2_046", function(c) for a in pairs({}) do end return (t.a)() end, F) + s("aft_047", function(c) for a in pairs({}) do end return ((t.a)).z end, T) + s("aft2_048", function(c) for a in pairs({}) do end return ((t.a))() end, F) + s("aft_049", function(c) for a in pairs({}) do end return ((c and u or u)).z end, T) + s("aft2_050", function(c) for a in pairs({}) do end return ((c and u or u))() end, F) + s("aft_051", function(c) for a in pairs({}) do end return ((c and nil or u)).z end, T) + s("aft2_052", function(c) for a in pairs({}) do end return ((c and nil or u))() end, F) + s("nest_053", function(c) for a in pairs({}) do for b in pairs({}) do end return (nil).z end end, T) + s("nest_054", function(c) for a in pairs({}) do for b in pairs({}) do end return ((nil)).z end end, F) + s("nest_055", function(c) for a in pairs({}) do for b in pairs({}) do end return (u).z end end, T) + s("nest_056", function(c) for a in pairs({}) do for b in pairs({}) do end return ((u)).z end end, F) + s("nest_057", function(c) for a in pairs({}) do for b in pairs({}) do end return (gn).z end end, T) + s("nest_058", function(c) for a in pairs({}) do for b in pairs({}) do end return ((gn)).z end end, F) + s("nest_059", function(c) for a in pairs({}) do for b in pairs({}) do end return ("s").z end end, T) + s("nest_060", function(c) for a in pairs({}) do for b in pairs({}) do end return (1).z end end, F) + s("nest_061", function(c) for a in pairs({}) do for b in pairs({}) do end return ({}).z end end, T) + s("nest_062", function(c) for a in pairs({}) do for b in pairs({}) do end return (t.a).z end end, F) + s("nest_063", function(c) for a in pairs({}) do for b in pairs({}) do end return ((t.a)).z end end, T) + s("nest_064", function(c) for a in pairs({}) do for b in pairs({}) do end return ((c and u or u)).z end end, F) + s("nest_065", function(c) for a in pairs({}) do for b in pairs({}) do end return ((c and nil or u)).z end end, T) + s("it2_066", function(c) local i = 0 for a in pairs({ 1, 2 }) do i = i + 1 if i == 2 then return (nil).z end end end, F) + s("it2_067", function(c) local i = 0 for a in pairs({ 1, 2 }) do i = i + 1 if i == 2 then return ((nil)).z end end end, T) + s("it2_068", function(c) local i = 0 for a in pairs({ 1, 2 }) do i = i + 1 if i == 2 then return (u).z end end end, F) + s("it2_069", function(c) local i = 0 for a in pairs({ 1, 2 }) do i = i + 1 if i == 2 then return ((u)).z end end end, T) + s("it2_070", function(c) local i = 0 for a in pairs({ 1, 2 }) do i = i + 1 if i == 2 then return (gn).z end end end, F) + s("it2_071", function(c) local i = 0 for a in pairs({ 1, 2 }) do i = i + 1 if i == 2 then return ((gn)).z end end end, T) + s("it2_072", function(c) local i = 0 for a in pairs({ 1, 2 }) do i = i + 1 if i == 2 then return ("s").z end end end, F) + s("it2_073", function(c) local i = 0 for a in pairs({ 1, 2 }) do i = i + 1 if i == 2 then return (1).z end end end, T) + s("it2_074", function(c) local i = 0 for a in pairs({ 1, 2 }) do i = i + 1 if i == 2 then return ({}).z end end end, F) + s("it2_075", function(c) local i = 0 for a in pairs({ 1, 2 }) do i = i + 1 if i == 2 then return (t.a).z end end end, T) + s("it2_076", function(c) local i = 0 for a in pairs({ 1, 2 }) do i = i + 1 if i == 2 then return ((t.a)).z end end end, F) + s("it2_077", function(c) local i = 0 for a in pairs({ 1, 2 }) do i = i + 1 if i == 2 then return ((c and u or u)).z end end end, T) + s("it2_078", function(c) local i = 0 for a in pairs({ 1, 2 }) do i = i + 1 if i == 2 then return ((c and nil or u)).z end end end, F) + s("var_079", function(c) for k, v in pairs({ [1] = nil }) do return v.z end end, T) + s("var2_080", function(c) for k, v in pairs({ a = 1 }) do return (k).z end end, F) + s("var3_081", function(c) for k, v in pairs({}) do end return (k or nil).z end, T) + end + print("done") + """); + } + + /** + * 表达式 × 运算符网格:把「前缀 × 表达式 × 运算符 × 条件真假」全铺开。 + *

+ * 2592 种组合里去掉无意义的(用了 {@code x} 却没声明 {@code x} 的那些), + * 留下 828 行。手写挑用例在这片上是没有用的:{@code kname}、 + * {@code upvalname}、{@code instack}、{@code OP_MOVE} 这几处的敏感度 + * (131 / 211 / 435 / 22)几乎全靠这一片撑起来,vi/fp/fp2/fp4 四组加起来 + * 才贡献零头。 + *

+ * {@code (global 'x')} 与 {@code (local 'x')} 同时出现是刻意的:第二组前缀 + * 只声明 {@code t2},于是同一批表达式里的 {@code x} 变成 {@code _ENV.x}, + * kind 从 {@code local} 翻成 {@code global}——{@code isenv} 的分岔靠这个照到。 + */ + @Test + void varInfoExpressionGridMatchesReference() { + Diff.assertMatches(""" + local T, F = true, false + local function s(tag, f, ...) local ok, e = pcall(f, ...) print(tag, ok and "OK" or e) end + local gn = nil + do + local u = nil + local t = { a = nil } + s("g001", function(c) return (nil).z end, T) + s("g002", function(c) return (nil).z end, F) + s("g003", function(c) return (nil)() end, T) + s("g004", function(c) return (nil)() end, F) + s("g005", function(c) return (nil)+ 1 end, T) + s("g006", function(c) return (nil)+ 1 end, F) + s("g007", function(c) return (nil).. "q" end, T) + s("g008", function(c) return (nil).. "q" end, F) + s("g009", function(c) return (nil)& 1 end, T) + s("g010", function(c) return (nil)& 1 end, F) + s("g011", function(c) local q = (nil) q.k = 1 end, T) + s("g012", function(c) local q = (nil) q.k = 1 end, F) + s("g013", function(c) return ((nil)).z end, T) + s("g014", function(c) return ((nil)).z end, F) + s("g015", function(c) return ((nil))() end, T) + s("g016", function(c) return ((nil))() end, F) + s("g017", function(c) return ((nil))+ 1 end, T) + s("g018", function(c) return ((nil))+ 1 end, F) + s("g019", function(c) return ((nil)).. "q" end, T) + s("g020", function(c) return ((nil)).. "q" end, F) + s("g021", function(c) return ((nil))& 1 end, T) + s("g022", function(c) return ((nil))& 1 end, F) + s("g023", function(c) local q = ((nil)) q.k = 1 end, T) + s("g024", function(c) local q = ((nil)) q.k = 1 end, F) + s("g025", function(c) return (u).z end, T) + s("g026", function(c) return (u).z end, F) + s("g027", function(c) return (u)() end, T) + s("g028", function(c) return (u)() end, F) + s("g029", function(c) return (u)+ 1 end, T) + s("g030", function(c) return (u)+ 1 end, F) + s("g031", function(c) return (u).. "q" end, T) + s("g032", function(c) return (u).. "q" end, F) + s("g033", function(c) return (u)& 1 end, T) + s("g034", function(c) return (u)& 1 end, F) + s("g035", function(c) local q = (u) q.k = 1 end, T) + s("g036", function(c) local q = (u) q.k = 1 end, F) + s("g037", function(c) return ((u)).z end, T) + s("g038", function(c) return ((u)).z end, F) + s("g039", function(c) return ((u))() end, T) + s("g040", function(c) return ((u))() end, F) + s("g041", function(c) return ((u))+ 1 end, T) + s("g042", function(c) return ((u))+ 1 end, F) + s("g043", function(c) return ((u)).. "q" end, T) + s("g044", function(c) return ((u)).. "q" end, F) + s("g045", function(c) return ((u))& 1 end, T) + s("g046", function(c) return ((u))& 1 end, F) + s("g047", function(c) local q = ((u)) q.k = 1 end, T) + s("g048", function(c) local q = ((u)) q.k = 1 end, F) + s("g049", function(c) return (gn).z end, T) + s("g050", function(c) return (gn).z end, F) + s("g051", function(c) return (gn)() end, T) + s("g052", function(c) return (gn)() end, F) + s("g053", function(c) return (gn)+ 1 end, T) + s("g054", function(c) return (gn)+ 1 end, F) + s("g055", function(c) return (gn).. "q" end, T) + s("g056", function(c) return (gn).. "q" end, F) + s("g057", function(c) return (gn)& 1 end, T) + s("g058", function(c) return (gn)& 1 end, F) + s("g059", function(c) local q = (gn) q.k = 1 end, T) + s("g060", function(c) local q = (gn) q.k = 1 end, F) + s("g061", function(c) return ((gn)).z end, T) + s("g062", function(c) return ((gn)).z end, F) + s("g063", function(c) return ((gn))() end, T) + s("g064", function(c) return ((gn))() end, F) + s("g065", function(c) return ((gn))+ 1 end, T) + s("g066", function(c) return ((gn))+ 1 end, F) + s("g067", function(c) return ((gn)).. "q" end, T) + s("g068", function(c) return ((gn)).. "q" end, F) + s("g069", function(c) return ((gn))& 1 end, T) + s("g070", function(c) return ((gn))& 1 end, F) + s("g071", function(c) local q = ((gn)) q.k = 1 end, T) + s("g072", function(c) local q = ((gn)) q.k = 1 end, F) + s("g073", function(c) return (t.a).z end, T) + s("g074", function(c) return (t.a).z end, F) + s("g075", function(c) return (t.a)() end, T) + s("g076", function(c) return (t.a)() end, F) + s("g077", function(c) return (t.a)+ 1 end, T) + s("g078", function(c) return (t.a)+ 1 end, F) + s("g079", function(c) return (t.a).. "q" end, T) + s("g080", function(c) return (t.a).. "q" end, F) + s("g081", function(c) return (t.a)& 1 end, T) + s("g082", function(c) return (t.a)& 1 end, F) + s("g083", function(c) local q = (t.a) q.k = 1 end, T) + s("g084", function(c) local q = (t.a) q.k = 1 end, F) + s("g085", function(c) return ((t.a)).z end, T) + s("g086", function(c) return ((t.a)).z end, F) + s("g087", function(c) return ((t.a))() end, T) + s("g088", function(c) return ((t.a))() end, F) + s("g089", function(c) return ((t.a))+ 1 end, T) + s("g090", function(c) return ((t.a))+ 1 end, F) + s("g091", function(c) return ((t.a)).. "q" end, T) + s("g092", function(c) return ((t.a)).. "q" end, F) + s("g093", function(c) return ((t.a))& 1 end, T) + s("g094", function(c) return ((t.a))& 1 end, F) + s("g095", function(c) local q = ((t.a)) q.k = 1 end, T) + s("g096", function(c) local q = ((t.a)) q.k = 1 end, F) + s("g097", function(c) return ("s").z end, T) + s("g098", function(c) return ("s").z end, F) + s("g099", function(c) return ("s")() end, T) + s("g100", function(c) return ("s")() end, F) + s("g101", function(c) return ("s")+ 1 end, T) + s("g102", function(c) return ("s")+ 1 end, F) + s("g103", function(c) return ("s").. "q" end, T) + s("g104", function(c) return ("s").. "q" end, F) + s("g105", function(c) return ("s")& 1 end, T) + s("g106", function(c) return ("s")& 1 end, F) + s("g107", function(c) local q = ("s") q.k = 1 end, T) + s("g108", function(c) local q = ("s") q.k = 1 end, F) + s("g109", function(c) return (1).z end, T) + s("g110", function(c) return (1).z end, F) + s("g111", function(c) return (1)() end, T) + s("g112", function(c) return (1)() end, F) + s("g113", function(c) return (1)+ 1 end, T) + s("g114", function(c) return (1)+ 1 end, F) + s("g115", function(c) return (1).. "q" end, T) + s("g116", function(c) return (1).. "q" end, F) + s("g117", function(c) return (1)& 1 end, T) + s("g118", function(c) return (1)& 1 end, F) + s("g119", function(c) local q = (1) q.k = 1 end, T) + s("g120", function(c) local q = (1) q.k = 1 end, F) + s("g121", function(c) return ({}).z end, T) + s("g122", function(c) return ({}).z end, F) + s("g123", function(c) return ({})() end, T) + s("g124", function(c) return ({})() end, F) + s("g125", function(c) return ({})+ 1 end, T) + s("g126", function(c) return ({})+ 1 end, F) + s("g127", function(c) return ({}).. "q" end, T) + s("g128", function(c) return ({}).. "q" end, F) + s("g129", function(c) return ({})& 1 end, T) + s("g130", function(c) return ({})& 1 end, F) + s("g131", function(c) local q = ({}) q.k = 1 end, T) + s("g132", function(c) local q = ({}) q.k = 1 end, F) + s("g133", function(c) return ((c and nil or u)).z end, T) + s("g134", function(c) return ((c and nil or u)).z end, F) + s("g135", function(c) return ((c and nil or u))() end, T) + s("g136", function(c) return ((c and nil or u))() end, F) + s("g137", function(c) return ((c and nil or u))+ 1 end, T) + s("g138", function(c) return ((c and nil or u))+ 1 end, F) + s("g139", function(c) return ((c and nil or u)).. "q" end, T) + s("g140", function(c) return ((c and nil or u)).. "q" end, F) + s("g141", function(c) return ((c and nil or u))& 1 end, T) + s("g142", function(c) return ((c and nil or u))& 1 end, F) + s("g143", function(c) local q = ((c and nil or u)) q.k = 1 end, T) + s("g144", function(c) local q = ((c and nil or u)) q.k = 1 end, F) + s("g145", function(c) return ((c and gn or gn)).z end, T) + s("g146", function(c) return ((c and gn or gn)).z end, F) + s("g147", function(c) return ((c and gn or gn))() end, T) + s("g148", function(c) return ((c and gn or gn))() end, F) + s("g149", function(c) return ((c and gn or gn))+ 1 end, T) + s("g150", function(c) return ((c and gn or gn))+ 1 end, F) + s("g151", function(c) return ((c and gn or gn)).. "q" end, T) + s("g152", function(c) return ((c and gn or gn)).. "q" end, F) + s("g153", function(c) return ((c and gn or gn))& 1 end, T) + s("g154", function(c) return ((c and gn or gn))& 1 end, F) + s("g155", function(c) local q = ((c and gn or gn)) q.k = 1 end, T) + s("g156", function(c) local q = ((c and gn or gn)) q.k = 1 end, F) + s("g157", function(c) return ((c and u or u)).z end, T) + s("g158", function(c) return ((c and u or u)).z end, F) + s("g159", function(c) return ((c and u or u))() end, T) + s("g160", function(c) return ((c and u or u))() end, F) + s("g161", function(c) return ((c and u or u))+ 1 end, T) + s("g162", function(c) return ((c and u or u))+ 1 end, F) + s("g163", function(c) return ((c and u or u)).. "q" end, T) + s("g164", function(c) return ((c and u or u)).. "q" end, F) + s("g165", function(c) return ((c and u or u))& 1 end, T) + s("g166", function(c) return ((c and u or u))& 1 end, F) + s("g167", function(c) local q = ((c and u or u)) q.k = 1 end, T) + s("g168", function(c) local q = ((c and u or u)) q.k = 1 end, F) + s("g169", function(c) local x = nil return (nil).z end, T) + s("g170", function(c) local x = nil return (nil).z end, F) + s("g171", function(c) local x = nil return (nil)() end, T) + s("g172", function(c) local x = nil return (nil)() end, F) + s("g173", function(c) local x = nil return (nil)+ 1 end, T) + s("g174", function(c) local x = nil return (nil)+ 1 end, F) + s("g175", function(c) local x = nil return (nil).. "q" end, T) + s("g176", function(c) local x = nil return (nil).. "q" end, F) + s("g177", function(c) local x = nil return (nil)& 1 end, T) + s("g178", function(c) local x = nil return (nil)& 1 end, F) + s("g179", function(c) local x = nil local q = (nil) q.k = 1 end, T) + s("g180", function(c) local x = nil local q = (nil) q.k = 1 end, F) + s("g181", function(c) local x = nil return ((nil)).z end, T) + s("g182", function(c) local x = nil return ((nil)).z end, F) + s("g183", function(c) local x = nil return ((nil))() end, T) + s("g184", function(c) local x = nil return ((nil))() end, F) + s("g185", function(c) local x = nil return ((nil))+ 1 end, T) + s("g186", function(c) local x = nil return ((nil))+ 1 end, F) + s("g187", function(c) local x = nil return ((nil)).. "q" end, T) + s("g188", function(c) local x = nil return ((nil)).. "q" end, F) + s("g189", function(c) local x = nil return ((nil))& 1 end, T) + s("g190", function(c) local x = nil return ((nil))& 1 end, F) + s("g191", function(c) local x = nil local q = ((nil)) q.k = 1 end, T) + s("g192", function(c) local x = nil local q = ((nil)) q.k = 1 end, F) + s("g193", function(c) local x = nil return (x).z end, T) + s("g194", function(c) local x = nil return (x).z end, F) + s("g195", function(c) local x = nil return (x)() end, T) + s("g196", function(c) local x = nil return (x)() end, F) + s("g197", function(c) local x = nil return (x)+ 1 end, T) + s("g198", function(c) local x = nil return (x)+ 1 end, F) + s("g199", function(c) local x = nil return (x).. "q" end, T) + s("g200", function(c) local x = nil return (x).. "q" end, F) + s("g201", function(c) local x = nil return (x)& 1 end, T) + s("g202", function(c) local x = nil return (x)& 1 end, F) + s("g203", function(c) local x = nil local q = (x) q.k = 1 end, T) + s("g204", function(c) local x = nil local q = (x) q.k = 1 end, F) + s("g205", function(c) local x = nil return ((x)).z end, T) + s("g206", function(c) local x = nil return ((x)).z end, F) + s("g207", function(c) local x = nil return ((x))() end, T) + s("g208", function(c) local x = nil return ((x))() end, F) + s("g209", function(c) local x = nil return ((x))+ 1 end, T) + s("g210", function(c) local x = nil return ((x))+ 1 end, F) + s("g211", function(c) local x = nil return ((x)).. "q" end, T) + s("g212", function(c) local x = nil return ((x)).. "q" end, F) + s("g213", function(c) local x = nil return ((x))& 1 end, T) + s("g214", function(c) local x = nil return ((x))& 1 end, F) + s("g215", function(c) local x = nil local q = ((x)) q.k = 1 end, T) + s("g216", function(c) local x = nil local q = ((x)) q.k = 1 end, F) + s("g217", function(c) local x = nil return (u).z end, T) + s("g218", function(c) local x = nil return (u).z end, F) + s("g219", function(c) local x = nil return (u)() end, T) + s("g220", function(c) local x = nil return (u)() end, F) + s("g221", function(c) local x = nil return (u)+ 1 end, T) + s("g222", function(c) local x = nil return (u)+ 1 end, F) + s("g223", function(c) local x = nil return (u).. "q" end, T) + s("g224", function(c) local x = nil return (u).. "q" end, F) + s("g225", function(c) local x = nil return (u)& 1 end, T) + s("g226", function(c) local x = nil return (u)& 1 end, F) + s("g227", function(c) local x = nil local q = (u) q.k = 1 end, T) + s("g228", function(c) local x = nil local q = (u) q.k = 1 end, F) + s("g229", function(c) local x = nil return ((u)).z end, T) + s("g230", function(c) local x = nil return ((u)).z end, F) + s("g231", function(c) local x = nil return ((u))() end, T) + s("g232", function(c) local x = nil return ((u))() end, F) + s("g233", function(c) local x = nil return ((u))+ 1 end, T) + s("g234", function(c) local x = nil return ((u))+ 1 end, F) + s("g235", function(c) local x = nil return ((u)).. "q" end, T) + s("g236", function(c) local x = nil return ((u)).. "q" end, F) + s("g237", function(c) local x = nil return ((u))& 1 end, T) + s("g238", function(c) local x = nil return ((u))& 1 end, F) + s("g239", function(c) local x = nil local q = ((u)) q.k = 1 end, T) + s("g240", function(c) local x = nil local q = ((u)) q.k = 1 end, F) + s("g241", function(c) local x = nil return (gn).z end, T) + s("g242", function(c) local x = nil return (gn).z end, F) + s("g243", function(c) local x = nil return (gn)() end, T) + s("g244", function(c) local x = nil return (gn)() end, F) + s("g245", function(c) local x = nil return (gn)+ 1 end, T) + s("g246", function(c) local x = nil return (gn)+ 1 end, F) + s("g247", function(c) local x = nil return (gn).. "q" end, T) + s("g248", function(c) local x = nil return (gn).. "q" end, F) + s("g249", function(c) local x = nil return (gn)& 1 end, T) + s("g250", function(c) local x = nil return (gn)& 1 end, F) + s("g251", function(c) local x = nil local q = (gn) q.k = 1 end, T) + s("g252", function(c) local x = nil local q = (gn) q.k = 1 end, F) + s("g253", function(c) local x = nil return ((gn)).z end, T) + s("g254", function(c) local x = nil return ((gn)).z end, F) + s("g255", function(c) local x = nil return ((gn))() end, T) + s("g256", function(c) local x = nil return ((gn))() end, F) + s("g257", function(c) local x = nil return ((gn))+ 1 end, T) + s("g258", function(c) local x = nil return ((gn))+ 1 end, F) + s("g259", function(c) local x = nil return ((gn)).. "q" end, T) + s("g260", function(c) local x = nil return ((gn)).. "q" end, F) + s("g261", function(c) local x = nil return ((gn))& 1 end, T) + s("g262", function(c) local x = nil return ((gn))& 1 end, F) + s("g263", function(c) local x = nil local q = ((gn)) q.k = 1 end, T) + s("g264", function(c) local x = nil local q = ((gn)) q.k = 1 end, F) + s("g265", function(c) local x = nil return (t.a).z end, T) + s("g266", function(c) local x = nil return (t.a).z end, F) + s("g267", function(c) local x = nil return (t.a)() end, T) + s("g268", function(c) local x = nil return (t.a)() end, F) + s("g269", function(c) local x = nil return (t.a)+ 1 end, T) + s("g270", function(c) local x = nil return (t.a)+ 1 end, F) + s("g271", function(c) local x = nil return (t.a).. "q" end, T) + s("g272", function(c) local x = nil return (t.a).. "q" end, F) + s("g273", function(c) local x = nil return (t.a)& 1 end, T) + s("g274", function(c) local x = nil return (t.a)& 1 end, F) + s("g275", function(c) local x = nil local q = (t.a) q.k = 1 end, T) + s("g276", function(c) local x = nil local q = (t.a) q.k = 1 end, F) + s("g277", function(c) local x = nil return ((t.a)).z end, T) + s("g278", function(c) local x = nil return ((t.a)).z end, F) + s("g279", function(c) local x = nil return ((t.a))() end, T) + s("g280", function(c) local x = nil return ((t.a))() end, F) + s("g281", function(c) local x = nil return ((t.a))+ 1 end, T) + s("g282", function(c) local x = nil return ((t.a))+ 1 end, F) + s("g283", function(c) local x = nil return ((t.a)).. "q" end, T) + s("g284", function(c) local x = nil return ((t.a)).. "q" end, F) + s("g285", function(c) local x = nil return ((t.a))& 1 end, T) + s("g286", function(c) local x = nil return ((t.a))& 1 end, F) + s("g287", function(c) local x = nil local q = ((t.a)) q.k = 1 end, T) + s("g288", function(c) local x = nil local q = ((t.a)) q.k = 1 end, F) + s("g289", function(c) local x = nil return ("s").z end, T) + s("g290", function(c) local x = nil return ("s").z end, F) + s("g291", function(c) local x = nil return ("s")() end, T) + s("g292", function(c) local x = nil return ("s")() end, F) + s("g293", function(c) local x = nil return ("s")+ 1 end, T) + s("g294", function(c) local x = nil return ("s")+ 1 end, F) + s("g295", function(c) local x = nil return ("s").. "q" end, T) + s("g296", function(c) local x = nil return ("s").. "q" end, F) + s("g297", function(c) local x = nil return ("s")& 1 end, T) + s("g298", function(c) local x = nil return ("s")& 1 end, F) + s("g299", function(c) local x = nil local q = ("s") q.k = 1 end, T) + s("g300", function(c) local x = nil local q = ("s") q.k = 1 end, F) + s("g301", function(c) local x = nil return (1).z end, T) + s("g302", function(c) local x = nil return (1).z end, F) + s("g303", function(c) local x = nil return (1)() end, T) + s("g304", function(c) local x = nil return (1)() end, F) + s("g305", function(c) local x = nil return (1)+ 1 end, T) + s("g306", function(c) local x = nil return (1)+ 1 end, F) + s("g307", function(c) local x = nil return (1).. "q" end, T) + s("g308", function(c) local x = nil return (1).. "q" end, F) + s("g309", function(c) local x = nil return (1)& 1 end, T) + s("g310", function(c) local x = nil return (1)& 1 end, F) + s("g311", function(c) local x = nil local q = (1) q.k = 1 end, T) + s("g312", function(c) local x = nil local q = (1) q.k = 1 end, F) + s("g313", function(c) local x = nil return ({}).z end, T) + s("g314", function(c) local x = nil return ({}).z end, F) + s("g315", function(c) local x = nil return ({})() end, T) + s("g316", function(c) local x = nil return ({})() end, F) + s("g317", function(c) local x = nil return ({})+ 1 end, T) + s("g318", function(c) local x = nil return ({})+ 1 end, F) + s("g319", function(c) local x = nil return ({}).. "q" end, T) + s("g320", function(c) local x = nil return ({}).. "q" end, F) + s("g321", function(c) local x = nil return ({})& 1 end, T) + s("g322", function(c) local x = nil return ({})& 1 end, F) + s("g323", function(c) local x = nil local q = ({}) q.k = 1 end, T) + s("g324", function(c) local x = nil local q = ({}) q.k = 1 end, F) + s("g325", function(c) local x = nil return ((c and x or x)).z end, T) + s("g326", function(c) local x = nil return ((c and x or x)).z end, F) + s("g327", function(c) local x = nil return ((c and x or x))() end, T) + s("g328", function(c) local x = nil return ((c and x or x))() end, F) + s("g329", function(c) local x = nil return ((c and x or x))+ 1 end, T) + s("g330", function(c) local x = nil return ((c and x or x))+ 1 end, F) + s("g331", function(c) local x = nil return ((c and x or x)).. "q" end, T) + s("g332", function(c) local x = nil return ((c and x or x)).. "q" end, F) + s("g333", function(c) local x = nil return ((c and x or x))& 1 end, T) + s("g334", function(c) local x = nil return ((c and x or x))& 1 end, F) + s("g335", function(c) local x = nil local q = ((c and x or x)) q.k = 1 end, T) + s("g336", function(c) local x = nil local q = ((c and x or x)) q.k = 1 end, F) + s("g337", function(c) local x = nil return ((c and x or u)).z end, T) + s("g338", function(c) local x = nil return ((c and x or u)).z end, F) + s("g339", function(c) local x = nil return ((c and x or u))() end, T) + s("g340", function(c) local x = nil return ((c and x or u))() end, F) + s("g341", function(c) local x = nil return ((c and x or u))+ 1 end, T) + s("g342", function(c) local x = nil return ((c and x or u))+ 1 end, F) + s("g343", function(c) local x = nil return ((c and x or u)).. "q" end, T) + s("g344", function(c) local x = nil return ((c and x or u)).. "q" end, F) + s("g345", function(c) local x = nil return ((c and x or u))& 1 end, T) + s("g346", function(c) local x = nil return ((c and x or u))& 1 end, F) + s("g347", function(c) local x = nil local q = ((c and x or u)) q.k = 1 end, T) + s("g348", function(c) local x = nil local q = ((c and x or u)) q.k = 1 end, F) + s("g349", function(c) local x = nil return ((c and nil or x)).z end, T) + s("g350", function(c) local x = nil return ((c and nil or x)).z end, F) + s("g351", function(c) local x = nil return ((c and nil or x))() end, T) + s("g352", function(c) local x = nil return ((c and nil or x))() end, F) + s("g353", function(c) local x = nil return ((c and nil or x))+ 1 end, T) + s("g354", function(c) local x = nil return ((c and nil or x))+ 1 end, F) + s("g355", function(c) local x = nil return ((c and nil or x)).. "q" end, T) + s("g356", function(c) local x = nil return ((c and nil or x)).. "q" end, F) + s("g357", function(c) local x = nil return ((c and nil or x))& 1 end, T) + s("g358", function(c) local x = nil return ((c and nil or x))& 1 end, F) + s("g359", function(c) local x = nil local q = ((c and nil or x)) q.k = 1 end, T) + s("g360", function(c) local x = nil local q = ((c and nil or x)) q.k = 1 end, F) + s("g361", function(c) local x = nil return ((c and nil or u)).z end, T) + s("g362", function(c) local x = nil return ((c and nil or u)).z end, F) + s("g363", function(c) local x = nil return ((c and nil or u))() end, T) + s("g364", function(c) local x = nil return ((c and nil or u))() end, F) + s("g365", function(c) local x = nil return ((c and nil or u))+ 1 end, T) + s("g366", function(c) local x = nil return ((c and nil or u))+ 1 end, F) + s("g367", function(c) local x = nil return ((c and nil or u)).. "q" end, T) + s("g368", function(c) local x = nil return ((c and nil or u)).. "q" end, F) + s("g369", function(c) local x = nil return ((c and nil or u))& 1 end, T) + s("g370", function(c) local x = nil return ((c and nil or u))& 1 end, F) + s("g371", function(c) local x = nil local q = ((c and nil or u)) q.k = 1 end, T) + s("g372", function(c) local x = nil local q = ((c and nil or u)) q.k = 1 end, F) + s("g373", function(c) local x = nil return ((x or x)).z end, T) + s("g374", function(c) local x = nil return ((x or x)).z end, F) + s("g375", function(c) local x = nil return ((x or x))() end, T) + s("g376", function(c) local x = nil return ((x or x))() end, F) + s("g377", function(c) local x = nil return ((x or x))+ 1 end, T) + s("g378", function(c) local x = nil return ((x or x))+ 1 end, F) + s("g379", function(c) local x = nil return ((x or x)).. "q" end, T) + s("g380", function(c) local x = nil return ((x or x)).. "q" end, F) + s("g381", function(c) local x = nil return ((x or x))& 1 end, T) + s("g382", function(c) local x = nil return ((x or x))& 1 end, F) + s("g383", function(c) local x = nil local q = ((x or x)) q.k = 1 end, T) + s("g384", function(c) local x = nil local q = ((x or x)) q.k = 1 end, F) + s("g385", function(c) local x = nil return ((x or nil)).z end, T) + s("g386", function(c) local x = nil return ((x or nil)).z end, F) + s("g387", function(c) local x = nil return ((x or nil))() end, T) + s("g388", function(c) local x = nil return ((x or nil))() end, F) + s("g389", function(c) local x = nil return ((x or nil))+ 1 end, T) + s("g390", function(c) local x = nil return ((x or nil))+ 1 end, F) + s("g391", function(c) local x = nil return ((x or nil)).. "q" end, T) + s("g392", function(c) local x = nil return ((x or nil)).. "q" end, F) + s("g393", function(c) local x = nil return ((x or nil))& 1 end, T) + s("g394", function(c) local x = nil return ((x or nil))& 1 end, F) + s("g395", function(c) local x = nil local q = ((x or nil)) q.k = 1 end, T) + s("g396", function(c) local x = nil local q = ((x or nil)) q.k = 1 end, F) + s("g397", function(c) local x = nil return ((nil or x)).z end, T) + s("g398", function(c) local x = nil return ((nil or x)).z end, F) + s("g399", function(c) local x = nil return ((nil or x))() end, T) + s("g400", function(c) local x = nil return ((nil or x))() end, F) + s("g401", function(c) local x = nil return ((nil or x))+ 1 end, T) + s("g402", function(c) local x = nil return ((nil or x))+ 1 end, F) + s("g403", function(c) local x = nil return ((nil or x)).. "q" end, T) + s("g404", function(c) local x = nil return ((nil or x)).. "q" end, F) + s("g405", function(c) local x = nil return ((nil or x))& 1 end, T) + s("g406", function(c) local x = nil return ((nil or x))& 1 end, F) + s("g407", function(c) local x = nil local q = ((nil or x)) q.k = 1 end, T) + s("g408", function(c) local x = nil local q = ((nil or x)) q.k = 1 end, F) + s("g409", function(c) local x = nil return ((c and t.a or x)).z end, T) + s("g410", function(c) local x = nil return ((c and t.a or x)).z end, F) + s("g411", function(c) local x = nil return ((c and t.a or x))() end, T) + s("g412", function(c) local x = nil return ((c and t.a or x))() end, F) + s("g413", function(c) local x = nil return ((c and t.a or x))+ 1 end, T) + s("g414", function(c) local x = nil return ((c and t.a or x))+ 1 end, F) + s("g415", function(c) local x = nil return ((c and t.a or x)).. "q" end, T) + s("g416", function(c) local x = nil return ((c and t.a or x)).. "q" end, F) + s("g417", function(c) local x = nil return ((c and t.a or x))& 1 end, T) + s("g418", function(c) local x = nil return ((c and t.a or x))& 1 end, F) + s("g419", function(c) local x = nil local q = ((c and t.a or x)) q.k = 1 end, T) + s("g420", function(c) local x = nil local q = ((c and t.a or x)) q.k = 1 end, F) + s("g421", function(c) local x = nil return ((c and "s" or x)).z end, T) + s("g422", function(c) local x = nil return ((c and "s" or x)).z end, F) + s("g423", function(c) local x = nil return ((c and "s" or x))() end, T) + s("g424", function(c) local x = nil return ((c and "s" or x))() end, F) + s("g425", function(c) local x = nil return ((c and "s" or x))+ 1 end, T) + s("g426", function(c) local x = nil return ((c and "s" or x))+ 1 end, F) + s("g427", function(c) local x = nil return ((c and "s" or x)).. "q" end, T) + s("g428", function(c) local x = nil return ((c and "s" or x)).. "q" end, F) + s("g429", function(c) local x = nil return ((c and "s" or x))& 1 end, T) + s("g430", function(c) local x = nil return ((c and "s" or x))& 1 end, F) + s("g431", function(c) local x = nil local q = ((c and "s" or x)) q.k = 1 end, T) + s("g432", function(c) local x = nil local q = ((c and "s" or x)) q.k = 1 end, F) + s("g433", function(c) local x = nil return ((c and 1 or x)).z end, T) + s("g434", function(c) local x = nil return ((c and 1 or x)).z end, F) + s("g435", function(c) local x = nil return ((c and 1 or x))() end, T) + s("g436", function(c) local x = nil return ((c and 1 or x))() end, F) + s("g437", function(c) local x = nil return ((c and 1 or x))+ 1 end, T) + s("g438", function(c) local x = nil return ((c and 1 or x))+ 1 end, F) + s("g439", function(c) local x = nil return ((c and 1 or x)).. "q" end, T) + s("g440", function(c) local x = nil return ((c and 1 or x)).. "q" end, F) + s("g441", function(c) local x = nil return ((c and 1 or x))& 1 end, T) + s("g442", function(c) local x = nil return ((c and 1 or x))& 1 end, F) + s("g443", function(c) local x = nil local q = ((c and 1 or x)) q.k = 1 end, T) + s("g444", function(c) local x = nil local q = ((c and 1 or x)) q.k = 1 end, F) + s("g445", function(c) local x = nil return (((c and x or u) or x)).z end, T) + s("g446", function(c) local x = nil return (((c and x or u) or x)).z end, F) + s("g447", function(c) local x = nil return (((c and x or u) or x))() end, T) + s("g448", function(c) local x = nil return (((c and x or u) or x))() end, F) + s("g449", function(c) local x = nil return (((c and x or u) or x))+ 1 end, T) + s("g450", function(c) local x = nil return (((c and x or u) or x))+ 1 end, F) + s("g451", function(c) local x = nil return (((c and x or u) or x)).. "q" end, T) + s("g452", function(c) local x = nil return (((c and x or u) or x)).. "q" end, F) + s("g453", function(c) local x = nil return (((c and x or u) or x))& 1 end, T) + s("g454", function(c) local x = nil return (((c and x or u) or x))& 1 end, F) + s("g455", function(c) local x = nil local q = (((c and x or u) or x)) q.k = 1 end, T) + s("g456", function(c) local x = nil local q = (((c and x or u) or x)) q.k = 1 end, F) + s("g457", function(c) local x = nil return ((c and (x or u) or x)).z end, T) + s("g458", function(c) local x = nil return ((c and (x or u) or x)).z end, F) + s("g459", function(c) local x = nil return ((c and (x or u) or x))() end, T) + s("g460", function(c) local x = nil return ((c and (x or u) or x))() end, F) + s("g461", function(c) local x = nil return ((c and (x or u) or x))+ 1 end, T) + s("g462", function(c) local x = nil return ((c and (x or u) or x))+ 1 end, F) + s("g463", function(c) local x = nil return ((c and (x or u) or x)).. "q" end, T) + s("g464", function(c) local x = nil return ((c and (x or u) or x)).. "q" end, F) + s("g465", function(c) local x = nil return ((c and (x or u) or x))& 1 end, T) + s("g466", function(c) local x = nil return ((c and (x or u) or x))& 1 end, F) + s("g467", function(c) local x = nil local q = ((c and (x or u) or x)) q.k = 1 end, T) + s("g468", function(c) local x = nil local q = ((c and (x or u) or x)) q.k = 1 end, F) + s("g469", function(c) local x = nil return ((c and gn or gn)).z end, T) + s("g470", function(c) local x = nil return ((c and gn or gn)).z end, F) + s("g471", function(c) local x = nil return ((c and gn or gn))() end, T) + s("g472", function(c) local x = nil return ((c and gn or gn))() end, F) + s("g473", function(c) local x = nil return ((c and gn or gn))+ 1 end, T) + s("g474", function(c) local x = nil return ((c and gn or gn))+ 1 end, F) + s("g475", function(c) local x = nil return ((c and gn or gn)).. "q" end, T) + s("g476", function(c) local x = nil return ((c and gn or gn)).. "q" end, F) + s("g477", function(c) local x = nil return ((c and gn or gn))& 1 end, T) + s("g478", function(c) local x = nil return ((c and gn or gn))& 1 end, F) + s("g479", function(c) local x = nil local q = ((c and gn or gn)) q.k = 1 end, T) + s("g480", function(c) local x = nil local q = ((c and gn or gn)) q.k = 1 end, F) + s("g481", function(c) local x = nil return ((c and u or u)).z end, T) + s("g482", function(c) local x = nil return ((c and u or u)).z end, F) + s("g483", function(c) local x = nil return ((c and u or u))() end, T) + s("g484", function(c) local x = nil return ((c and u or u))() end, F) + s("g485", function(c) local x = nil return ((c and u or u))+ 1 end, T) + s("g486", function(c) local x = nil return ((c and u or u))+ 1 end, F) + s("g487", function(c) local x = nil return ((c and u or u)).. "q" end, T) + s("g488", function(c) local x = nil return ((c and u or u)).. "q" end, F) + s("g489", function(c) local x = nil return ((c and u or u))& 1 end, T) + s("g490", function(c) local x = nil return ((c and u or u))& 1 end, F) + s("g491", function(c) local x = nil local q = ((c and u or u)) q.k = 1 end, T) + s("g492", function(c) local x = nil local q = ((c and u or u)) q.k = 1 end, F) + s("g493", function(c) local t2 = {} return (nil).z end, T) + s("g494", function(c) local t2 = {} return (nil).z end, F) + s("g495", function(c) local t2 = {} return (nil)() end, T) + s("g496", function(c) local t2 = {} return (nil)() end, F) + s("g497", function(c) local t2 = {} return (nil)+ 1 end, T) + s("g498", function(c) local t2 = {} return (nil)+ 1 end, F) + s("g499", function(c) local t2 = {} return (nil).. "q" end, T) + s("g500", function(c) local t2 = {} return (nil).. "q" end, F) + s("g501", function(c) local t2 = {} return (nil)& 1 end, T) + s("g502", function(c) local t2 = {} return (nil)& 1 end, F) + s("g503", function(c) local t2 = {} local q = (nil) q.k = 1 end, T) + s("g504", function(c) local t2 = {} local q = (nil) q.k = 1 end, F) + s("g505", function(c) local t2 = {} return ((nil)).z end, T) + s("g506", function(c) local t2 = {} return ((nil)).z end, F) + s("g507", function(c) local t2 = {} return ((nil))() end, T) + s("g508", function(c) local t2 = {} return ((nil))() end, F) + s("g509", function(c) local t2 = {} return ((nil))+ 1 end, T) + s("g510", function(c) local t2 = {} return ((nil))+ 1 end, F) + s("g511", function(c) local t2 = {} return ((nil)).. "q" end, T) + s("g512", function(c) local t2 = {} return ((nil)).. "q" end, F) + s("g513", function(c) local t2 = {} return ((nil))& 1 end, T) + s("g514", function(c) local t2 = {} return ((nil))& 1 end, F) + s("g515", function(c) local t2 = {} local q = ((nil)) q.k = 1 end, T) + s("g516", function(c) local t2 = {} local q = ((nil)) q.k = 1 end, F) + s("g517", function(c) local t2 = {} return (x).z end, T) + s("g518", function(c) local t2 = {} return (x).z end, F) + s("g519", function(c) local t2 = {} return (x)() end, T) + s("g520", function(c) local t2 = {} return (x)() end, F) + s("g521", function(c) local t2 = {} return (x)+ 1 end, T) + s("g522", function(c) local t2 = {} return (x)+ 1 end, F) + s("g523", function(c) local t2 = {} return (x).. "q" end, T) + s("g524", function(c) local t2 = {} return (x).. "q" end, F) + s("g525", function(c) local t2 = {} return (x)& 1 end, T) + s("g526", function(c) local t2 = {} return (x)& 1 end, F) + s("g527", function(c) local t2 = {} local q = (x) q.k = 1 end, T) + s("g528", function(c) local t2 = {} local q = (x) q.k = 1 end, F) + s("g529", function(c) local t2 = {} return ((x)).z end, T) + s("g530", function(c) local t2 = {} return ((x)).z end, F) + s("g531", function(c) local t2 = {} return ((x))() end, T) + s("g532", function(c) local t2 = {} return ((x))() end, F) + s("g533", function(c) local t2 = {} return ((x))+ 1 end, T) + s("g534", function(c) local t2 = {} return ((x))+ 1 end, F) + s("g535", function(c) local t2 = {} return ((x)).. "q" end, T) + s("g536", function(c) local t2 = {} return ((x)).. "q" end, F) + s("g537", function(c) local t2 = {} return ((x))& 1 end, T) + s("g538", function(c) local t2 = {} return ((x))& 1 end, F) + s("g539", function(c) local t2 = {} local q = ((x)) q.k = 1 end, T) + s("g540", function(c) local t2 = {} local q = ((x)) q.k = 1 end, F) + s("g541", function(c) local t2 = {} return (u).z end, T) + s("g542", function(c) local t2 = {} return (u).z end, F) + s("g543", function(c) local t2 = {} return (u)() end, T) + s("g544", function(c) local t2 = {} return (u)() end, F) + s("g545", function(c) local t2 = {} return (u)+ 1 end, T) + s("g546", function(c) local t2 = {} return (u)+ 1 end, F) + s("g547", function(c) local t2 = {} return (u).. "q" end, T) + s("g548", function(c) local t2 = {} return (u).. "q" end, F) + s("g549", function(c) local t2 = {} return (u)& 1 end, T) + s("g550", function(c) local t2 = {} return (u)& 1 end, F) + s("g551", function(c) local t2 = {} local q = (u) q.k = 1 end, T) + s("g552", function(c) local t2 = {} local q = (u) q.k = 1 end, F) + s("g553", function(c) local t2 = {} return ((u)).z end, T) + s("g554", function(c) local t2 = {} return ((u)).z end, F) + s("g555", function(c) local t2 = {} return ((u))() end, T) + s("g556", function(c) local t2 = {} return ((u))() end, F) + s("g557", function(c) local t2 = {} return ((u))+ 1 end, T) + s("g558", function(c) local t2 = {} return ((u))+ 1 end, F) + s("g559", function(c) local t2 = {} return ((u)).. "q" end, T) + s("g560", function(c) local t2 = {} return ((u)).. "q" end, F) + s("g561", function(c) local t2 = {} return ((u))& 1 end, T) + s("g562", function(c) local t2 = {} return ((u))& 1 end, F) + s("g563", function(c) local t2 = {} local q = ((u)) q.k = 1 end, T) + s("g564", function(c) local t2 = {} local q = ((u)) q.k = 1 end, F) + s("g565", function(c) local t2 = {} return (gn).z end, T) + s("g566", function(c) local t2 = {} return (gn).z end, F) + s("g567", function(c) local t2 = {} return (gn)() end, T) + s("g568", function(c) local t2 = {} return (gn)() end, F) + s("g569", function(c) local t2 = {} return (gn)+ 1 end, T) + s("g570", function(c) local t2 = {} return (gn)+ 1 end, F) + s("g571", function(c) local t2 = {} return (gn).. "q" end, T) + s("g572", function(c) local t2 = {} return (gn).. "q" end, F) + s("g573", function(c) local t2 = {} return (gn)& 1 end, T) + s("g574", function(c) local t2 = {} return (gn)& 1 end, F) + s("g575", function(c) local t2 = {} local q = (gn) q.k = 1 end, T) + s("g576", function(c) local t2 = {} local q = (gn) q.k = 1 end, F) + s("g577", function(c) local t2 = {} return ((gn)).z end, T) + s("g578", function(c) local t2 = {} return ((gn)).z end, F) + s("g579", function(c) local t2 = {} return ((gn))() end, T) + s("g580", function(c) local t2 = {} return ((gn))() end, F) + s("g581", function(c) local t2 = {} return ((gn))+ 1 end, T) + s("g582", function(c) local t2 = {} return ((gn))+ 1 end, F) + s("g583", function(c) local t2 = {} return ((gn)).. "q" end, T) + s("g584", function(c) local t2 = {} return ((gn)).. "q" end, F) + s("g585", function(c) local t2 = {} return ((gn))& 1 end, T) + s("g586", function(c) local t2 = {} return ((gn))& 1 end, F) + s("g587", function(c) local t2 = {} local q = ((gn)) q.k = 1 end, T) + s("g588", function(c) local t2 = {} local q = ((gn)) q.k = 1 end, F) + s("g589", function(c) local t2 = {} return (t.a).z end, T) + s("g590", function(c) local t2 = {} return (t.a).z end, F) + s("g591", function(c) local t2 = {} return (t.a)() end, T) + s("g592", function(c) local t2 = {} return (t.a)() end, F) + s("g593", function(c) local t2 = {} return (t.a)+ 1 end, T) + s("g594", function(c) local t2 = {} return (t.a)+ 1 end, F) + s("g595", function(c) local t2 = {} return (t.a).. "q" end, T) + s("g596", function(c) local t2 = {} return (t.a).. "q" end, F) + s("g597", function(c) local t2 = {} return (t.a)& 1 end, T) + s("g598", function(c) local t2 = {} return (t.a)& 1 end, F) + s("g599", function(c) local t2 = {} local q = (t.a) q.k = 1 end, T) + s("g600", function(c) local t2 = {} local q = (t.a) q.k = 1 end, F) + s("g601", function(c) local t2 = {} return ((t.a)).z end, T) + s("g602", function(c) local t2 = {} return ((t.a)).z end, F) + s("g603", function(c) local t2 = {} return ((t.a))() end, T) + s("g604", function(c) local t2 = {} return ((t.a))() end, F) + s("g605", function(c) local t2 = {} return ((t.a))+ 1 end, T) + s("g606", function(c) local t2 = {} return ((t.a))+ 1 end, F) + s("g607", function(c) local t2 = {} return ((t.a)).. "q" end, T) + s("g608", function(c) local t2 = {} return ((t.a)).. "q" end, F) + s("g609", function(c) local t2 = {} return ((t.a))& 1 end, T) + s("g610", function(c) local t2 = {} return ((t.a))& 1 end, F) + s("g611", function(c) local t2 = {} local q = ((t.a)) q.k = 1 end, T) + s("g612", function(c) local t2 = {} local q = ((t.a)) q.k = 1 end, F) + s("g613", function(c) local t2 = {} return ("s").z end, T) + s("g614", function(c) local t2 = {} return ("s").z end, F) + s("g615", function(c) local t2 = {} return ("s")() end, T) + s("g616", function(c) local t2 = {} return ("s")() end, F) + s("g617", function(c) local t2 = {} return ("s")+ 1 end, T) + s("g618", function(c) local t2 = {} return ("s")+ 1 end, F) + s("g619", function(c) local t2 = {} return ("s").. "q" end, T) + s("g620", function(c) local t2 = {} return ("s").. "q" end, F) + s("g621", function(c) local t2 = {} return ("s")& 1 end, T) + s("g622", function(c) local t2 = {} return ("s")& 1 end, F) + s("g623", function(c) local t2 = {} local q = ("s") q.k = 1 end, T) + s("g624", function(c) local t2 = {} local q = ("s") q.k = 1 end, F) + s("g625", function(c) local t2 = {} return (1).z end, T) + s("g626", function(c) local t2 = {} return (1).z end, F) + s("g627", function(c) local t2 = {} return (1)() end, T) + s("g628", function(c) local t2 = {} return (1)() end, F) + s("g629", function(c) local t2 = {} return (1)+ 1 end, T) + s("g630", function(c) local t2 = {} return (1)+ 1 end, F) + s("g631", function(c) local t2 = {} return (1).. "q" end, T) + s("g632", function(c) local t2 = {} return (1).. "q" end, F) + s("g633", function(c) local t2 = {} return (1)& 1 end, T) + s("g634", function(c) local t2 = {} return (1)& 1 end, F) + s("g635", function(c) local t2 = {} local q = (1) q.k = 1 end, T) + s("g636", function(c) local t2 = {} local q = (1) q.k = 1 end, F) + s("g637", function(c) local t2 = {} return ({}).z end, T) + s("g638", function(c) local t2 = {} return ({}).z end, F) + s("g639", function(c) local t2 = {} return ({})() end, T) + s("g640", function(c) local t2 = {} return ({})() end, F) + s("g641", function(c) local t2 = {} return ({})+ 1 end, T) + s("g642", function(c) local t2 = {} return ({})+ 1 end, F) + s("g643", function(c) local t2 = {} return ({}).. "q" end, T) + s("g644", function(c) local t2 = {} return ({}).. "q" end, F) + s("g645", function(c) local t2 = {} return ({})& 1 end, T) + s("g646", function(c) local t2 = {} return ({})& 1 end, F) + s("g647", function(c) local t2 = {} local q = ({}) q.k = 1 end, T) + s("g648", function(c) local t2 = {} local q = ({}) q.k = 1 end, F) + s("g649", function(c) local t2 = {} return ((c and x or x)).z end, T) + s("g650", function(c) local t2 = {} return ((c and x or x)).z end, F) + s("g651", function(c) local t2 = {} return ((c and x or x))() end, T) + s("g652", function(c) local t2 = {} return ((c and x or x))() end, F) + s("g653", function(c) local t2 = {} return ((c and x or x))+ 1 end, T) + s("g654", function(c) local t2 = {} return ((c and x or x))+ 1 end, F) + s("g655", function(c) local t2 = {} return ((c and x or x)).. "q" end, T) + s("g656", function(c) local t2 = {} return ((c and x or x)).. "q" end, F) + s("g657", function(c) local t2 = {} return ((c and x or x))& 1 end, T) + s("g658", function(c) local t2 = {} return ((c and x or x))& 1 end, F) + s("g659", function(c) local t2 = {} local q = ((c and x or x)) q.k = 1 end, T) + s("g660", function(c) local t2 = {} local q = ((c and x or x)) q.k = 1 end, F) + s("g661", function(c) local t2 = {} return ((c and x or u)).z end, T) + s("g662", function(c) local t2 = {} return ((c and x or u)).z end, F) + s("g663", function(c) local t2 = {} return ((c and x or u))() end, T) + s("g664", function(c) local t2 = {} return ((c and x or u))() end, F) + s("g665", function(c) local t2 = {} return ((c and x or u))+ 1 end, T) + s("g666", function(c) local t2 = {} return ((c and x or u))+ 1 end, F) + s("g667", function(c) local t2 = {} return ((c and x or u)).. "q" end, T) + s("g668", function(c) local t2 = {} return ((c and x or u)).. "q" end, F) + s("g669", function(c) local t2 = {} return ((c and x or u))& 1 end, T) + s("g670", function(c) local t2 = {} return ((c and x or u))& 1 end, F) + s("g671", function(c) local t2 = {} local q = ((c and x or u)) q.k = 1 end, T) + s("g672", function(c) local t2 = {} local q = ((c and x or u)) q.k = 1 end, F) + s("g673", function(c) local t2 = {} return ((c and nil or x)).z end, T) + s("g674", function(c) local t2 = {} return ((c and nil or x)).z end, F) + s("g675", function(c) local t2 = {} return ((c and nil or x))() end, T) + s("g676", function(c) local t2 = {} return ((c and nil or x))() end, F) + s("g677", function(c) local t2 = {} return ((c and nil or x))+ 1 end, T) + s("g678", function(c) local t2 = {} return ((c and nil or x))+ 1 end, F) + s("g679", function(c) local t2 = {} return ((c and nil or x)).. "q" end, T) + s("g680", function(c) local t2 = {} return ((c and nil or x)).. "q" end, F) + s("g681", function(c) local t2 = {} return ((c and nil or x))& 1 end, T) + s("g682", function(c) local t2 = {} return ((c and nil or x))& 1 end, F) + s("g683", function(c) local t2 = {} local q = ((c and nil or x)) q.k = 1 end, T) + s("g684", function(c) local t2 = {} local q = ((c and nil or x)) q.k = 1 end, F) + s("g685", function(c) local t2 = {} return ((c and nil or u)).z end, T) + s("g686", function(c) local t2 = {} return ((c and nil or u)).z end, F) + s("g687", function(c) local t2 = {} return ((c and nil or u))() end, T) + s("g688", function(c) local t2 = {} return ((c and nil or u))() end, F) + s("g689", function(c) local t2 = {} return ((c and nil or u))+ 1 end, T) + s("g690", function(c) local t2 = {} return ((c and nil or u))+ 1 end, F) + s("g691", function(c) local t2 = {} return ((c and nil or u)).. "q" end, T) + s("g692", function(c) local t2 = {} return ((c and nil or u)).. "q" end, F) + s("g693", function(c) local t2 = {} return ((c and nil or u))& 1 end, T) + s("g694", function(c) local t2 = {} return ((c and nil or u))& 1 end, F) + s("g695", function(c) local t2 = {} local q = ((c and nil or u)) q.k = 1 end, T) + s("g696", function(c) local t2 = {} local q = ((c and nil or u)) q.k = 1 end, F) + s("g697", function(c) local t2 = {} return ((x or x)).z end, T) + s("g698", function(c) local t2 = {} return ((x or x)).z end, F) + s("g699", function(c) local t2 = {} return ((x or x))() end, T) + s("g700", function(c) local t2 = {} return ((x or x))() end, F) + s("g701", function(c) local t2 = {} return ((x or x))+ 1 end, T) + s("g702", function(c) local t2 = {} return ((x or x))+ 1 end, F) + s("g703", function(c) local t2 = {} return ((x or x)).. "q" end, T) + s("g704", function(c) local t2 = {} return ((x or x)).. "q" end, F) + s("g705", function(c) local t2 = {} return ((x or x))& 1 end, T) + s("g706", function(c) local t2 = {} return ((x or x))& 1 end, F) + s("g707", function(c) local t2 = {} local q = ((x or x)) q.k = 1 end, T) + s("g708", function(c) local t2 = {} local q = ((x or x)) q.k = 1 end, F) + s("g709", function(c) local t2 = {} return ((x or nil)).z end, T) + s("g710", function(c) local t2 = {} return ((x or nil)).z end, F) + s("g711", function(c) local t2 = {} return ((x or nil))() end, T) + s("g712", function(c) local t2 = {} return ((x or nil))() end, F) + s("g713", function(c) local t2 = {} return ((x or nil))+ 1 end, T) + s("g714", function(c) local t2 = {} return ((x or nil))+ 1 end, F) + s("g715", function(c) local t2 = {} return ((x or nil)).. "q" end, T) + s("g716", function(c) local t2 = {} return ((x or nil)).. "q" end, F) + s("g717", function(c) local t2 = {} return ((x or nil))& 1 end, T) + s("g718", function(c) local t2 = {} return ((x or nil))& 1 end, F) + s("g719", function(c) local t2 = {} local q = ((x or nil)) q.k = 1 end, T) + s("g720", function(c) local t2 = {} local q = ((x or nil)) q.k = 1 end, F) + s("g721", function(c) local t2 = {} return ((nil or x)).z end, T) + s("g722", function(c) local t2 = {} return ((nil or x)).z end, F) + s("g723", function(c) local t2 = {} return ((nil or x))() end, T) + s("g724", function(c) local t2 = {} return ((nil or x))() end, F) + s("g725", function(c) local t2 = {} return ((nil or x))+ 1 end, T) + s("g726", function(c) local t2 = {} return ((nil or x))+ 1 end, F) + s("g727", function(c) local t2 = {} return ((nil or x)).. "q" end, T) + s("g728", function(c) local t2 = {} return ((nil or x)).. "q" end, F) + s("g729", function(c) local t2 = {} return ((nil or x))& 1 end, T) + s("g730", function(c) local t2 = {} return ((nil or x))& 1 end, F) + s("g731", function(c) local t2 = {} local q = ((nil or x)) q.k = 1 end, T) + s("g732", function(c) local t2 = {} local q = ((nil or x)) q.k = 1 end, F) + s("g733", function(c) local t2 = {} return ((c and t.a or x)).z end, T) + s("g734", function(c) local t2 = {} return ((c and t.a or x)).z end, F) + s("g735", function(c) local t2 = {} return ((c and t.a or x))() end, T) + s("g736", function(c) local t2 = {} return ((c and t.a or x))() end, F) + s("g737", function(c) local t2 = {} return ((c and t.a or x))+ 1 end, T) + s("g738", function(c) local t2 = {} return ((c and t.a or x))+ 1 end, F) + s("g739", function(c) local t2 = {} return ((c and t.a or x)).. "q" end, T) + s("g740", function(c) local t2 = {} return ((c and t.a or x)).. "q" end, F) + s("g741", function(c) local t2 = {} return ((c and t.a or x))& 1 end, T) + s("g742", function(c) local t2 = {} return ((c and t.a or x))& 1 end, F) + s("g743", function(c) local t2 = {} local q = ((c and t.a or x)) q.k = 1 end, T) + s("g744", function(c) local t2 = {} local q = ((c and t.a or x)) q.k = 1 end, F) + s("g745", function(c) local t2 = {} return ((c and "s" or x)).z end, T) + s("g746", function(c) local t2 = {} return ((c and "s" or x)).z end, F) + s("g747", function(c) local t2 = {} return ((c and "s" or x))() end, T) + s("g748", function(c) local t2 = {} return ((c and "s" or x))() end, F) + s("g749", function(c) local t2 = {} return ((c and "s" or x))+ 1 end, T) + s("g750", function(c) local t2 = {} return ((c and "s" or x))+ 1 end, F) + s("g751", function(c) local t2 = {} return ((c and "s" or x)).. "q" end, T) + s("g752", function(c) local t2 = {} return ((c and "s" or x)).. "q" end, F) + s("g753", function(c) local t2 = {} return ((c and "s" or x))& 1 end, T) + s("g754", function(c) local t2 = {} return ((c and "s" or x))& 1 end, F) + s("g755", function(c) local t2 = {} local q = ((c and "s" or x)) q.k = 1 end, T) + s("g756", function(c) local t2 = {} local q = ((c and "s" or x)) q.k = 1 end, F) + s("g757", function(c) local t2 = {} return ((c and 1 or x)).z end, T) + s("g758", function(c) local t2 = {} return ((c and 1 or x)).z end, F) + s("g759", function(c) local t2 = {} return ((c and 1 or x))() end, T) + s("g760", function(c) local t2 = {} return ((c and 1 or x))() end, F) + s("g761", function(c) local t2 = {} return ((c and 1 or x))+ 1 end, T) + s("g762", function(c) local t2 = {} return ((c and 1 or x))+ 1 end, F) + s("g763", function(c) local t2 = {} return ((c and 1 or x)).. "q" end, T) + s("g764", function(c) local t2 = {} return ((c and 1 or x)).. "q" end, F) + s("g765", function(c) local t2 = {} return ((c and 1 or x))& 1 end, T) + s("g766", function(c) local t2 = {} return ((c and 1 or x))& 1 end, F) + s("g767", function(c) local t2 = {} local q = ((c and 1 or x)) q.k = 1 end, T) + s("g768", function(c) local t2 = {} local q = ((c and 1 or x)) q.k = 1 end, F) + s("g769", function(c) local t2 = {} return (((c and x or u) or x)).z end, T) + s("g770", function(c) local t2 = {} return (((c and x or u) or x)).z end, F) + s("g771", function(c) local t2 = {} return (((c and x or u) or x))() end, T) + s("g772", function(c) local t2 = {} return (((c and x or u) or x))() end, F) + s("g773", function(c) local t2 = {} return (((c and x or u) or x))+ 1 end, T) + s("g774", function(c) local t2 = {} return (((c and x or u) or x))+ 1 end, F) + s("g775", function(c) local t2 = {} return (((c and x or u) or x)).. "q" end, T) + s("g776", function(c) local t2 = {} return (((c and x or u) or x)).. "q" end, F) + s("g777", function(c) local t2 = {} return (((c and x or u) or x))& 1 end, T) + s("g778", function(c) local t2 = {} return (((c and x or u) or x))& 1 end, F) + s("g779", function(c) local t2 = {} local q = (((c and x or u) or x)) q.k = 1 end, T) + s("g780", function(c) local t2 = {} local q = (((c and x or u) or x)) q.k = 1 end, F) + s("g781", function(c) local t2 = {} return ((c and (x or u) or x)).z end, T) + s("g782", function(c) local t2 = {} return ((c and (x or u) or x)).z end, F) + s("g783", function(c) local t2 = {} return ((c and (x or u) or x))() end, T) + s("g784", function(c) local t2 = {} return ((c and (x or u) or x))() end, F) + s("g785", function(c) local t2 = {} return ((c and (x or u) or x))+ 1 end, T) + s("g786", function(c) local t2 = {} return ((c and (x or u) or x))+ 1 end, F) + s("g787", function(c) local t2 = {} return ((c and (x or u) or x)).. "q" end, T) + s("g788", function(c) local t2 = {} return ((c and (x or u) or x)).. "q" end, F) + s("g789", function(c) local t2 = {} return ((c and (x or u) or x))& 1 end, T) + s("g790", function(c) local t2 = {} return ((c and (x or u) or x))& 1 end, F) + s("g791", function(c) local t2 = {} local q = ((c and (x or u) or x)) q.k = 1 end, T) + s("g792", function(c) local t2 = {} local q = ((c and (x or u) or x)) q.k = 1 end, F) + s("g793", function(c) local t2 = {} return ((c and t2 or t2)).z end, T) + s("g794", function(c) local t2 = {} return ((c and t2 or t2)).z end, F) + s("g795", function(c) local t2 = {} return ((c and t2 or t2))() end, T) + s("g796", function(c) local t2 = {} return ((c and t2 or t2))() end, F) + s("g797", function(c) local t2 = {} return ((c and t2 or t2))+ 1 end, T) + s("g798", function(c) local t2 = {} return ((c and t2 or t2))+ 1 end, F) + s("g799", function(c) local t2 = {} return ((c and t2 or t2)).. "q" end, T) + s("g800", function(c) local t2 = {} return ((c and t2 or t2)).. "q" end, F) + s("g801", function(c) local t2 = {} return ((c and t2 or t2))& 1 end, T) + s("g802", function(c) local t2 = {} return ((c and t2 or t2))& 1 end, F) + s("g803", function(c) local t2 = {} local q = ((c and t2 or t2)) q.k = 1 end, T) + s("g804", function(c) local t2 = {} local q = ((c and t2 or t2)) q.k = 1 end, F) + s("g805", function(c) local t2 = {} return ((c and gn or gn)).z end, T) + s("g806", function(c) local t2 = {} return ((c and gn or gn)).z end, F) + s("g807", function(c) local t2 = {} return ((c and gn or gn))() end, T) + s("g808", function(c) local t2 = {} return ((c and gn or gn))() end, F) + s("g809", function(c) local t2 = {} return ((c and gn or gn))+ 1 end, T) + s("g810", function(c) local t2 = {} return ((c and gn or gn))+ 1 end, F) + s("g811", function(c) local t2 = {} return ((c and gn or gn)).. "q" end, T) + s("g812", function(c) local t2 = {} return ((c and gn or gn)).. "q" end, F) + s("g813", function(c) local t2 = {} return ((c and gn or gn))& 1 end, T) + s("g814", function(c) local t2 = {} return ((c and gn or gn))& 1 end, F) + s("g815", function(c) local t2 = {} local q = ((c and gn or gn)) q.k = 1 end, T) + s("g816", function(c) local t2 = {} local q = ((c and gn or gn)) q.k = 1 end, F) + s("g817", function(c) local t2 = {} return ((c and u or u)).z end, T) + s("g818", function(c) local t2 = {} return ((c and u or u)).z end, F) + s("g819", function(c) local t2 = {} return ((c and u or u))() end, T) + s("g820", function(c) local t2 = {} return ((c and u or u))() end, F) + s("g821", function(c) local t2 = {} return ((c and u or u))+ 1 end, T) + s("g822", function(c) local t2 = {} return ((c and u or u))+ 1 end, F) + s("g823", function(c) local t2 = {} return ((c and u or u)).. "q" end, T) + s("g824", function(c) local t2 = {} return ((c and u or u)).. "q" end, F) + s("g825", function(c) local t2 = {} return ((c and u or u))& 1 end, T) + s("g826", function(c) local t2 = {} return ((c and u or u))& 1 end, F) + s("g827", function(c) local t2 = {} local q = ((c and u or u)) q.k = 1 end, T) + s("g828", function(c) local t2 = {} local q = ((c and u or u)) q.k = 1 end, F) + end + print("done") + """); + } + + /** + * 库函数 × 错误参数网格:{@code bad argument #N to 'f' (X expected, got Y)}。 + *

+ * 函数表是运行时枚举出来的({@code _G} + 九个库表),不手写清单—— + * 手写清单漏掉的函数不会被任何用例发现({@code warn} 整个函数没实现时 828 个 + * 用例全绿,就是这么来的)。108 个函数 × {0,1,2} 个实参 × 10 种值 = 2396 行。 + *

+ * 三处最容易错的判定被这片网格钉住: + *

    + *
  • 缺参报 {@code no value},不是 {@code nil}: + * {@code luaL_typename} 对越界下标给 {@code no value}。破坏它红 212 行。
  • + *
  • {@code lua_type} 对越界下标给 {@code LUA_TNONE},于是 + * 「nil 也算合法」的参数不会静默通过({@code setmetatable({})} 必须报 + * {@code nil or table expected, got no value})。红 1 行但那 1 行是 + * 唯一照到它的。
  • + *
  • 方法形式调用要 {@code arg--},减完是 0 就改报 + * {@code calling 'x' on bad self}。直接调用走不到这两条,所以另铺 + * 一组字面生成的 {@code (""):f(v)} / {@code (1):f(v)}(377 行): + * {@code OP_SELF} 只能由字面方法调用产生,运行时拼不出来。 + * 敏感度 75 / 22。
  • + *
+ *

+ * 两处归一化是刻意的:{@code math.atan} 与 {@code math.atan2} 是同一个 C 函数, + * {@code findfield} 反查到哪个取决于字符串哈希的 seed(每次启动都变);地址 + * 一律换成 {@code PTR}。 + *

+ * 黑名单里的函数不是「测不了」,是副作用不可控或输出随时间变: + * {@code print} 会把值打进 stdout、{@code io.write} 同理、{@code os.date}/ + * {@code os.time}/{@code os.clock} 读当前时间、{@code math.random} 随机、 + * {@code io.read}/{@code io.lines} 会读 stdin、{@code debug.sethook} 装了钩子 + * 不卸、{@code debug.setmetatable} 改的是全局类型元表、{@code collectgarbage} + * 的 {@code step}/{@code count} 是记录在案的有意偏离。它们各有专门的网格。 + */ + @Test + void libraryArgumentErrorGridMatchesReference() { + Diff.assertMatches(""" + -- 每个库函数 × 每种错误参数:枚举「库表里所有函数」×「0/1/2 个实参 × 9 种值」 + local function norm(e) + -- math.atan 与 math.atan2 是同一个 C 函数,findfield 反查到哪个取决于 + -- 字符串哈希的 seed(luai_makeseed,每次启动都变),故归一化成同一个名字 + return (tostring(e):gsub("0x%x+", "PTR"):gsub("math%.atan2", "math.atan")) + end + local function s(tag, f, ...) + local ok, err = pcall(f, ...) + print(tag, ok and "OK" or norm(err)) + end + + local BLACK = { + print = 1, ["os.exit"] = 1, ["os.remove"] = 1, ["os.rename"] = 1, + ["os.execute"] = 1, ["os.setlocale"] = 1, ["os.date"] = 1, ["os.time"] = 1, + ["os.clock"] = 1, ["math.random"] = 1, ["math.randomseed"] = 1, + ["io.open"] = 1, ["io.lines"] = 1, ["io.read"] = 1, ["io.close"] = 1, + ["io.output"] = 1, ["io.input"] = 1, ["io.popen"] = 1, ["io.write"] = 1, + ["debug.sethook"] = 1, ["debug.setmetatable"] = 1, ["debug.setlocal"] = 1, + ["debug.setupvalue"] = 1, collectgarbage = 1, ["package.loadlib"] = 1, + } + + local function dummy() end + local VALS = { nil, false, true, 0, 1, -1, 1.5, "s", "", {} } + local NV = 10 -- 别用 ipairs / #:VALS[1] 是 nil,会从第一条就截断 + local function fresh(v) if type(v) == "table" then return {} end return v end + + -- 收集:_G(base 库)+ 九个库表 + local names, fns = {}, {} + local function collect(prefix, t) + for k, v in pairs(t) do + if type(v) == "function" and type(k) == "string" and not BLACK[prefix .. k] then + names[#names + 1] = prefix .. k + fns[prefix .. k] = v + end + end + end + collect("", _G) + for _, nm in ipairs({ "string", "table", "math", "os", "io", "utf8", "coroutine", "debug", "package" }) do + collect(nm .. ".", _G[nm]) + end + table.sort(names) + print("nfns", #names) + + for _, n in ipairs(names) do + local f = fns[n] + s(n .. "|a0", f) + for i = 1, NV do + local v = VALS[i] + s(n .. "|a1|" .. i, f, fresh(v)) + end + for i = 1, NV do + local v = VALS[i] + s(n .. "|a2|" .. i, f, fresh(v), fresh(v)) + end + end + -- string 库函数名清单:新增函数会让这一行变化,逼着同步下面的字面块 + do + local ks = {} + for k, v in pairs(string) do if type(v) == "function" then ks[#ks + 1] = k end end + table.sort(ks) + print("strfns", table.concat(ks, ",")) + end + + -- 方法形式调用:self 不占参数编号(lauxlib.c: luaL_argerror 里的 arg--), + -- 且 self 自己类型不对时报 calling 'x' on bad self 而不是 bad argument。 + -- 必须用字面写法(OP_SELF 才成立),所以这一段由生成器铺开; + -- 接收者要加括号:字符串字面量不是 prefixexp,(""):len() 才合法。 + s("m_str_byte|1", function() return (""):byte(nil) end) + s("m_str_byte|2", function() return (""):byte(false) end) + s("m_str_byte|3", function() return (""):byte(true) end) + s("m_str_byte|4", function() return (""):byte(0) end) + s("m_str_byte|5", function() return (""):byte(1) end) + s("m_str_byte|6", function() return (""):byte(-1) end) + s("m_str_byte|7", function() return (""):byte(1.5) end) + s("m_str_byte|8", function() return (""):byte("s") end) + s("m_str_byte|9", function() return (""):byte("") end) + s("m_str_byte|10", function() return (""):byte({}) end) + s("m_str_byte|a0", function() return (""):byte() end) + s("m_str_char|1", function() return (""):char(nil) end) + s("m_str_char|2", function() return (""):char(false) end) + s("m_str_char|3", function() return (""):char(true) end) + s("m_str_char|4", function() return (""):char(0) end) + s("m_str_char|5", function() return (""):char(1) end) + s("m_str_char|6", function() return (""):char(-1) end) + s("m_str_char|7", function() return (""):char(1.5) end) + s("m_str_char|8", function() return (""):char("s") end) + s("m_str_char|9", function() return (""):char("") end) + s("m_str_char|10", function() return (""):char({}) end) + s("m_str_char|a0", function() return (""):char() end) + s("m_str_dump|1", function() return (""):dump(nil) end) + s("m_str_dump|2", function() return (""):dump(false) end) + s("m_str_dump|3", function() return (""):dump(true) end) + s("m_str_dump|4", function() return (""):dump(0) end) + s("m_str_dump|5", function() return (""):dump(1) end) + s("m_str_dump|6", function() return (""):dump(-1) end) + s("m_str_dump|7", function() return (""):dump(1.5) end) + s("m_str_dump|8", function() return (""):dump("s") end) + s("m_str_dump|9", function() return (""):dump("") end) + s("m_str_dump|10", function() return (""):dump({}) end) + s("m_str_dump|a0", function() return (""):dump() end) + s("m_str_find|1", function() return (""):find(nil) end) + s("m_str_find|2", function() return (""):find(false) end) + s("m_str_find|3", function() return (""):find(true) end) + s("m_str_find|4", function() return (""):find(0) end) + s("m_str_find|5", function() return (""):find(1) end) + s("m_str_find|6", function() return (""):find(-1) end) + s("m_str_find|7", function() return (""):find(1.5) end) + s("m_str_find|8", function() return (""):find("s") end) + s("m_str_find|9", function() return (""):find("") end) + s("m_str_find|10", function() return (""):find({}) end) + s("m_str_find|a0", function() return (""):find() end) + s("m_str_format|1", function() return (""):format(nil) end) + s("m_str_format|2", function() return (""):format(false) end) + s("m_str_format|3", function() return (""):format(true) end) + s("m_str_format|4", function() return (""):format(0) end) + s("m_str_format|5", function() return (""):format(1) end) + s("m_str_format|6", function() return (""):format(-1) end) + s("m_str_format|7", function() return (""):format(1.5) end) + s("m_str_format|8", function() return (""):format("s") end) + s("m_str_format|9", function() return (""):format("") end) + s("m_str_format|10", function() return (""):format({}) end) + s("m_str_format|a0", function() return (""):format() end) + s("m_str_gmatch|1", function() return (""):gmatch(nil) end) + s("m_str_gmatch|2", function() return (""):gmatch(false) end) + s("m_str_gmatch|3", function() return (""):gmatch(true) end) + s("m_str_gmatch|4", function() return (""):gmatch(0) end) + s("m_str_gmatch|5", function() return (""):gmatch(1) end) + s("m_str_gmatch|6", function() return (""):gmatch(-1) end) + s("m_str_gmatch|7", function() return (""):gmatch(1.5) end) + s("m_str_gmatch|8", function() return (""):gmatch("s") end) + s("m_str_gmatch|9", function() return (""):gmatch("") end) + s("m_str_gmatch|10", function() return (""):gmatch({}) end) + s("m_str_gmatch|a0", function() return (""):gmatch() end) + s("m_str_gsub|1", function() return (""):gsub(nil) end) + s("m_str_gsub|2", function() return (""):gsub(false) end) + s("m_str_gsub|3", function() return (""):gsub(true) end) + s("m_str_gsub|4", function() return (""):gsub(0) end) + s("m_str_gsub|5", function() return (""):gsub(1) end) + s("m_str_gsub|6", function() return (""):gsub(-1) end) + s("m_str_gsub|7", function() return (""):gsub(1.5) end) + s("m_str_gsub|8", function() return (""):gsub("s") end) + s("m_str_gsub|9", function() return (""):gsub("") end) + s("m_str_gsub|10", function() return (""):gsub({}) end) + s("m_str_gsub|a0", function() return (""):gsub() end) + s("m_str_len|1", function() return (""):len(nil) end) + s("m_str_len|2", function() return (""):len(false) end) + s("m_str_len|3", function() return (""):len(true) end) + s("m_str_len|4", function() return (""):len(0) end) + s("m_str_len|5", function() return (""):len(1) end) + s("m_str_len|6", function() return (""):len(-1) end) + s("m_str_len|7", function() return (""):len(1.5) end) + s("m_str_len|8", function() return (""):len("s") end) + s("m_str_len|9", function() return (""):len("") end) + s("m_str_len|10", function() return (""):len({}) end) + s("m_str_len|a0", function() return (""):len() end) + s("m_str_lower|1", function() return (""):lower(nil) end) + s("m_str_lower|2", function() return (""):lower(false) end) + s("m_str_lower|3", function() return (""):lower(true) end) + s("m_str_lower|4", function() return (""):lower(0) end) + s("m_str_lower|5", function() return (""):lower(1) end) + s("m_str_lower|6", function() return (""):lower(-1) end) + s("m_str_lower|7", function() return (""):lower(1.5) end) + s("m_str_lower|8", function() return (""):lower("s") end) + s("m_str_lower|9", function() return (""):lower("") end) + s("m_str_lower|10", function() return (""):lower({}) end) + s("m_str_lower|a0", function() return (""):lower() end) + s("m_str_match|1", function() return (""):match(nil) end) + s("m_str_match|2", function() return (""):match(false) end) + s("m_str_match|3", function() return (""):match(true) end) + s("m_str_match|4", function() return (""):match(0) end) + s("m_str_match|5", function() return (""):match(1) end) + s("m_str_match|6", function() return (""):match(-1) end) + s("m_str_match|7", function() return (""):match(1.5) end) + s("m_str_match|8", function() return (""):match("s") end) + s("m_str_match|9", function() return (""):match("") end) + s("m_str_match|10", function() return (""):match({}) end) + s("m_str_match|a0", function() return (""):match() end) + s("m_str_pack|1", function() return (""):pack(nil) end) + s("m_str_pack|2", function() return (""):pack(false) end) + s("m_str_pack|3", function() return (""):pack(true) end) + s("m_str_pack|4", function() return (""):pack(0) end) + s("m_str_pack|5", function() return (""):pack(1) end) + s("m_str_pack|6", function() return (""):pack(-1) end) + s("m_str_pack|7", function() return (""):pack(1.5) end) + s("m_str_pack|8", function() return (""):pack("s") end) + s("m_str_pack|9", function() return (""):pack("") end) + s("m_str_pack|10", function() return (""):pack({}) end) + s("m_str_pack|a0", function() return (""):pack() end) + s("m_str_packsize|1", function() return (""):packsize(nil) end) + s("m_str_packsize|2", function() return (""):packsize(false) end) + s("m_str_packsize|3", function() return (""):packsize(true) end) + s("m_str_packsize|4", function() return (""):packsize(0) end) + s("m_str_packsize|5", function() return (""):packsize(1) end) + s("m_str_packsize|6", function() return (""):packsize(-1) end) + s("m_str_packsize|7", function() return (""):packsize(1.5) end) + s("m_str_packsize|8", function() return (""):packsize("s") end) + s("m_str_packsize|9", function() return (""):packsize("") end) + s("m_str_packsize|10", function() return (""):packsize({}) end) + s("m_str_packsize|a0", function() return (""):packsize() end) + s("m_str_rep|1", function() return (""):rep(nil) end) + s("m_str_rep|2", function() return (""):rep(false) end) + s("m_str_rep|3", function() return (""):rep(true) end) + s("m_str_rep|4", function() return (""):rep(0) end) + s("m_str_rep|5", function() return (""):rep(1) end) + s("m_str_rep|6", function() return (""):rep(-1) end) + s("m_str_rep|7", function() return (""):rep(1.5) end) + s("m_str_rep|8", function() return (""):rep("s") end) + s("m_str_rep|9", function() return (""):rep("") end) + s("m_str_rep|10", function() return (""):rep({}) end) + s("m_str_rep|a0", function() return (""):rep() end) + s("m_str_reverse|1", function() return (""):reverse(nil) end) + s("m_str_reverse|2", function() return (""):reverse(false) end) + s("m_str_reverse|3", function() return (""):reverse(true) end) + s("m_str_reverse|4", function() return (""):reverse(0) end) + s("m_str_reverse|5", function() return (""):reverse(1) end) + s("m_str_reverse|6", function() return (""):reverse(-1) end) + s("m_str_reverse|7", function() return (""):reverse(1.5) end) + s("m_str_reverse|8", function() return (""):reverse("s") end) + s("m_str_reverse|9", function() return (""):reverse("") end) + s("m_str_reverse|10", function() return (""):reverse({}) end) + s("m_str_reverse|a0", function() return (""):reverse() end) + s("m_str_sub|1", function() return (""):sub(nil) end) + s("m_str_sub|2", function() return (""):sub(false) end) + s("m_str_sub|3", function() return (""):sub(true) end) + s("m_str_sub|4", function() return (""):sub(0) end) + s("m_str_sub|5", function() return (""):sub(1) end) + s("m_str_sub|6", function() return (""):sub(-1) end) + s("m_str_sub|7", function() return (""):sub(1.5) end) + s("m_str_sub|8", function() return (""):sub("s") end) + s("m_str_sub|9", function() return (""):sub("") end) + s("m_str_sub|10", function() return (""):sub({}) end) + s("m_str_sub|a0", function() return (""):sub() end) + s("m_str_unpack|1", function() return (""):unpack(nil) end) + s("m_str_unpack|2", function() return (""):unpack(false) end) + s("m_str_unpack|3", function() return (""):unpack(true) end) + s("m_str_unpack|4", function() return (""):unpack(0) end) + s("m_str_unpack|5", function() return (""):unpack(1) end) + s("m_str_unpack|6", function() return (""):unpack(-1) end) + s("m_str_unpack|7", function() return (""):unpack(1.5) end) + s("m_str_unpack|8", function() return (""):unpack("s") end) + s("m_str_unpack|9", function() return (""):unpack("") end) + s("m_str_unpack|10", function() return (""):unpack({}) end) + s("m_str_unpack|a0", function() return (""):unpack() end) + s("m_str_upper|1", function() return (""):upper(nil) end) + s("m_str_upper|2", function() return (""):upper(false) end) + s("m_str_upper|3", function() return (""):upper(true) end) + s("m_str_upper|4", function() return (""):upper(0) end) + s("m_str_upper|5", function() return (""):upper(1) end) + s("m_str_upper|6", function() return (""):upper(-1) end) + s("m_str_upper|7", function() return (""):upper(1.5) end) + s("m_str_upper|8", function() return (""):upper("s") end) + s("m_str_upper|9", function() return (""):upper("") end) + s("m_str_upper|10", function() return (""):upper({}) end) + s("m_str_upper|a0", function() return (""):upper() end) + s("m_num_byte|1", function() return (1):byte(nil) end) + s("m_num_byte|2", function() return (1):byte(false) end) + s("m_num_byte|3", function() return (1):byte(true) end) + s("m_num_byte|4", function() return (1):byte(0) end) + s("m_num_byte|5", function() return (1):byte(1) end) + s("m_num_byte|6", function() return (1):byte(-1) end) + s("m_num_byte|7", function() return (1):byte(1.5) end) + s("m_num_byte|8", function() return (1):byte("s") end) + s("m_num_byte|9", function() return (1):byte("") end) + s("m_num_byte|10", function() return (1):byte({}) end) + s("m_num_byte|a0", function() return (1):byte() end) + s("m_num_char|1", function() return (1):char(nil) end) + s("m_num_char|2", function() return (1):char(false) end) + s("m_num_char|3", function() return (1):char(true) end) + s("m_num_char|4", function() return (1):char(0) end) + s("m_num_char|5", function() return (1):char(1) end) + s("m_num_char|6", function() return (1):char(-1) end) + s("m_num_char|7", function() return (1):char(1.5) end) + s("m_num_char|8", function() return (1):char("s") end) + s("m_num_char|9", function() return (1):char("") end) + s("m_num_char|10", function() return (1):char({}) end) + s("m_num_char|a0", function() return (1):char() end) + s("m_num_dump|1", function() return (1):dump(nil) end) + s("m_num_dump|2", function() return (1):dump(false) end) + s("m_num_dump|3", function() return (1):dump(true) end) + s("m_num_dump|4", function() return (1):dump(0) end) + s("m_num_dump|5", function() return (1):dump(1) end) + s("m_num_dump|6", function() return (1):dump(-1) end) + s("m_num_dump|7", function() return (1):dump(1.5) end) + s("m_num_dump|8", function() return (1):dump("s") end) + s("m_num_dump|9", function() return (1):dump("") end) + s("m_num_dump|10", function() return (1):dump({}) end) + s("m_num_dump|a0", function() return (1):dump() end) + s("m_num_find|1", function() return (1):find(nil) end) + s("m_num_find|2", function() return (1):find(false) end) + s("m_num_find|3", function() return (1):find(true) end) + s("m_num_find|4", function() return (1):find(0) end) + s("m_num_find|5", function() return (1):find(1) end) + s("m_num_find|6", function() return (1):find(-1) end) + s("m_num_find|7", function() return (1):find(1.5) end) + s("m_num_find|8", function() return (1):find("s") end) + s("m_num_find|9", function() return (1):find("") end) + s("m_num_find|10", function() return (1):find({}) end) + s("m_num_find|a0", function() return (1):find() end) + s("m_num_format|1", function() return (1):format(nil) end) + s("m_num_format|2", function() return (1):format(false) end) + s("m_num_format|3", function() return (1):format(true) end) + s("m_num_format|4", function() return (1):format(0) end) + s("m_num_format|5", function() return (1):format(1) end) + s("m_num_format|6", function() return (1):format(-1) end) + s("m_num_format|7", function() return (1):format(1.5) end) + s("m_num_format|8", function() return (1):format("s") end) + s("m_num_format|9", function() return (1):format("") end) + s("m_num_format|10", function() return (1):format({}) end) + s("m_num_format|a0", function() return (1):format() end) + s("m_num_gmatch|1", function() return (1):gmatch(nil) end) + s("m_num_gmatch|2", function() return (1):gmatch(false) end) + s("m_num_gmatch|3", function() return (1):gmatch(true) end) + s("m_num_gmatch|4", function() return (1):gmatch(0) end) + s("m_num_gmatch|5", function() return (1):gmatch(1) end) + s("m_num_gmatch|6", function() return (1):gmatch(-1) end) + s("m_num_gmatch|7", function() return (1):gmatch(1.5) end) + s("m_num_gmatch|8", function() return (1):gmatch("s") end) + s("m_num_gmatch|9", function() return (1):gmatch("") end) + s("m_num_gmatch|10", function() return (1):gmatch({}) end) + s("m_num_gmatch|a0", function() return (1):gmatch() end) + s("m_num_gsub|1", function() return (1):gsub(nil) end) + s("m_num_gsub|2", function() return (1):gsub(false) end) + s("m_num_gsub|3", function() return (1):gsub(true) end) + s("m_num_gsub|4", function() return (1):gsub(0) end) + s("m_num_gsub|5", function() return (1):gsub(1) end) + s("m_num_gsub|6", function() return (1):gsub(-1) end) + s("m_num_gsub|7", function() return (1):gsub(1.5) end) + s("m_num_gsub|8", function() return (1):gsub("s") end) + s("m_num_gsub|9", function() return (1):gsub("") end) + s("m_num_gsub|10", function() return (1):gsub({}) end) + s("m_num_gsub|a0", function() return (1):gsub() end) + s("m_num_len|1", function() return (1):len(nil) end) + s("m_num_len|2", function() return (1):len(false) end) + s("m_num_len|3", function() return (1):len(true) end) + s("m_num_len|4", function() return (1):len(0) end) + s("m_num_len|5", function() return (1):len(1) end) + s("m_num_len|6", function() return (1):len(-1) end) + s("m_num_len|7", function() return (1):len(1.5) end) + s("m_num_len|8", function() return (1):len("s") end) + s("m_num_len|9", function() return (1):len("") end) + s("m_num_len|10", function() return (1):len({}) end) + s("m_num_len|a0", function() return (1):len() end) + s("m_num_lower|1", function() return (1):lower(nil) end) + s("m_num_lower|2", function() return (1):lower(false) end) + s("m_num_lower|3", function() return (1):lower(true) end) + s("m_num_lower|4", function() return (1):lower(0) end) + s("m_num_lower|5", function() return (1):lower(1) end) + s("m_num_lower|6", function() return (1):lower(-1) end) + s("m_num_lower|7", function() return (1):lower(1.5) end) + s("m_num_lower|8", function() return (1):lower("s") end) + s("m_num_lower|9", function() return (1):lower("") end) + s("m_num_lower|10", function() return (1):lower({}) end) + s("m_num_lower|a0", function() return (1):lower() end) + s("m_num_match|1", function() return (1):match(nil) end) + s("m_num_match|2", function() return (1):match(false) end) + s("m_num_match|3", function() return (1):match(true) end) + s("m_num_match|4", function() return (1):match(0) end) + s("m_num_match|5", function() return (1):match(1) end) + s("m_num_match|6", function() return (1):match(-1) end) + s("m_num_match|7", function() return (1):match(1.5) end) + s("m_num_match|8", function() return (1):match("s") end) + s("m_num_match|9", function() return (1):match("") end) + s("m_num_match|10", function() return (1):match({}) end) + s("m_num_match|a0", function() return (1):match() end) + s("m_num_pack|1", function() return (1):pack(nil) end) + s("m_num_pack|2", function() return (1):pack(false) end) + s("m_num_pack|3", function() return (1):pack(true) end) + s("m_num_pack|4", function() return (1):pack(0) end) + s("m_num_pack|5", function() return (1):pack(1) end) + s("m_num_pack|6", function() return (1):pack(-1) end) + s("m_num_pack|7", function() return (1):pack(1.5) end) + s("m_num_pack|8", function() return (1):pack("s") end) + s("m_num_pack|9", function() return (1):pack("") end) + s("m_num_pack|10", function() return (1):pack({}) end) + s("m_num_pack|a0", function() return (1):pack() end) + s("m_num_packsize|1", function() return (1):packsize(nil) end) + s("m_num_packsize|2", function() return (1):packsize(false) end) + s("m_num_packsize|3", function() return (1):packsize(true) end) + s("m_num_packsize|4", function() return (1):packsize(0) end) + s("m_num_packsize|5", function() return (1):packsize(1) end) + s("m_num_packsize|6", function() return (1):packsize(-1) end) + s("m_num_packsize|7", function() return (1):packsize(1.5) end) + s("m_num_packsize|8", function() return (1):packsize("s") end) + s("m_num_packsize|9", function() return (1):packsize("") end) + s("m_num_packsize|10", function() return (1):packsize({}) end) + s("m_num_packsize|a0", function() return (1):packsize() end) + s("m_num_rep|1", function() return (1):rep(nil) end) + s("m_num_rep|2", function() return (1):rep(false) end) + s("m_num_rep|3", function() return (1):rep(true) end) + s("m_num_rep|4", function() return (1):rep(0) end) + s("m_num_rep|5", function() return (1):rep(1) end) + s("m_num_rep|6", function() return (1):rep(-1) end) + s("m_num_rep|7", function() return (1):rep(1.5) end) + s("m_num_rep|8", function() return (1):rep("s") end) + s("m_num_rep|9", function() return (1):rep("") end) + s("m_num_rep|10", function() return (1):rep({}) end) + s("m_num_rep|a0", function() return (1):rep() end) + s("m_num_reverse|1", function() return (1):reverse(nil) end) + s("m_num_reverse|2", function() return (1):reverse(false) end) + s("m_num_reverse|3", function() return (1):reverse(true) end) + s("m_num_reverse|4", function() return (1):reverse(0) end) + s("m_num_reverse|5", function() return (1):reverse(1) end) + s("m_num_reverse|6", function() return (1):reverse(-1) end) + s("m_num_reverse|7", function() return (1):reverse(1.5) end) + s("m_num_reverse|8", function() return (1):reverse("s") end) + s("m_num_reverse|9", function() return (1):reverse("") end) + s("m_num_reverse|10", function() return (1):reverse({}) end) + s("m_num_reverse|a0", function() return (1):reverse() end) + s("m_num_sub|1", function() return (1):sub(nil) end) + s("m_num_sub|2", function() return (1):sub(false) end) + s("m_num_sub|3", function() return (1):sub(true) end) + s("m_num_sub|4", function() return (1):sub(0) end) + s("m_num_sub|5", function() return (1):sub(1) end) + s("m_num_sub|6", function() return (1):sub(-1) end) + s("m_num_sub|7", function() return (1):sub(1.5) end) + s("m_num_sub|8", function() return (1):sub("s") end) + s("m_num_sub|9", function() return (1):sub("") end) + s("m_num_sub|10", function() return (1):sub({}) end) + s("m_num_sub|a0", function() return (1):sub() end) + s("m_num_unpack|1", function() return (1):unpack(nil) end) + s("m_num_unpack|2", function() return (1):unpack(false) end) + s("m_num_unpack|3", function() return (1):unpack(true) end) + s("m_num_unpack|4", function() return (1):unpack(0) end) + s("m_num_unpack|5", function() return (1):unpack(1) end) + s("m_num_unpack|6", function() return (1):unpack(-1) end) + s("m_num_unpack|7", function() return (1):unpack(1.5) end) + s("m_num_unpack|8", function() return (1):unpack("s") end) + s("m_num_unpack|9", function() return (1):unpack("") end) + s("m_num_unpack|10", function() return (1):unpack({}) end) + s("m_num_unpack|a0", function() return (1):unpack() end) + s("m_num_upper|1", function() return (1):upper(nil) end) + s("m_num_upper|2", function() return (1):upper(false) end) + s("m_num_upper|3", function() return (1):upper(true) end) + s("m_num_upper|4", function() return (1):upper(0) end) + s("m_num_upper|5", function() return (1):upper(1) end) + s("m_num_upper|6", function() return (1):upper(-1) end) + s("m_num_upper|7", function() return (1):upper(1.5) end) + s("m_num_upper|8", function() return (1):upper("s") end) + s("m_num_upper|9", function() return (1):upper("") end) + s("m_num_upper|10", function() return (1):upper({}) end) + s("m_num_upper|a0", function() return (1):upper() end) + print("done") + """); + } + + /** + * 同一片网格的第三层:合法参数返回什么。 + *

+ * 上一片只在失败时打 {@code OK},成功时把返回值全丢了——而「返回值的类型」是 + * 一类完全独立的错:{@code math.abs(-1)} 返回 整数 {@code 1} 而不是浮点 + * {@code 1.0},{@code tostring} 出来一个是 {@code 1} 一个是 {@code 1.0}。 + * 把 {@code math.abs} 的整数分支改成走浮点,上一片网格 一行都不红, + * 这一片红 6 行。反过来也成立:{@code math.type} 返回值的破坏在这一片红 2 行。 + *

+ * 106 个函数 × {0,1,2} 个实参 × 10 种值,2729 行输出里有 959 行是成功调用, + * 覆盖 139 种不同的返回值形状({@code nil} / {@code 0.0} / {@code table: PTR} / + * {@code thread: PTR,true} / {@code file (PTR)} …)。 + *

+ * 两个函数因返回值不可比而排除:{@code os.tmpname} 给随机文件名; + * {@code debug.traceback} 的返回值里含 {@code stack traceback:},打印出来会被 + * {@code Diff.assertMatches} 从那儿把后面全部砍掉(见那条「回溯落在中间」 + * 的老教训),等于让这条用例之后的内容一行也不比。 + */ + @Test + void libraryReturnValueGridMatchesReference() { + Diff.assertMatches(""" + -- 每个库函数 × 每种错误参数:枚举「库表里所有函数」×「0/1/2 个实参 × 9 种值」 + local function norm(e) + -- math.atan 与 math.atan2 是同一个 C 函数,findfield 反查到哪个取决于 + -- 字符串哈希的 seed(luai_makeseed,每次启动都变),故归一化成同一个名字 + return (tostring(e):gsub("0x%x+", "PTR"):gsub("math%.atan2", "math.atan")) + end + local function show(...) + local n = select('#', ...) + local t = {} + for i = 1, n do t[i] = norm(tostring((select(i, ...)))) end + return table.concat(t, ",") + end + local function s(tag, f, ...) + local r = table.pack(pcall(f, ...)) + if not r[1] then + print(tag, norm(tostring(r[2]))) + else + print(tag, "OK", show(table.unpack(r, 2, r.n))) + end + end + + local BLACK = { + print = 1, ["os.exit"] = 1, ["os.remove"] = 1, ["os.rename"] = 1, + ["os.execute"] = 1, ["os.setlocale"] = 1, ["os.date"] = 1, ["os.time"] = 1, + ["os.clock"] = 1, ["math.random"] = 1, ["math.randomseed"] = 1, + ["io.open"] = 1, ["io.lines"] = 1, ["io.read"] = 1, ["io.close"] = 1, + ["io.output"] = 1, ["io.input"] = 1, ["io.popen"] = 1, ["io.write"] = 1, + ["debug.sethook"] = 1, ["debug.setmetatable"] = 1, ["debug.setlocal"] = 1, + ["debug.setupvalue"] = 1, collectgarbage = 1, ["package.loadlib"] = 1, + -- 下面这几个是「返回值不可比」而不是副作用: + ["debug.traceback"] = 1, -- 返回含 stack traceback: 的串,会被归一化整段砍掉 + ["os.tmpname"] = 1, -- 随机文件名 + } + + local function dummy() end + local VALS = { nil, false, true, 0, 1, -1, 1.5, "s", "", {} } + local NV = 10 -- 别用 ipairs / #:VALS[1] 是 nil,会从第一条就截断 + local function fresh(v) if type(v) == "table" then return {} end return v end + + -- 收集:_G(base 库)+ 九个库表 + local names, fns = {}, {} + local function collect(prefix, t) + for k, v in pairs(t) do + if type(v) == "function" and type(k) == "string" and not BLACK[prefix .. k] then + names[#names + 1] = prefix .. k + fns[prefix .. k] = v + end + end + end + collect("", _G) + for _, nm in ipairs({ "string", "table", "math", "os", "io", "utf8", "coroutine", "debug", "package" }) do + collect(nm .. ".", _G[nm]) + end + table.sort(names) + print("nfns", #names) + + for _, n in ipairs(names) do + local f = fns[n] + s(n .. "|a0", f) + for i = 1, NV do + local v = VALS[i] + s(n .. "|a1|" .. i, f, fresh(v)) + end + for i = 1, NV do + local v = VALS[i] + s(n .. "|a2|" .. i, f, fresh(v), fresh(v)) + end + end + -- string 库函数名清单:新增函数会让这一行变化,逼着同步下面的字面块 + do + local ks = {} + for k, v in pairs(string) do if type(v) == "function" then ks[#ks + 1] = k end end + table.sort(ks) + print("strfns", table.concat(ks, ",")) + end + + -- 方法形式调用:self 不占参数编号(lauxlib.c: luaL_argerror 里的 arg--), + -- 且 self 自己类型不对时报 calling 'x' on bad self 而不是 bad argument。 + -- 必须用字面写法(OP_SELF 才成立),所以这一段由生成器铺开; + -- 接收者要加括号:字符串字面量不是 prefixexp,(""):len() 才合法。 + s("m_str_byte|1", function() return (""):byte(nil) end) + s("m_str_byte|2", function() return (""):byte(false) end) + s("m_str_byte|3", function() return (""):byte(true) end) + s("m_str_byte|4", function() return (""):byte(0) end) + s("m_str_byte|5", function() return (""):byte(1) end) + s("m_str_byte|6", function() return (""):byte(-1) end) + s("m_str_byte|7", function() return (""):byte(1.5) end) + s("m_str_byte|8", function() return (""):byte("s") end) + s("m_str_byte|9", function() return (""):byte("") end) + s("m_str_byte|10", function() return (""):byte({}) end) + s("m_str_byte|a0", function() return (""):byte() end) + s("m_str_char|1", function() return (""):char(nil) end) + s("m_str_char|2", function() return (""):char(false) end) + s("m_str_char|3", function() return (""):char(true) end) + s("m_str_char|4", function() return (""):char(0) end) + s("m_str_char|5", function() return (""):char(1) end) + s("m_str_char|6", function() return (""):char(-1) end) + s("m_str_char|7", function() return (""):char(1.5) end) + s("m_str_char|8", function() return (""):char("s") end) + s("m_str_char|9", function() return (""):char("") end) + s("m_str_char|10", function() return (""):char({}) end) + s("m_str_char|a0", function() return (""):char() end) + s("m_str_dump|1", function() return (""):dump(nil) end) + s("m_str_dump|2", function() return (""):dump(false) end) + s("m_str_dump|3", function() return (""):dump(true) end) + s("m_str_dump|4", function() return (""):dump(0) end) + s("m_str_dump|5", function() return (""):dump(1) end) + s("m_str_dump|6", function() return (""):dump(-1) end) + s("m_str_dump|7", function() return (""):dump(1.5) end) + s("m_str_dump|8", function() return (""):dump("s") end) + s("m_str_dump|9", function() return (""):dump("") end) + s("m_str_dump|10", function() return (""):dump({}) end) + s("m_str_dump|a0", function() return (""):dump() end) + s("m_str_find|1", function() return (""):find(nil) end) + s("m_str_find|2", function() return (""):find(false) end) + s("m_str_find|3", function() return (""):find(true) end) + s("m_str_find|4", function() return (""):find(0) end) + s("m_str_find|5", function() return (""):find(1) end) + s("m_str_find|6", function() return (""):find(-1) end) + s("m_str_find|7", function() return (""):find(1.5) end) + s("m_str_find|8", function() return (""):find("s") end) + s("m_str_find|9", function() return (""):find("") end) + s("m_str_find|10", function() return (""):find({}) end) + s("m_str_find|a0", function() return (""):find() end) + s("m_str_format|1", function() return (""):format(nil) end) + s("m_str_format|2", function() return (""):format(false) end) + s("m_str_format|3", function() return (""):format(true) end) + s("m_str_format|4", function() return (""):format(0) end) + s("m_str_format|5", function() return (""):format(1) end) + s("m_str_format|6", function() return (""):format(-1) end) + s("m_str_format|7", function() return (""):format(1.5) end) + s("m_str_format|8", function() return (""):format("s") end) + s("m_str_format|9", function() return (""):format("") end) + s("m_str_format|10", function() return (""):format({}) end) + s("m_str_format|a0", function() return (""):format() end) + s("m_str_gmatch|1", function() return (""):gmatch(nil) end) + s("m_str_gmatch|2", function() return (""):gmatch(false) end) + s("m_str_gmatch|3", function() return (""):gmatch(true) end) + s("m_str_gmatch|4", function() return (""):gmatch(0) end) + s("m_str_gmatch|5", function() return (""):gmatch(1) end) + s("m_str_gmatch|6", function() return (""):gmatch(-1) end) + s("m_str_gmatch|7", function() return (""):gmatch(1.5) end) + s("m_str_gmatch|8", function() return (""):gmatch("s") end) + s("m_str_gmatch|9", function() return (""):gmatch("") end) + s("m_str_gmatch|10", function() return (""):gmatch({}) end) + s("m_str_gmatch|a0", function() return (""):gmatch() end) + s("m_str_gsub|1", function() return (""):gsub(nil) end) + s("m_str_gsub|2", function() return (""):gsub(false) end) + s("m_str_gsub|3", function() return (""):gsub(true) end) + s("m_str_gsub|4", function() return (""):gsub(0) end) + s("m_str_gsub|5", function() return (""):gsub(1) end) + s("m_str_gsub|6", function() return (""):gsub(-1) end) + s("m_str_gsub|7", function() return (""):gsub(1.5) end) + s("m_str_gsub|8", function() return (""):gsub("s") end) + s("m_str_gsub|9", function() return (""):gsub("") end) + s("m_str_gsub|10", function() return (""):gsub({}) end) + s("m_str_gsub|a0", function() return (""):gsub() end) + s("m_str_len|1", function() return (""):len(nil) end) + s("m_str_len|2", function() return (""):len(false) end) + s("m_str_len|3", function() return (""):len(true) end) + s("m_str_len|4", function() return (""):len(0) end) + s("m_str_len|5", function() return (""):len(1) end) + s("m_str_len|6", function() return (""):len(-1) end) + s("m_str_len|7", function() return (""):len(1.5) end) + s("m_str_len|8", function() return (""):len("s") end) + s("m_str_len|9", function() return (""):len("") end) + s("m_str_len|10", function() return (""):len({}) end) + s("m_str_len|a0", function() return (""):len() end) + s("m_str_lower|1", function() return (""):lower(nil) end) + s("m_str_lower|2", function() return (""):lower(false) end) + s("m_str_lower|3", function() return (""):lower(true) end) + s("m_str_lower|4", function() return (""):lower(0) end) + s("m_str_lower|5", function() return (""):lower(1) end) + s("m_str_lower|6", function() return (""):lower(-1) end) + s("m_str_lower|7", function() return (""):lower(1.5) end) + s("m_str_lower|8", function() return (""):lower("s") end) + s("m_str_lower|9", function() return (""):lower("") end) + s("m_str_lower|10", function() return (""):lower({}) end) + s("m_str_lower|a0", function() return (""):lower() end) + s("m_str_match|1", function() return (""):match(nil) end) + s("m_str_match|2", function() return (""):match(false) end) + s("m_str_match|3", function() return (""):match(true) end) + s("m_str_match|4", function() return (""):match(0) end) + s("m_str_match|5", function() return (""):match(1) end) + s("m_str_match|6", function() return (""):match(-1) end) + s("m_str_match|7", function() return (""):match(1.5) end) + s("m_str_match|8", function() return (""):match("s") end) + s("m_str_match|9", function() return (""):match("") end) + s("m_str_match|10", function() return (""):match({}) end) + s("m_str_match|a0", function() return (""):match() end) + s("m_str_pack|1", function() return (""):pack(nil) end) + s("m_str_pack|2", function() return (""):pack(false) end) + s("m_str_pack|3", function() return (""):pack(true) end) + s("m_str_pack|4", function() return (""):pack(0) end) + s("m_str_pack|5", function() return (""):pack(1) end) + s("m_str_pack|6", function() return (""):pack(-1) end) + s("m_str_pack|7", function() return (""):pack(1.5) end) + s("m_str_pack|8", function() return (""):pack("s") end) + s("m_str_pack|9", function() return (""):pack("") end) + s("m_str_pack|10", function() return (""):pack({}) end) + s("m_str_pack|a0", function() return (""):pack() end) + s("m_str_packsize|1", function() return (""):packsize(nil) end) + s("m_str_packsize|2", function() return (""):packsize(false) end) + s("m_str_packsize|3", function() return (""):packsize(true) end) + s("m_str_packsize|4", function() return (""):packsize(0) end) + s("m_str_packsize|5", function() return (""):packsize(1) end) + s("m_str_packsize|6", function() return (""):packsize(-1) end) + s("m_str_packsize|7", function() return (""):packsize(1.5) end) + s("m_str_packsize|8", function() return (""):packsize("s") end) + s("m_str_packsize|9", function() return (""):packsize("") end) + s("m_str_packsize|10", function() return (""):packsize({}) end) + s("m_str_packsize|a0", function() return (""):packsize() end) + s("m_str_rep|1", function() return (""):rep(nil) end) + s("m_str_rep|2", function() return (""):rep(false) end) + s("m_str_rep|3", function() return (""):rep(true) end) + s("m_str_rep|4", function() return (""):rep(0) end) + s("m_str_rep|5", function() return (""):rep(1) end) + s("m_str_rep|6", function() return (""):rep(-1) end) + s("m_str_rep|7", function() return (""):rep(1.5) end) + s("m_str_rep|8", function() return (""):rep("s") end) + s("m_str_rep|9", function() return (""):rep("") end) + s("m_str_rep|10", function() return (""):rep({}) end) + s("m_str_rep|a0", function() return (""):rep() end) + s("m_str_reverse|1", function() return (""):reverse(nil) end) + s("m_str_reverse|2", function() return (""):reverse(false) end) + s("m_str_reverse|3", function() return (""):reverse(true) end) + s("m_str_reverse|4", function() return (""):reverse(0) end) + s("m_str_reverse|5", function() return (""):reverse(1) end) + s("m_str_reverse|6", function() return (""):reverse(-1) end) + s("m_str_reverse|7", function() return (""):reverse(1.5) end) + s("m_str_reverse|8", function() return (""):reverse("s") end) + s("m_str_reverse|9", function() return (""):reverse("") end) + s("m_str_reverse|10", function() return (""):reverse({}) end) + s("m_str_reverse|a0", function() return (""):reverse() end) + s("m_str_sub|1", function() return (""):sub(nil) end) + s("m_str_sub|2", function() return (""):sub(false) end) + s("m_str_sub|3", function() return (""):sub(true) end) + s("m_str_sub|4", function() return (""):sub(0) end) + s("m_str_sub|5", function() return (""):sub(1) end) + s("m_str_sub|6", function() return (""):sub(-1) end) + s("m_str_sub|7", function() return (""):sub(1.5) end) + s("m_str_sub|8", function() return (""):sub("s") end) + s("m_str_sub|9", function() return (""):sub("") end) + s("m_str_sub|10", function() return (""):sub({}) end) + s("m_str_sub|a0", function() return (""):sub() end) + s("m_str_unpack|1", function() return (""):unpack(nil) end) + s("m_str_unpack|2", function() return (""):unpack(false) end) + s("m_str_unpack|3", function() return (""):unpack(true) end) + s("m_str_unpack|4", function() return (""):unpack(0) end) + s("m_str_unpack|5", function() return (""):unpack(1) end) + s("m_str_unpack|6", function() return (""):unpack(-1) end) + s("m_str_unpack|7", function() return (""):unpack(1.5) end) + s("m_str_unpack|8", function() return (""):unpack("s") end) + s("m_str_unpack|9", function() return (""):unpack("") end) + s("m_str_unpack|10", function() return (""):unpack({}) end) + s("m_str_unpack|a0", function() return (""):unpack() end) + s("m_str_upper|1", function() return (""):upper(nil) end) + s("m_str_upper|2", function() return (""):upper(false) end) + s("m_str_upper|3", function() return (""):upper(true) end) + s("m_str_upper|4", function() return (""):upper(0) end) + s("m_str_upper|5", function() return (""):upper(1) end) + s("m_str_upper|6", function() return (""):upper(-1) end) + s("m_str_upper|7", function() return (""):upper(1.5) end) + s("m_str_upper|8", function() return (""):upper("s") end) + s("m_str_upper|9", function() return (""):upper("") end) + s("m_str_upper|10", function() return (""):upper({}) end) + s("m_str_upper|a0", function() return (""):upper() end) + s("m_num_byte|1", function() return (1):byte(nil) end) + s("m_num_byte|2", function() return (1):byte(false) end) + s("m_num_byte|3", function() return (1):byte(true) end) + s("m_num_byte|4", function() return (1):byte(0) end) + s("m_num_byte|5", function() return (1):byte(1) end) + s("m_num_byte|6", function() return (1):byte(-1) end) + s("m_num_byte|7", function() return (1):byte(1.5) end) + s("m_num_byte|8", function() return (1):byte("s") end) + s("m_num_byte|9", function() return (1):byte("") end) + s("m_num_byte|10", function() return (1):byte({}) end) + s("m_num_byte|a0", function() return (1):byte() end) + s("m_num_char|1", function() return (1):char(nil) end) + s("m_num_char|2", function() return (1):char(false) end) + s("m_num_char|3", function() return (1):char(true) end) + s("m_num_char|4", function() return (1):char(0) end) + s("m_num_char|5", function() return (1):char(1) end) + s("m_num_char|6", function() return (1):char(-1) end) + s("m_num_char|7", function() return (1):char(1.5) end) + s("m_num_char|8", function() return (1):char("s") end) + s("m_num_char|9", function() return (1):char("") end) + s("m_num_char|10", function() return (1):char({}) end) + s("m_num_char|a0", function() return (1):char() end) + s("m_num_dump|1", function() return (1):dump(nil) end) + s("m_num_dump|2", function() return (1):dump(false) end) + s("m_num_dump|3", function() return (1):dump(true) end) + s("m_num_dump|4", function() return (1):dump(0) end) + s("m_num_dump|5", function() return (1):dump(1) end) + s("m_num_dump|6", function() return (1):dump(-1) end) + s("m_num_dump|7", function() return (1):dump(1.5) end) + s("m_num_dump|8", function() return (1):dump("s") end) + s("m_num_dump|9", function() return (1):dump("") end) + s("m_num_dump|10", function() return (1):dump({}) end) + s("m_num_dump|a0", function() return (1):dump() end) + s("m_num_find|1", function() return (1):find(nil) end) + s("m_num_find|2", function() return (1):find(false) end) + s("m_num_find|3", function() return (1):find(true) end) + s("m_num_find|4", function() return (1):find(0) end) + s("m_num_find|5", function() return (1):find(1) end) + s("m_num_find|6", function() return (1):find(-1) end) + s("m_num_find|7", function() return (1):find(1.5) end) + s("m_num_find|8", function() return (1):find("s") end) + s("m_num_find|9", function() return (1):find("") end) + s("m_num_find|10", function() return (1):find({}) end) + s("m_num_find|a0", function() return (1):find() end) + s("m_num_format|1", function() return (1):format(nil) end) + s("m_num_format|2", function() return (1):format(false) end) + s("m_num_format|3", function() return (1):format(true) end) + s("m_num_format|4", function() return (1):format(0) end) + s("m_num_format|5", function() return (1):format(1) end) + s("m_num_format|6", function() return (1):format(-1) end) + s("m_num_format|7", function() return (1):format(1.5) end) + s("m_num_format|8", function() return (1):format("s") end) + s("m_num_format|9", function() return (1):format("") end) + s("m_num_format|10", function() return (1):format({}) end) + s("m_num_format|a0", function() return (1):format() end) + s("m_num_gmatch|1", function() return (1):gmatch(nil) end) + s("m_num_gmatch|2", function() return (1):gmatch(false) end) + s("m_num_gmatch|3", function() return (1):gmatch(true) end) + s("m_num_gmatch|4", function() return (1):gmatch(0) end) + s("m_num_gmatch|5", function() return (1):gmatch(1) end) + s("m_num_gmatch|6", function() return (1):gmatch(-1) end) + s("m_num_gmatch|7", function() return (1):gmatch(1.5) end) + s("m_num_gmatch|8", function() return (1):gmatch("s") end) + s("m_num_gmatch|9", function() return (1):gmatch("") end) + s("m_num_gmatch|10", function() return (1):gmatch({}) end) + s("m_num_gmatch|a0", function() return (1):gmatch() end) + s("m_num_gsub|1", function() return (1):gsub(nil) end) + s("m_num_gsub|2", function() return (1):gsub(false) end) + s("m_num_gsub|3", function() return (1):gsub(true) end) + s("m_num_gsub|4", function() return (1):gsub(0) end) + s("m_num_gsub|5", function() return (1):gsub(1) end) + s("m_num_gsub|6", function() return (1):gsub(-1) end) + s("m_num_gsub|7", function() return (1):gsub(1.5) end) + s("m_num_gsub|8", function() return (1):gsub("s") end) + s("m_num_gsub|9", function() return (1):gsub("") end) + s("m_num_gsub|10", function() return (1):gsub({}) end) + s("m_num_gsub|a0", function() return (1):gsub() end) + s("m_num_len|1", function() return (1):len(nil) end) + s("m_num_len|2", function() return (1):len(false) end) + s("m_num_len|3", function() return (1):len(true) end) + s("m_num_len|4", function() return (1):len(0) end) + s("m_num_len|5", function() return (1):len(1) end) + s("m_num_len|6", function() return (1):len(-1) end) + s("m_num_len|7", function() return (1):len(1.5) end) + s("m_num_len|8", function() return (1):len("s") end) + s("m_num_len|9", function() return (1):len("") end) + s("m_num_len|10", function() return (1):len({}) end) + s("m_num_len|a0", function() return (1):len() end) + s("m_num_lower|1", function() return (1):lower(nil) end) + s("m_num_lower|2", function() return (1):lower(false) end) + s("m_num_lower|3", function() return (1):lower(true) end) + s("m_num_lower|4", function() return (1):lower(0) end) + s("m_num_lower|5", function() return (1):lower(1) end) + s("m_num_lower|6", function() return (1):lower(-1) end) + s("m_num_lower|7", function() return (1):lower(1.5) end) + s("m_num_lower|8", function() return (1):lower("s") end) + s("m_num_lower|9", function() return (1):lower("") end) + s("m_num_lower|10", function() return (1):lower({}) end) + s("m_num_lower|a0", function() return (1):lower() end) + s("m_num_match|1", function() return (1):match(nil) end) + s("m_num_match|2", function() return (1):match(false) end) + s("m_num_match|3", function() return (1):match(true) end) + s("m_num_match|4", function() return (1):match(0) end) + s("m_num_match|5", function() return (1):match(1) end) + s("m_num_match|6", function() return (1):match(-1) end) + s("m_num_match|7", function() return (1):match(1.5) end) + s("m_num_match|8", function() return (1):match("s") end) + s("m_num_match|9", function() return (1):match("") end) + s("m_num_match|10", function() return (1):match({}) end) + s("m_num_match|a0", function() return (1):match() end) + s("m_num_pack|1", function() return (1):pack(nil) end) + s("m_num_pack|2", function() return (1):pack(false) end) + s("m_num_pack|3", function() return (1):pack(true) end) + s("m_num_pack|4", function() return (1):pack(0) end) + s("m_num_pack|5", function() return (1):pack(1) end) + s("m_num_pack|6", function() return (1):pack(-1) end) + s("m_num_pack|7", function() return (1):pack(1.5) end) + s("m_num_pack|8", function() return (1):pack("s") end) + s("m_num_pack|9", function() return (1):pack("") end) + s("m_num_pack|10", function() return (1):pack({}) end) + s("m_num_pack|a0", function() return (1):pack() end) + s("m_num_packsize|1", function() return (1):packsize(nil) end) + s("m_num_packsize|2", function() return (1):packsize(false) end) + s("m_num_packsize|3", function() return (1):packsize(true) end) + s("m_num_packsize|4", function() return (1):packsize(0) end) + s("m_num_packsize|5", function() return (1):packsize(1) end) + s("m_num_packsize|6", function() return (1):packsize(-1) end) + s("m_num_packsize|7", function() return (1):packsize(1.5) end) + s("m_num_packsize|8", function() return (1):packsize("s") end) + s("m_num_packsize|9", function() return (1):packsize("") end) + s("m_num_packsize|10", function() return (1):packsize({}) end) + s("m_num_packsize|a0", function() return (1):packsize() end) + s("m_num_rep|1", function() return (1):rep(nil) end) + s("m_num_rep|2", function() return (1):rep(false) end) + s("m_num_rep|3", function() return (1):rep(true) end) + s("m_num_rep|4", function() return (1):rep(0) end) + s("m_num_rep|5", function() return (1):rep(1) end) + s("m_num_rep|6", function() return (1):rep(-1) end) + s("m_num_rep|7", function() return (1):rep(1.5) end) + s("m_num_rep|8", function() return (1):rep("s") end) + s("m_num_rep|9", function() return (1):rep("") end) + s("m_num_rep|10", function() return (1):rep({}) end) + s("m_num_rep|a0", function() return (1):rep() end) + s("m_num_reverse|1", function() return (1):reverse(nil) end) + s("m_num_reverse|2", function() return (1):reverse(false) end) + s("m_num_reverse|3", function() return (1):reverse(true) end) + s("m_num_reverse|4", function() return (1):reverse(0) end) + s("m_num_reverse|5", function() return (1):reverse(1) end) + s("m_num_reverse|6", function() return (1):reverse(-1) end) + s("m_num_reverse|7", function() return (1):reverse(1.5) end) + s("m_num_reverse|8", function() return (1):reverse("s") end) + s("m_num_reverse|9", function() return (1):reverse("") end) + s("m_num_reverse|10", function() return (1):reverse({}) end) + s("m_num_reverse|a0", function() return (1):reverse() end) + s("m_num_sub|1", function() return (1):sub(nil) end) + s("m_num_sub|2", function() return (1):sub(false) end) + s("m_num_sub|3", function() return (1):sub(true) end) + s("m_num_sub|4", function() return (1):sub(0) end) + s("m_num_sub|5", function() return (1):sub(1) end) + s("m_num_sub|6", function() return (1):sub(-1) end) + s("m_num_sub|7", function() return (1):sub(1.5) end) + s("m_num_sub|8", function() return (1):sub("s") end) + s("m_num_sub|9", function() return (1):sub("") end) + s("m_num_sub|10", function() return (1):sub({}) end) + s("m_num_sub|a0", function() return (1):sub() end) + s("m_num_unpack|1", function() return (1):unpack(nil) end) + s("m_num_unpack|2", function() return (1):unpack(false) end) + s("m_num_unpack|3", function() return (1):unpack(true) end) + s("m_num_unpack|4", function() return (1):unpack(0) end) + s("m_num_unpack|5", function() return (1):unpack(1) end) + s("m_num_unpack|6", function() return (1):unpack(-1) end) + s("m_num_unpack|7", function() return (1):unpack(1.5) end) + s("m_num_unpack|8", function() return (1):unpack("s") end) + s("m_num_unpack|9", function() return (1):unpack("") end) + s("m_num_unpack|10", function() return (1):unpack({}) end) + s("m_num_unpack|a0", function() return (1):unpack() end) + s("m_num_upper|1", function() return (1):upper(nil) end) + s("m_num_upper|2", function() return (1):upper(false) end) + s("m_num_upper|3", function() return (1):upper(true) end) + s("m_num_upper|4", function() return (1):upper(0) end) + s("m_num_upper|5", function() return (1):upper(1) end) + s("m_num_upper|6", function() return (1):upper(-1) end) + s("m_num_upper|7", function() return (1):upper(1.5) end) + s("m_num_upper|8", function() return (1):upper("s") end) + s("m_num_upper|9", function() return (1):upper("") end) + s("m_num_upper|10", function() return (1):upper({}) end) + s("m_num_upper|a0", function() return (1):upper() end) + print("done") + """); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/StringLibTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/StringLibTest.java new file mode 100644 index 0000000..cc3ec47 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/StringLibTest.java @@ -0,0 +1,468 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; + +/** + * 字符串库的差分用例 —— 阶段 7 的第一道闸门。 + *

+ * 选例的着眼点是「照着 C 源码写会写错的那些地方」,不是把每个函数点一遍: + *

    + *
  • 逐字节而非逐字符:{@code #s}、{@code byte}、{@code sub}、{@code rep} + * 都要能处理含 {@code \0} 与非法 UTF-8 的串;
  • + *
  • 相对下标的夹取:{@code sub(0)}、{@code sub(-3)}、{@code byte(100)};
  • + *
  • 模式匹配的边界:锚点、{@code %} 结尾、{@code %b}、{%code %f}、 + * 位置捕获、{@code gsub} 的「没改动就返回原串」;
  • + *
  • {@code format}:{@code %g} 去尾零、{@code %a} 的平局舍入、 + * {@code %q} 的转义、{@code %c} 写单字节。
  • + *
+ */ +class StringLibTest { + + @TestFactory + List stringScriptsMatchReference() { + List tests = new ArrayList<>(); + add(tests, "str", SCRIPTS); + add(tests, "fmt", FORMAT); + add(tests, "pat", PATTERN); + add(tests, "err", ERRORS); + return tests; + } + + private static void add(List tests, String tag, String[] srcs) { + for (int i = 0; i < srcs.length; i++) { + String src = srcs[i]; + int n = i; + tests.add(DynamicTest.dynamicTest(tag + "[" + n + "]", () -> Diff.assertMatches(src))); + } + } + + private static final String[] SCRIPTS = { + + """ + print(("Hello"):len(), ("Hello"):upper(), ("Hello"):lower(), ("abc"):reverse()) + print(string.len(42), string.len(""), #"abc") + """, + + """ + print(("abcdef"):sub(2,3), ("abcdef"):sub(-3), ("abcdef"):sub(0), ("abcdef"):sub(5,3)) + print(("abcdef"):sub(2,-2), ("abcdef"):sub(-100), ("abcdef"):sub(100)) + print(("abcdef"):sub(2,3,4)) + """, + + """ + print(("x"):rep(3, "-")) + print(string.rep("ab", 0), string.rep("ab", -1)) + print(string.rep("a", 3, "sep")) + print(#string.rep("ab", 10, "--")) + """, + + """ + print(("abc"):byte(1,-1)) + print(("abc"):byte(0), ("abc"):byte(100), ("abc"):byte(1,1)) + print(("abc"):byte(2)) + print(string.char(72,101,108,108,111)) + print(#string.char(0, 255, 10)) + print(("a\\0b"):len(), ("a\\0b"):byte(1,3)) + print(("\\xff\\xfe"):byte(1,2)) + """, + + """ + -- 数字参数会被转换(luaL_check* 那一套的转换规则) + print(string.sub("abcdef", "2", "4")) + print(string.rep("ab", "2")) + print(("x"):rep(3, 2)) + print(string.byte(65.0)) + print(pcall(string.byte, 65)) + """, + + """ + -- __index = string:方法式调用 + print(("abc"):upper(), ("ABC"):lower(), ("abc"):len()) + print(("a,b,c"):gsub(",", ";")) + local f = ("abc").upper + print(f("xyz")) + print(("abc"):sub(2)) + """, + + """ + -- 字符串照旧参与算术(元表里的 __add 等还在) + print("10" + 1, "3" * "4", -"7") + print(("10"):len() + 1) + """, + }; + + private static final String[] PATTERN = { + + """ + print(string.find("hello world", "o w")) + print(string.find("hello", "l+")) + print(string.find("hello", "l+", 4)) + print(string.find("abc", "b", 1, true)) + print(string.find("a.b", ".", 1, true)) + print(string.find("abc", ".b")) + print(string.find("abc", "")) + print(string.find("", "")) + print(string.find("", "x")) + print(("hello"):find("^h")) + print(("hello"):find("l+", 5)) + print(string.find("abc", "b", 1, false)) + """, + + """ + print(string.match("hello world", "(%w+) (%w+)")) + print(string.match("hello=world", "(%w+)=(%w+)")) + print(string.match("2024-01-02", "(%d+)-(%d+)-(%d+)")) + print(string.match("abc", "(b)(c)")) + print(string.match("hello", "()l()")) + print(string.match("abc", "x")) + print(("hello"):match("^()l")) + """, + + """ + print(string.gsub("hello world", "o", "0")) + print(string.gsub("hello world", "%w+", "<%0>")) + print(string.gsub("hello", "()l()", "[%1,%2]")) + print(string.gsub("hello", "l", function(c) return c:upper() end)) + print(string.gsub("hello", "%w", {h="H", e="E"})) + print(string.gsub("hello", "l", false)) + print(string.gsub("hello", "l", nil)) + print(string.gsub("hello", "z", "y")) + print(("aaa"):gsub("a", "b", 2)) + print(("aaa"):gsub("a", "b", 0)) + print(("abc"):gsub("", "-")) + print(string.gsub("hello", "l", 5)) + """, + + """ + local acc = "" + for w in ("one two three"):gmatch("%a+") do acc = acc .. w .. "," end + print(acc) + local acc2 = "" + for k, v in ("a=1, b=2"):gmatch("(%w+)=(%w+)") do acc2 = acc2 .. k .. "->" .. v .. " " end + print(acc2) + local acc3 = "" + for w in ("abc"):gmatch("") do acc3 = acc3 .. "[" .. w .. "]" end + print(acc3) + local n = 0 + for _ in ("aaa"):gmatch("a") do n = n + 1 end + print(n) + print(("hello"):gmatch("l+")()) + """, + + """ + -- %b 与字符类 + print(string.match("(a(b)c)", "%b()")) + print(string.match("xxyy", "%b<>")) + print(string.gsub("(a(b)c)", "%b()", "X")) + print(string.match("hello world", "%a+")) + print(string.match("42", "%d+")) + print(string.match(" x", "%s+x")) + print(string.match("HELLO", "%u+")) + print(string.match("a1b2", "%w+")) + print(string.match("z", "%A")) + print(string.match("123", "%D")) + print(string.match("abc", "[a-c]+")) + print(string.match("abc", "[^a-b]+")) + print(string.match("a-c", "%a[-]%a")) + """, + + """ + -- frontier %f + print(string.gsub("THE (quick) fox", "%f[%a]%a+", "X")) + print(string.gsub("THE (quick) fox", "%f[%w]%w+", "X")) + """, + + """ + -- 位置捕获 + print(string.find("hello", "()ll()")) + print(("hello world"):find("()o()", 6)) + print(("abc"):gsub("()", "|")) + """, + + """ + -- 贪婪与懒惰 + print(string.match("", "<(.-)>")) + print(string.match("", "<(.*)>")) + print(string.match("aaa", "a-")) + print(string.match("aaa", "a*")) + print(string.match("aaa", "a+")) + print(("aaa"):gsub("a-", "[%0]")) + """, + }; + + private static final String[] FORMAT = { + + """ + print(string.format("[%s] [%d] [%5.2f] [%x] [%X] [%o] [%c] [%e] [%g] [%%]", + "hi", 42, 3.14159, 255, 255, 8, 65, 12345.678, 12345.678)) + print(string.format("%5d|%-5d|%05d|%+d|% d", 42, 42, 42, 42, 42)) + print(string.format("%.3s|%10s|%-10s|", "abcdef", "x", "y")) + """, + + """ + print(("|%.3f|%f|%5.2f|%-8.2f|%+f|% f|"):format(1/0, 1/0, 3.14159, 3.14159, 1.5, 1.5)) + print(("|%.3f|%.3e|%.3g|%g|%G|"):format(-1/0, -1/0, -1/0, 0/0, 0/0)) + print(("|%E|%E|%A|%A|"):format(1/0, 0/0, 1/0, 0/0)) + print(("|%.0f|%.0e|%#e|"):format(1.0, 1.0, 1.0)) + """, + + """ + -- 「舍弃的第一位十六进制数字恰为 8」的平局用例不在这里:glibc 是 + -- 就近舍入 + 平局取偶,Apple libc 在这一段根本不是就近舍入,两边 + -- 答案不同;它们由 hexFloatPrecisionRoundingFollowsGlibc 钉住。 + print(("|%a|%A|%.2a|%010a|%-20a|%+#a|"):format(1.5, 1.5, 1.5, 1.5, 1.5, 1.5)) + print(("|%a|%a|%a|%a|"):format(0.0, -0.0, 1.0, 255.5)) + print(("|%#a|%#a|%.1a|%.2a|"):format(1.0, 1.5, 1.5, 1.5)) + print(("|%.0a|%.0a|%.0a|%.1a|"):format(1.75, 2.5, 4.5, 1.99)) + print(("|%.1a|%.2a|%.2a|"):format(-1.5, -1.53125, 1.51953125)) + """, + + """ + print(("|%g|%g|%g|%g|%g|"):format(0.0001, 0.00001, 100000, 1000000, 123456789)) + print(("|%#.3g|%.3g|%5.1g|%-8g|"):format(1.0, 1.0, 0.0, 1.5)) + print(("|%#g|%#g|%#.0f|"):format(1.0, 1.5, 1.0)) + print(("|%.0f|"):format(0.5), ("|%.0f|"):format(1.5), ("|%.0f|"):format(2.5)) + """, + + """ + print(("|%d|%5d|%-5d|%05d|%+d|% d|%.0d|%.5d|"):format(0, 42, 42, 42, 42, 42, 0, 42)) + print(("|%x|%X|%#x|%#X|%#o|%o|%5.3x|%-6x|"):format(255, 255, 255, 255, 8, 8, 255, 255)) + print(("|%u|%u|%u|"):format(-1, 0, 255)) + print(("|%#x|%#X|%#.0o|%#o|"):format(0, 0, 0, 0)) + print(("|%.3d|%.3u|"):format(-5, 7)) + print(("|%x|"):format(-1)) + print(("|%d|"):format(math.mininteger), ("|%d|"):format(math.maxinteger)) + """, + + """ + print(("|%c|%c|%3c|%-3c|"):format(65, 200, 65, 65)) + print(("|%5c|"):format(9)) + print(#("%c"):format(200)) + print(("|%.3s|%10s|%-10s|%.0s|"):format("abcdef", "x", "y", "z")) + print(("[%s]"):format(12), ("[%s]"):format(true), ("[%s]"):format(nil)) + print(("[%.5s]"):format(string.rep("x", 150))) + print(#("[%s]"):format(string.rep("x", 150))) + """, + + """ + print(("|%q|%q|%q|%q|%q|"):format(0, -0.0, 1/0, 0/0, "a\\0b\\\\c\\nd\\1e")) + print(("|%q|%q|%q|"):format(2^53, -2^63, 1.5)) + print(("|%q|"):format(0.1)) + print(("|%q|%q|%q|"):format(nil, true, false)) + print(("|%q|"):format(math.mininteger)) + """, + + """ + print(("|%010.2f|%-010.2f|"):format(3.14159, 3.14159)) + print(("|%+.2e|% .2e|"):format(3.14159, 3.14159)) + print(("%s %s %s"):format(1, 2, 3)) + print(("%s"):format(setmetatable({}, {__tostring = function() return "TS" end}))) + print(pcall(string.format, "%s", setmetatable({}, {__tostring = function() return 42 end}))) + print(("%d"):format("7"), ("%d"):format(7.0)) + print(("%.99f|"):format(-1.7976931348623157e308):len()) + """, + }; + + private static final String[] ERRORS = { + + """ + print(pcall(string.len, nil)) + print(pcall(string.len, true)) + print(pcall(string.sub, "abc")) + print(pcall(string.sub, "abc", "x")) + print(pcall(string.rep, "a")) + print(pcall(string.rep, "a", "x")) + print(pcall(string.byte)) + print(pcall(string.rep, "a", 2^62)) + print(pcall(string.char, 256)) + print(pcall(string.char, -1)) + """, + + """ + print(pcall(string.format)) + print(pcall(string.format, "%d")) + print(pcall(string.format, "%s", {})) + print(pcall(string.format, "%z", 1)) + print(pcall(string.format, "%1000000d", 1)) + print(pcall(string.format, "%.200d", 1)) + print(pcall(string.format, "%")) + print(pcall(string.format, "%5")) + print(pcall(string.format, "%q", print)) + print(pcall(string.format, "%5.2s", "abc\\0def")) + print(pcall(string.format, "%d", 2^63)) + """, + + """ + print(pcall(string.gsub, "abc", "b")) + print(pcall(string.gsub, "abc", "b", true)) + print(pcall(string.gsub, "abc", "b", {})) + print(pcall(string.find, "abc", "%")) + print(pcall(string.find, "abc", "%b")) + print(pcall(string.find, "abc", "[abc")) + print(pcall(string.match, "abc", "%f")) + print(pcall(string.gsub, "abc", "b", "x%")) + print(pcall(string.gsub, "abc", "(b)", "%2")) + print(pcall(string.match, "abc", "(b")) + """, + + """ + print(pcall(string.len)) + local sl = string.len + print(pcall(sl)) + local t = {len = string.len} + print(pcall(t.len)) + print(pcall(function() return string.len(nil) end)) + print(pcall(function() return string.format("%d", "x") end)) + print(pcall(function() local f = string.len return f(5) end)) + print(pcall(function() return ("x"):rep(nil) end)) + print(pcall(function() return ("x"):sub(nil) end)) + """, + + """ + -- 递归深度上限:MAXCCALLS 挡住,不至于把 C 栈(这里是 JVM 栈)打爆 + print(pcall(string.find, string.rep("a", 12), string.rep("a*", 10) .. "b")) + """, + + """ + -- 参数过多 / 过少时 format 的行为 + print(string.format("%s %s", "a")) + """, + }; + + /** + * 次正规浮点的 {@code %q} —— 刻意不进差分表。 + *

+ * {@code lstrlib.c: quotefloat} 用的是 C 的 {@code %a}: + *

+     * int nb = lua_number2strx(L, buff, MAX_ITEM, "%a", n);
+     * 
+ * C99 对 {@code %a} 只规定「小数点左边恰好一位十六进制数字」,并且 + * 「这个数字只在数次正规时为 0,其它情况取值由实现定义」—— + * 次正规怎么写属于 unspecified。实测两个 libc 正好相反: + *
    + *
  • glibc(CI 的 ubuntu):忠实于机器编码, + * {@code 5e-324} → {@code 0x0.0000000000001p-1022};
  • + *
  • Apple libc / musl:先规格化, + * {@code 5e-324} → {@code 0x1p-1074}。
  • + *
+ * 这里选 glibc:CI 跑在 ubuntu 上,那里的一致性闸门才是主闸门。 + * 拿 macOS 上的参考实现来比会红,但那是 libc 的分歧不是本实现的分歧—— + * 所以这条用普通断言钉住,不进差分表,免得有人照着 macOS 的参考实现 + * 「修」成规格化写法,把 Linux 上本来是对的行为改错。 + *

+ * {@code Double.toHexString} 对次正规给出的正是 glibc 那种写法, + * 所以实现上不需要额外做什么。 + */ + @Test + void subnormalQuoteFollowsGlibc() { + LuaState L = new LuaState(); + LuaLibs.openAll(L); + // 次正规:glibc 保留机器编码,不规格化 + assertEquals("0x0.0000000000001p-1022", quote(L, 5e-324)); + assertEquals("0x0.0000000000003p-1022", quote(L, 3 * 5e-324)); + assertEquals("0x0.8p-1022", quote(L, Math.pow(2, -1023))); + assertEquals("-0x0.0000000000001p-1022", quote(L, -5e-324)); + // 正规:两个 libc 一致,都是规格化写法 + assertEquals("0x1p-1022", quote(L, Math.pow(2, -1022))); + assertEquals("0x1p+0", quote(L, 1.0)); + assertEquals("0x1p+1", quote(L, 2.0)); + assertEquals("-0x0p+0", quote(L, -0.0)); + L.close(); + } + + /** + * 带精度的 {@code %a}:就近舍入、平局取偶,且这一条只能靠读 glibc + * 源码钉,不能靠本机(Apple libc)的参考实现比。 + *

+ * 判据在 {@code stdio-common/printf_fphex.c}: + *

+     * bool more_bits = ((next_digit_value & 7) != 0
+     *                   || precision + 1 < numend - numstr);
+     * do_round_away = round_away (negative, last_digit_value & 1,
+     *                             next_digit_value >= 8, more_bits, mode);
+     * 
+ * 而 {@code round_away} 在 {@code FE_TONEAREST} 下是 + * {@code half_bit && (last_digit_odd || more_bits)}。拆开看: + *
    + *
  • 舍弃的第一位 {@code > 8}:{@code more_bits} 为真,进;
  • + *
  • {@code < 8}:{@code half_bit} 为假,舍;
  • + *
  • 恰为 {@code 8} 且后面还有非零位:超过一半,进;
  • + *
  • 恰为 {@code 8} 且后面全零(平局):看保留的最后一位的奇偶, + * 奇则进。{@code prec == 0} 时「保留的最后一位」是小数点左边那一位。
  • + *
+ *

+ * Apple libc 在这一段根本不是就近舍入,错得可以量化: + * {@code ("%.4a"):format(0x1.500082p0)} 给 {@code 0x1.5000p+0}——舍弃的 + * {@code 0x82} 明明大于半 {@code 0x80} 却舍了下去,而 + * {@code 0x1.500081p0} 反倒进上来(它的判据成了「第二位舍弃数字的奇偶」)。 + * 所以这些用例不进差分表,用普通断言钉住 glibc 的答案。 + */ + @Test + void hexFloatPrecisionRoundingFollowsGlibc() { + LuaState L = new LuaState(); + LuaLibs.openAll(L); + // 平局:看保留的最后一位的奇偶 + assertEquals("0x2p+0", fmt(L, "%.0a", 1.5)); // 保留的最后一位是整数位 1(奇)→ 进 + assertEquals("0x2p-1", fmt(L, "%.0a", 0.75)); // 0x1.8p-1,同上 + assertEquals("0x1.0p+0", fmt(L, "%.1a", 0x1.08p0)); // 0(偶)→ 舍 + assertEquals("0x1.2p+0", fmt(L, "%.1a", 0x1.18p0)); // 1(奇)→ 进 + assertEquals("0x1.2p+0", fmt(L, "%.1a", 0x1.28p0)); // 2(偶)→ 舍 + assertEquals("0x1.fep+0", fmt(L, "%.2a", 0x1.fe8p0)); // e(偶)→ 舍 + assertEquals("0x2.00p+0", fmt(L, "%.2a", 0x1.ff8p0)); // f(奇)→ 进,且连进位到整数位 + assertEquals("0X2P+0", fmt(L, "%.0A", 1.5)); + // 不到一半 / 超过一半:两边一致,但顺手钉住 + assertEquals("0x1p+1", fmt(L, "%.0a", 2.5)); // 0x1.4:4 < 8 + assertEquals("0x2p+1", fmt(L, "%.0a", 3.5)); // 0x1.c:c > 8 + // 超过一半但第一位恰为 8 —— Apple libc 在这里是错的,glibc 一律进 + assertEquals("0x1.5001p+0", fmt(L, "%.4a", 0x1.500081p0)); + assertEquals("0x1.5001p+0", fmt(L, "%.4a", 0x1.500082p0)); + assertEquals("0x1.7e43dp+996", fmt(L, "%.5a", 1e300)); + // 次正规:glibc 保留机器编码,小数点左边是 0,不规格化 + assertEquals("0x0.0000000000001p-1022", fmt(L, "%a", 5e-324)); + assertEquals("0x0p-1022", fmt(L, "%.0a", 5e-324)); + assertEquals("0x0p-1022", fmt(L, "%.0a", Math.pow(2, -1023))); + // inf / nan 不参与零填充:0 标志被忽略,宽度改用空格补 + assertEquals(" inf", fmtExpr(L, "%020.5a", "math.huge")); + assertEquals(" -inf", fmtExpr(L, "%020.5a", "-math.huge")); + assertEquals(" nan", fmtExpr(L, "%020.5a", "0/0")); + L.close(); + } + + /** 跑 {@code return string.format(spec, x)};{@code x} 用十六进制字面量传,保证精确 */ + private static String fmt(LuaState L, String spec, double x) { + return fmtExpr(L, spec, Double.toHexString(x)); + } + + private static String fmtExpr(LuaState L, String spec, String expr) { + String src = "return string.format('" + spec + "', " + expr + ")"; + int base = L.top; + L.pushClosure(L.load(src.getBytes(StandardCharsets.ISO_8859_1), "=f")); + LuaValue err = L.pcall(base, 1); + assertNull(err, "format script failed: " + src); + String r = L.stack[base].asString().toJavaString(); + L.top = base; + return r; + } + + /** 跑 {@code return string.format('%q', x)};{@code x} 用十六进制字面量传,保证精确 */ + private static String quote(LuaState L, double x) { + // Lua 5.4 认十六进制浮点字面量,用它传值不会在十进制往返里掉精度 + String src = "return string.format('%q', " + Double.toHexString(x) + ")"; + int base = L.top; + L.pushClosure(L.load(src.getBytes(StandardCharsets.ISO_8859_1), "=q")); + LuaValue err = L.pcall(base, 1); + assertNull(err, "quote script failed: " + src); + String r = L.stack[base].asString().toJavaString(); + L.top = base; + return r; + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/StringPoolTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/StringPoolTest.java new file mode 100644 index 0000000..67e3d21 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/StringPoolTest.java @@ -0,0 +1,68 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * 字符串驻留池。 + *

+ * 这里只能测「能观测到的」部分:内容相同的短字符串必须拿到同一个实例, + * 长字符串不驻留。弱引用那一条(没人引用后条目会被回收)没法写成稳定的 + * 断言——JVM 何时回收不受控——所以只在注释里说明设计意图,不写会偶发红的用例。 + */ +public class StringPoolTest { + + @Test + public void shortStringsAreInterned() { + StringPool pool = new StringPool(); + LuaString a = pool.intern(LuaString.latin1("hello")); + LuaString b = pool.intern(LuaString.of(new byte[] { 'h', 'e', 'l', 'l', 'o' })); + assertSame(a, b, "内容相同的短字符串应当是同一个实例"); + } + + @Test + public void differentStringsStayDistinct() { + StringPool pool = new StringPool(); + LuaString a = pool.intern(LuaString.latin1("hello")); + LuaString b = pool.intern(LuaString.latin1("world")); + assertNotSame(a, b); + assertFalse(a.equals(b)); + } + + /** 长字符串不驻留:每次都返回新实例 */ + @Test + public void longStringsAreNotInterned() { + StringPool pool = new StringPool(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < Lua.MAXSHORTLEN + 10; i++) { + sb.append('y'); + } + LuaString a = pool.intern(LuaString.latin1(sb.toString())); + LuaString b = pool.intern(LuaString.latin1(sb.toString())); + assertNotSame(a, b, "长字符串不该驻留"); + assertTrue(a.equals(b), "内容仍然相等"); + } + + /** 池的大小随不同字符串增长,重复驻留不增加条目 */ + @Test + public void poolGrowsWithDistinctStrings() { + StringPool pool = new StringPool(); + for (int i = 0; i < 50; i++) { + pool.intern(LuaString.latin1("s" + i)); + } + int afterInsert = pool.size(); + assertTrue(afterInsert >= 50, "size=" + afterInsert); + for (int i = 0; i < 50; i++) { + pool.intern(LuaString.latin1("s" + i)); + } + assertEquals(afterInsert, pool.size(), "重复驻留不应新增条目"); + } + + private static void assertEquals(int expected, int actual, String msg) { + org.junit.jupiter.api.Assertions.assertEquals(expected, actual, msg); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/SyntaxErrorTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/SyntaxErrorTest.java new file mode 100644 index 0000000..6694ddb --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/SyntaxErrorTest.java @@ -0,0 +1,698 @@ +package com.github.esrrhs.fakescript.lua; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +/** + * 语法 / 编译错误消息的差分矩阵 —— 阶段 12。 + *

+ * 为什么单开一类:JaCoCo 一度显示 {@code Parser} 有 28 行、{@code Lexer} 有 14 行 + * 一行没走到,而它们清一色是 {@code error()} / {@code syntaxError()} 点—— + * 也就是说,此前从来没有一句坏代码被喂进来过。上游用例里也没有专门压解析错误的 + * ({@code errors.lua} 在 5.4 已改测运行时错误)。 + *

+ * 错误消息是最容易跟参考实现分家的地方,三处尤其: + *

    + *
  • token 的显示名:{@code luaX_token2str} 对 + * {@code token >= TK_EOS} 返回裸串(第 72 条)。 + * 实测:这一处当前观测不到(破坏它 0 红)。
  • + *
  • "<eof>" 的写法:块没闭合时记的是 块结束 那一行, + * 不是坏 token 那一行;"unexpected symbol near" 才是坏 token 那一行。
  • + *
  • 行号:多行片段里取的是哪一行,逐条实测比对。
  • + *
+ *

+ * 矩阵两路: + *

    + *
  • 手写片段(A–J):空 / 只有注释 / 语句级缺 token / 表达式级 / + * 畸形数字 / 未闭合字符串 / 长括号等级 / goto 与标签 / + * {@code } 与 {@code } / 表构造与调用。
  • + *
  • 系统生成(K–L):把一批合法程序逐个砍短、逐字符删除、 + * 逐字符替换。不用人去想「哪里会错」,截到每一个位置自然就覆盖了 + * 每一种"话没说完"的形态——这才是铺矩阵该有的样子。
  • + *
+ *

+ * 另外单开一条 源码前导:UTF-8 BOM 与首行 {@code #!}。这条路径只有 + * 从文件加载 才走({@code lauxlib.c} 的 {@code skipBOM} + + * {@code skipcomment}),{@code load(字符串)} 不走——所以之前所有用例都碰不到, + * 而它恰恰是 {@code loadfile} / {@code dofile} / {@code require} 的真实入口。 + * 两类情形要分开比:文件里的 BOM 被吃掉;字符串里的 BOM 是非法字符。 + * + * @see Lexer + * @see Parser + */ +class SyntaxErrorTest { + + @BeforeAll + static void needReference() { + assumeTrue(ReferenceLua.find() != null, + "no lua5.4 reference binary; set LUA54_BIN"); + } + + /** 语法 / 编译错误消息矩阵(2586 行) */ + @Test + void syntaxErrorGridMatchesReference() { + Diff.assertMatches(""" +-- 元方法矩阵之外的新一片:语法 / 编译错误消息矩阵 +-- +-- 为什么开这一片:JaCoCo 显示 Parser 有 28 行、Lexer 有 14 行完全没走到, +-- 一眼看去全是 error() / syntaxError() 点——也就是说从来没有脚本喂过一句 +-- 坏代码。而错误消息是最容易跟参考实现分家的地方:token 的显示名 +-- (第 72 条)、"" 的写法、行号取的是哪一行。 +-- +-- 两条硬约束: +-- 1. load 遇到坏 chunk 是返回 nil + 消息,不是抛错。但脚本自身的 +-- 语法错误是编译期的,整份脚本都加载不了——所以每条坏片段都 +-- 得用 load 现造,不能直接写在脚本里。 +-- 2. 探针里不能出现反斜杠(要嵌进 Java 文本块,反斜杠是转义符), +-- 需要反斜杠的地方一律用 string.char(92) 拼。 + +local BS = string.char(92) -- 一个反斜杠 +local NL = string.char(10) -- 换行 +local Q = string.char(39) -- 单引号 +local DQ = string.char(34) -- 双引号 + +local function show(v) + local t = type(v) + if t == "string" then return "s[" .. v .. "]" end + if t == "number" then + if math.type(v) == "integer" then return "i" .. tostring(v) end + return "f" .. tostring(v) + end + return t +end + +--[[ + 每个片段都包在 pcall 里:万一某条把实现自身的异常激出来(不是 LuaError), + 脚本会当场死掉,后面几百条一条也不比而用例照样绿(第 38 条)。 +]] +local function try(s, name) + local ok, f, e = pcall(load, s, name) + if not ok then + return "THROW " .. show(f) + end + if f ~= nil then + return "OK func" + end + return "ERR " .. show(e) +end + +print("BEGIN") + +--[[ + A. 空 / 只有注释 / 只有分隔符 +]] +local A = { + "", " ", NL .. NL, "--c", "--[[c]]", "--[==[c]==]", ";", ";;", " ", + "--[[" .. NL .. "]]", "---", "--", +} +for i = 1, #A do print("A", i, try(A[i], "=X")) end + +--[[ + B. 语句级缺 token。 + 行号是关键:报错位置常在前一条语句的结尾而不是坏的地方 + ——"unexpected symbol near" 记的是坏 token 那一行, + 而 "'end' expected" 记的是块结束那一行。 +]] +local B = { + "=", "==", "a =", "a = =", "a = 1 +", "a = 1 + ", "a = * 1", + "local", "local = 1", "local a =", "local a,", "local a,,b", + "local a = 1,", "local 1 = 1", "local end = 1", "local a.b = 1", + "if", "if x", "if x then", "if x then y=1", "if x then else", + "if x then elseif", "if x then elseif y then", "if x then else y=1", + "while", "while x", "while x do", "while x do y=1", + "repeat y=1", "repeat until", "repeat until x", "repeat", + "for", "for i", "for i =", "for i = 1", "for i = 1,", "for i = 1,2", + "for i = 1,2 do", "for i = 1,2 do y=1", "for i = 1,2,3 do", + "for i = 1,2,3 do y=1", "for k in", "for k in t", "for k in t do", + "do", "do y=1", "end", "else", "elseif", "until", "then", + "function", "function f", "function f(", "function f()", + "function f() y=1", "function f.a", "function f.a.", "function f:a.", + "function f:a", "local function", "local function f", "local function f()", + "return", "break", +} +for i = 1, #B do print("B", i, try(B[i], "=X")) end + +--[[ + C. 表达式级。 +]] +local C = { + "a = (1", "a = 1)", "a = f(,)", "a = f(a,)", "a = f()", + "a = {}", "a = {,}", "a = {a=}", "a = {[1]}", "a = {[1]=}", + "a = {1 2}", "a = {1,2,}", "a = 1 +", "a = -", "a = #", + "a = not", "a = 1 ..", "a = ..1", "a = 1 and", "a = 1 or", + "a[]", "a[1", "a = a[]", "a = a.b.", "a = a:b.", + "a = 1 = 2", "a = 1 == ", "a = ((1)", + "a = 1 2", "a = f(a b)", "a = f(a)(b)", + "a = 2^-3", "a = 2^-", "a = #a", "a = -#a", +} +for i = 1, #C do print("C", i, try(C[i], "=X")) end + +--[[ + D. 数字字面量。 + Lua 5.4 对畸形数字是词法错误(luai_numisnan 那类检查在 lexer 里), + 消息形如 "malformed number near '1e'"。 +]] +local D = { + "a = 1e", "a = 1e+", "a = 1e-", "a = 0x", "a = 0x1p", "a = 0x1p+", + "a = 0x.1", "a = .", "a = ..", "a = 1..2", "a = 1e999", + "a = 0x7fffffffffffffff", "a = 0x8000000000000000", + "a = 0xffffffffffffffff", "a = 1e15", "a = 1e16", "a = -1e15", + "a = 9007199254740993", "a = 0x1p-1074", "a = 0x1p-1075", +} +for i = 1, #D do print("D", i, try(D[i], "=X")) end + +--[[ + E. 字符串字面量:未闭合、长括号等级不匹配。 + 转义序列相关的用 BS 拼出来(探针里不能有裸反斜杠)。 +]] +local E = { + "a = 'abc", 'a = "abc', "a = [[abc", "a = [=[abc]=", "a = [==[abc]=", + "a = [==[abc]==]", "a = [==[abc]===]", "a = --[[x", "a = [[]]", + "a = ''", 'a = ""', + -- 转义序列:十六进制转义不带数字、unicode 转义越界、十进制转义超过 255 + "a = '" .. BS .. "x'", + "a = '" .. BS .. "u{110000}'", + "a = '" .. BS .. "300'", + "a = '" .. BS .. "z " .. NL .. " x'", +} +for i = 1, #E do print("E", i, try(E[i], "=X")) end + +--[[ + F. goto / 标签:缺标签、重复标签、跳进局部变量作用域。 + 这些是语义错误(lparser.c 的 undefgoto / jumpScopeError), + 跟纯语法错误走不同的回路。 +]] +local F = { + "goto x", + "do ::x:: goto x end", + "::x:: ::x::", + "do goto x end ::x::", + "goto x do ::x:: end", + "for i=1,2 do goto cont ::cont:: end", + "while true do goto break end ::break::", + "::a:: goto b ::b:: goto a", +} +for i = 1, #F do print("F", i, try(F[i], "=X")) end + +--[[ + G. to-be-closed / const 的声明期检查。 + 同样得用 load 现造:重复给 赋值是编译期错误。 +]] +local G = { + "local x = 5", + "local x = nil", + "local x , y = nil, nil", + "local x = 1 x = 2", + "local x = 1", + "local a = setmetatable({}, {__close=print})", + "do local x = nil end", +} +for i = 1, #G do print("G", i, try(G[i], "=X")) end + +--[[ + H. 层级上限:表达式嵌太深、常量太多、upvalue 太多、局部量太多。 + lparser.c 的 errorlimit 给的是 "too many %s (limit is %d) in %s"。 +]] +local H = {} +do + H[#H + 1] = "a = " .. string.rep("(", 210) .. "1" .. string.rep(")", 210) + H[#H + 1] = "a = " .. string.rep("-", 210) .. "1" + H[#H + 1] = "local function f() return " .. string.rep("(", 210) .. "1" + .. string.rep(")", 210) .. " end" + -- 常量个数 + local t = {} + for i = 1, 300 do t[#t + 1] = "a = " .. i .. ".0" end + H[#H + 1] = table.concat(t, NL) + -- 局部变量个数(一层里 210 个) + local names = {} + for i = 1, 210 do names[#names + 1] = "v" .. i end + H[#H + 1] = "local " .. table.concat(names, ",") +end +for i = 1, #H do print("H", i, try(H[i], "=X")) end + +--[[ + I. 方法调用 / 自调用 / 可变参数的位置错误。 +]] +local I = { + "a:b()", "a:b", "('x'):upper()", "a = ('x'):upper()", + "local function f(...) return ... end", + "function f(...) return ... end f(1,2,3)", + "...", "return ...", + "a = 1 return", +} +for i = 1, #I do print("I", i, try(I[i], "=X")) end + +--[[ + J. 数字与名字的粘连、运算符连写。 +]] +local J = { + "a = 1a", "a = 0x1g", "a = 1..", "a = ..", "a = 1 ... 2", + "a = 1 +* 2", "a = 1 ** 2", "a = ~1", "a = 1 ~ 2", + "a = #", "a = ##a", "a = 1 //0", +} +for i = 1, #J do print("J", i, try(J[i], "=X")) end + +--[[ + K. 前缀截断:把一批合法程序逐个砍短,每个长度都喂一次 load。 + 这是系统性造语法错误的办法——不用人去想「哪里会错」, + 截断到每一个位置自然覆盖了每一种"话没说完"的形态。 +]] +local PROGS = { + "local x = 1", + "local function f(a, b) return a + b end", + "if x then y = 1 else y = 2 end", + "for i = 1, 10 do print(i) end", + "for k, v in pairs(t) do print(k, v) end", + "while true do break end", + "repeat x = x + 1 until x > 10", + "t = {1, 2, 3, a = 'b', [4] = 5}", + "return function(...) return ... end", + "local a = 1", + "do local b = nil end", + "goto done ::done::", + "a.b.c:d(1)(2){3}", + "x = -2^2 // 3 % 4", + "x = #t .. 'y'", + "local s = [[long]]", + "x = 0x1p4 + 1e2 + .5", + "function t.a.b:c(d) end", + "local t = {[1]=1, 2, 3}", + "return 1, 2, 3", +} +for pi = 1, #PROGS do + local p = PROGS[pi] + for k = 0, #p do print("K", pi, k, try(string.sub(p, 1, k), "=X")) end +end + +--[[ + L. 逐字符删除与逐字符替换。 + 替换集取三个括号类字符——它们最容易把"结构"改坏, + 于是消息里出现的 token 名也最杂。 +]] +local REP = { '"', "(", "}" } +for pi = 1, #PROGS do + local p = PROGS[pi] + for k = 1, #p do + print("LD", pi, k, try(string.sub(p, 1, k - 1) .. string.sub(p, k + 1), "=X")) + end + for k = 1, #p do + for r = 1, #REP do + print("LR", pi, k, r, try(string.sub(p, 1, k - 1) .. REP[r] .. string.sub(p, k + 1), "=X")) + end + end +end + +print("END") + """); + } + + /** + * 顺着 {@code lparser.c} / {@code llex.c} 的每一个报错点点名打。 + *

+ * 与上面那条网格的分工:那条是「把合法程序砍短 / 删字符 / 换字符」—— + * 覆盖面靠数量;这条是照着 C 源码里的 {@code throw} 一行一行点, + * 保证每个 {@code syntaxerror} / {@code semerror} 的调用点都有人走过。 + * JaCoCo 显示 {@code Parser} 有 24 行没走到,清一色是这些点。 + *

+ * 点到的点:{@code "%s expected"}、{@code "%s expected (to close %s at + * line %d)"}、{@code errorlimit} 的三种 {@code what}(局部变量 200 / + * upvalue 255 / 构造式项数)、{@code attempt to assign to const variable}、 + * {@code unknown attribute}、{@code multiple to-be-closed variables}、 + * {@code cannot use '...' outside a vararg function}、{@code or + * '...' expected}、{@code function arguments expected}、 + * {@code '=' or 'in' expected}、{@code ... jumps into the scope of + * local}、{@code no visible label}、{@code label already defined}、 + * lexer 的 {@code unfinished string} / {@code malformed number} / + * {@code hexadecimal digit expected},以及 {@code C stack overflow}。 + *

+ * 两处写法上的坑: + *

    + *
  • chunk 名统一用 {@code "=c"}:{@code load(字符串)} 默认把 + * 源码塞进 {@code [string "..."]},几十条用例的消息就成了几十倍噪声;
  • + *
  • 必须包 {@code pcall}:C 栈溢出是抛出不是返回(约定 + * #27)。不包的话那一条带着回溯把脚本打断,而回溯里的文件名两边 + * 不一样(约定 #37)。
  • + *
+ * {@code control structure too long} 与「构造式项数」两个上限要到 + * {@code MAX_INT} 才触发,打不出来,如实记为不可达(约定 #30)。 + */ + @Test + void parserErrorSitesMatchReference() { + Diff.assertMatches(""" +-- 语法错误全矩阵:顺着 lparser.c / llex.c / lcode.c 的每一个报错点写 +-- 起因:JaCoCo 里 Parser 有 24 行没走到,几乎全是 throw syntaxError —— +-- 也就是「错误消息与原话」这一片从来没被系统扫过。SyntaxErrorTest 只有 2 条。 +-- +-- chunk 名统一用 "=c"(load 的第二个参数):默认的 "[string \\"...\\"]" 会把 +-- 源码塞进消息里,几十条用例的消息就成了几十倍的噪声。 +-- 必须包 pcall:C 栈溢出是抛出而不是返回(约定 #27)。不包的话那 +-- 一条会带着回溯把脚本打断,而回溯里的文件名两边不一样(约定 #37)。 +local function p(tag, s) + local ok, a, b = pcall(load, s, "=c") + if not ok then + print(tag, "THROW", tostring(a)) + elseif a then + print(tag, "OK") + else + print(tag, tostring(b)) + end +end + +-- ① "%s expected":同一个 token 在不同位置上的报错(error_expected) +print("== 缺 token ==") +local miss = { + "if x then", "if x then y=1", "if x then y=1 else", "if x then y=1 elseif", + "while x do", "while x do y=1", + "for i=1,2 do", "for i=1,2", "for i", "for i in", "for i in x do", "for", + "repeat", "repeat x=1", + "do", "do x=1", + "function", "function f", "function f(", "function f(a", "function f(a,", + "function f(a,b", "function f()", "function f() x=1", + "local", "local a", "local a =", "local a,", "local function", + "local function f(", "return 1 2", + "x =", "x = 1 +", "x = (", "x = (1", "x = {", "x = {1", "x = {1,", "x = t[", + "x = t[1", "x = f(", "x = f(1", "x = f(1,", "x = a.", "x = a:", "x = a:b", + "x = -", "x = #", "x = not", "x = 1 and", "x = 1 or", "x = 1 ..", + "goto", "::", "::a", "::a::", "break", + ";", "x", "x = 1;", "", " ", "\\n", +} +for i, s in ipairs(miss) do p("M" .. i, s) end + +-- ② "%s expected (to close %s at line %d)":跨行的 check_match +print("== 跨行闭合 ==") +local spread = { + "if x then\\n\\nend", + "if x then\\n\\n\\nelse\\nend", + "while x do\\n\\n\\nend", + "for i=1,2 do\\n\\n\\nend", + "do\\n\\n\\nend", + "function f()\\n\\n\\nend", + "repeat\\n\\n\\nuntil x", + "x = function()\\n\\n\\nend", + "x = (\\n\\n\\n1)", + "x = {\\n\\n\\n1}", + "x = t[\\n\\n\\n1]", + "x = f(\\n\\n\\n1)", + "if x then\\ny=1\\nelseif z then\\n\\nelse\\nend", + "if x\\nthen\\nend", + "while x\\ndo\\nend", +} +for i, s in ipairs(spread) do p("S" .. i, s) end + +-- ③ errorlimit:"too many %s (limit is %d) in %s" +print("== 上限 ==") +do + -- 局部变量 200:主 chunk 与函数里的 where 不一样 + local a = {} + for i = 1, 201 do a[i] = "v" .. i end + p("L1", "local " .. table.concat(a, ",")) + p("L2", "local " .. table.concat(a, ",", 1, 200)) + p("L3", "function f()\\nlocal " .. table.concat(a, ",") .. "\\nend") + p("L4", "do\\nlocal " .. table.concat(a, ",") .. "\\nend") +end +do + -- upvalue 255 + local a = {} + for i = 1, 256 do a[i] = "u" .. i end + local outer = "local " .. table.concat(a, ",", 1, 256) .. "\\n" + local use = {} + for i = 1, 256 do use[i] = "u" .. i end + p("U1", outer .. "function f() return " .. table.concat(use, "+") .. " end") +end +do + -- 函数实参上限?以及 return 的个数 + local a = {} + for i = 1, 300 do a[i] = "1" end + p("A1", "f(" .. table.concat(a, ",") .. ")") + p("A2", "return " .. table.concat(a, ",")) +end + +-- ④ const / close 属性 +print("== 属性 ==") +p("C1", "local x = 1\\nx = 2") +p("C2", "local x = 1\\nx = 2\\nlocal y = x") +p("C3", "local x = 1") +p("C4", "local x = 1") +p("C5", "local x = nil") +p("C6", "local x , y = nil, nil") +p("C7", "local x = nil, y = nil") +p("C8", "function f() local x = 1 x = 2 end") +p("C9", "local x = 1 local x = 2") +p("C10", "for i = 1, 2 do end") +p("C11", "for i = 1, 2 do end") +p("C12", "local x ") +p("C13", "local x <>") +p("C14", "local x 本探针自己
先编不过 +local lex = { + "'abc", '"abc', 'x = "abc', 'x = [[abc', + 'x = [==[abc]', 'x = [==[abc]]', 'x = [[abc]]=', + 'x = [ [ 1 ] ]', + "0x", "0xg", "x = 0x", "x = 0x.", "x = 0x.1p", "x = 0x1p", + "1e", "x = 1e", "x = 1e+", "x = .", "x = 1..2", "x = 0xa", + 'x = "\\\\xZZ"', 'x = "\\\\x1"', 'x = "\\\\xff"', 'x = "\\\\z"', + 'x = "\\\\u{}"', 'x = "\\\\u{110000}"', 'x = "\\\\u{41}"', 'x = "\\\\u{1100000}"', + "$", "x = $", "x = 1 $ 2", "x = @", "x = `", "x = ?", + "--[==[", "--", "--[", "x = 1 --[[\\n", + "x = 'a\\\\", 'x = "a\\nb"', 'x = "a\\\\\\nb"', + "x = 0777", "x = 1_000", "x = 1e999", + "\\\\", "x = \\\\", "1abc", "x = 0b101", "x = 1e309", +} +for i, s in ipairs(lex) do p("X" .. i, s) end + +-- ⑧ 方法调用后跟的不是参数列表:"function arguments expected" +print("== 调用形状 ==") +p("F1", "x = a:b 5") +p("F2", "x = a:b\\n5") +p("F3", "x = a:b =") +p("F4", "x = a:b;") +p("F5", "x = a:b end") +p("F6", "a:b()") +p("F7", "a:b{}") +p("F8", [[a:b"c"]]) +p("F9", "a:b[[c]]") +p("F10", "a:b(1)(2)") + +-- ⑨ "'=' or 'in' expected" 与 " or '...' expected" +print("== for / 形参 ==") +p("N1", "for i do end") +p("N2", "for i, do end") +p("N3", "for 1=1,2 do end") +p("N4", "for i.j=1,2 do end") +p("N5", "function f(1) end") +p("N6", "function f(a, 1) end") +p("N7", "function f(a.b) end") +p("N8", "function f(..., a) end") +p("N9", "function f(a, ..., b) end") +p("N10", "for i=1,2,3,4 do end") + +-- ⑩ 语句层面接不上的东西 +print("== 语句 ==") +local stmt = { + "y=1 z", "y=1 (", "y=1 )", "y=1 }", "y=1 ]", "y=1 ,", + "y=1 .", "y=1 :", "y=1 =", "y=1 <", "y=1 >", "y=1 *", + "y = 1 then", "y = 1 do", "y = 1 end", "y = 1 else", + "y = 1 elseif", "y = 1 until", "y = 1 in", "y = 1 local", + "return", "return return", "return 1 return 2", + "::a:: ::a::", "break break", ";", ";;", "; ;", +} +for i, s in ipairs(stmt) do p("T" .. i, s) end + +-- ⑪ " at line N jumps into the scope of local 'y'"(jumpscopeerror) +print("== goto 跨局部变量作用域 ==") +p("J1", "do goto x local y = 1 ::x:: end") +p("J2", "do goto x local y ::x:: end") +p("J3", "while true do goto x local y = 1 ::x:: end") +p("J4", "for i=1,2 do goto x local y = 1 ::x:: end") +p("J5", "do local y = 1 goto x end ::x::") +p("J6", "function f() goto x local y = 1 ::x:: end") +p("J7", "do goto x local y = 1 ::x:: end") +p("J8", "do goto x local y = nil ::x:: end") +p("J9", "do do goto x local y = 1 ::x:: end end") +p("J10", "repeat goto x local y = 1 ::x:: until true") + +-- ⑫ upvalue 上限 255:一层套一层,内层的 upvalue 能超过 255 +print("== upvalue 上限 ==") +do + local a, b, use = {}, {}, {} + for i = 1, 200 do + a[i] = "a" .. i + b[i] = "b" .. i + use[#use + 1] = "a" .. i + use[#use + 1] = "b" .. i + end + local src = "local " .. table.concat(a, ",") .. "\\n" + .. "local function f()\\n" + .. "local " .. table.concat(b, ",") .. "\\n" + .. "local function g() return " .. table.concat(use, "+") .. " end\\n" + .. "return g\\nend" + p("P1", src) +end + +-- ⑬ C stack overflow:语法分析器的层数上限(约定 #26,参考恒 197) +print("== 层数 ==") +for _, n in ipairs({ 100, 190, 200, 210, 300 }) do + p("D" .. n, string.rep("do ", n) .. string.rep(" end", n)) + p("B" .. n, string.rep("(", n) .. "1" .. string.rep(")", n)) + p("T" .. n, string.rep("{", n) .. string.rep("}", n)) +end + +print("DONE") + """); + } + + /** 源码前导:BOM 与 "#!" 行(只有 loadfile 走) */ + @Test + void sourcePreambleMatchesReference() { + Diff.assertMatches(""" +-- 源码前导:BOM 与 '#!' 行的差分 +-- +-- JaCoCo 显示 Parser.skipFilePreamble 有三段从来没走到——UTF-8 BOM 与 +-- 首行 '#' 注释。这条路径只有 从文件加载才走(lauxlib.c 的 +-- skipBOM + skipcomment),load(字符串) 不走,所以之前所有用例都碰不到。 +-- +-- 而它恰恰是 loadfile / dofile / require 的真实入口。 + +local BOM = string.char(0xEF, 0xBB, 0xBF) +local NL = string.char(10) + +local function show(v) + local t = type(v) + if t == "string" then return "s[" .. v .. "]" end + if t == "number" then + if math.type(v) == "integer" then return "i" .. tostring(v) end + return "f" .. tostring(v) + end + return t +end + +local function go(tag, f, ...) + local r = table.pack(pcall(f, ...)) + local out = { tag, tostring(r[1]) } + for i = 2, r.n do out[#out + 1] = show(r[i]) end + print(table.concat(out, " ")) +end + +-- 临时文件写 target/rt/:两边同一个 cwd,写仓库根会互相踩(第 41 条)。 +-- 开头先删一次,免得上一次跑留下的东西混进来。 +local DIR = "target/rt/pre" +os.execute("rm -rf " .. DIR) +os.execute("mkdir -p " .. DIR) + +local function put(name, data) + local f = io.open(DIR .. "/" .. name, "wb") + f:write(data) + f:close() +end + +local function loadrun(name) + local f, e = loadfile(DIR .. "/" .. name) + if f == nil then return "ERR " .. show(e) end + local r = table.pack(pcall(f)) + local out = { "OK" } + for i = 1, r.n do out[#out + 1] = show(r[i]) end + return table.concat(out, " ") +end + +print("BEGIN") + +-- 1) 纯 BOM + 代码:参考实现应当吃掉 BOM 正常跑 +put("bom1.lc", BOM .. "return 1+1") +print("BOM1", loadrun("bom1.lc")) + +-- 2) 只有 BOM,没有代码 +put("bom2.lc", BOM) +print("BOM2", loadrun("bom2.lc")) + +-- 3) '#' 开头的一行 + 代码 +put("hash1.lc", "#!/usr/bin/lua" .. NL .. "return 2+2") +print("HASH1", loadrun("hash1.lc")) + +-- 4) '#' 开头但没换行(整份源码都是注释行) +put("hash2.lc", "#!/usr/bin/lua") +print("HASH2", loadrun("hash2.lc")) + +-- 5) BOM + '#' + 代码:两个都要吃掉 +put("both.lc", BOM .. "#!/usr/bin/lua" .. NL .. "return 3+3") +print("BOTH", loadrun("both.lc")) + +-- 6) '#' 在第一行中间(不是开头):不该被跳过 +put("mid.lc", " # x" .. NL .. "return 4+4") +print("MID", loadrun("mid.lc")) + +-- 7) BOM 出现两次:只吃第一个 +put("bom3.lc", BOM .. BOM .. "return 5+5") +print("BOM3", loadrun("bom3.lc")) + +-- 8) 只有前两个字节(不完整 BOM):不该吃 +put("bom4.lc", string.char(0xEF, 0xBB) .. "return 6+6") +print("BOM4", loadrun("bom4.lc")) + +-- 9) 长度刚好 2 的源码 +put("short.lc", "==") +print("SHORT", loadrun("short.lc")) + +-- 10) 空文件 +put("empty.lc", "") +print("EMPTY", loadrun("empty.lc")) + +-- 11) '#' 后面紧跟 EOF(无换行) +put("hash3.lc", "#") +print("HASH3", loadrun("hash3.lc")) + +-- 12) loadfile 一个目录 +print("DIR", loadrun(".")) + +-- 13) loadfile 不存在的文件 +print("MISSING", loadrun("target/rt/pre/does-not-exist-xyz.lc")) + +--[[ + 14) load(字符串) 里的 BOM 不该被吃掉——只有文件加载才跳前导。 + 这是 lauxlib.c 的 skipcomment 只在 loadfile 路径上。 +]] +go("STRBOM", function() return load(BOM .. "return 7") end) +go("STRHASH", function() return load("#!/x") end) +go("STRHASHNL", function() return load("#!/x" .. NL .. "return 8") end) + +-- 15) 只有 BOM 的字符串 +go("STRBOMONLY", function() return load(BOM) end) + +-- 16) BOM 在代码中间 +go("STRBOMMID", function() return load("return " .. BOM .. " 9") end) + +print("END") + """); + } + + private SyntaxErrorTest() { + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/TableLibTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/TableLibTest.java new file mode 100644 index 0000000..9e1f42e --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/TableLibTest.java @@ -0,0 +1,331 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; + +/** + * table 库的差分用例 —— 阶段 7。 + *

+ * {@code ltablib.c} 里最容易写错的是这几点: + *

    + *
  • {@code __index/__newindex/__len} 会把非表「变成」表: + * {@code TAB_R/W/L} 三个宏都要求元方法存在才放行,所以 + * {@code table.insert({}, 1, x)} 在对方只有 {@code __index} 时报的是 + * 「bad argument #1 to 'insert' (table expected, got userdata)」而不是 + * 「没有 __newindex」。
  • + *
  • {@code table.sort} 是不稳定的:参考实现是原地快排,测试只能 + * 断言「有序」不能断言「相等元素的相对次序」。另外排序比较函数返回 + * 不一致时会有一句专门的 {@code invalid order function for sorting}。
  • + *
  • {@code remove} 的返回值:空表返回 nil 而不是报错; + * {@code remove(t, n)} 的 {@code n} 允许 {@code #t+1}。
  • + *
  • {@code concat} 会真的把数字转成字符串,且只对 + * {@code [1, #t]} 区间生效。
  • + *
+ */ +class TableLibTest { + + @TestFactory + List tableScriptsMatchReference() { + List tests = new ArrayList<>(); + add(tests, "basic", BASIC); + add(tests, "sort", SORT); + add(tests, "meta", META); + add(tests, "err", ERRORS); + add(tests, "overflow", OVERFLOW); + return tests; + } + + private static void add(List tests, String tag, String[] srcs) { + for (int i = 0; i < srcs.length; i++) { + String src = srcs[i]; + int n = i; + tests.add(DynamicTest.dynamicTest(tag + "[" + n + "]", () -> Diff.assertMatches(src))); + } + } + + /** + * {@code choosePivot} 的不变量 {@code lo + r4 <= p <= up - r4} —— C 版里 + * 这是一条 {@code lua_assert},Java 版得自己守住。 + *

+ * 为什么值得单独钉一条:{@code IdxT} 是 {@code unsigned int}, + * {@code rnd} 也是 {@code unsigned int},所以 C 的取模恒为非负。Java 这边 + * {@code int} 有符号,而随机种子本来就是 C 那 32 个比特——一半概率是负数, + * 有符号取模跟着变负,{@code p} 就掉出 {@code [lo, up]},极端情形 + * {@code p <= 0},{@code a[p]} 是 nil,报出来的却是 + * 「attempt to compare nil with number」。 + *

+ * 这条缺陷在 {@code sort.lua} 上的表现是偶发:它用未播种的 + * {@code math.random()} 造 5 万个元素,只有分区严重失衡时才会去换随机种子。 + * 反射直打私有方法,把一个概率性失败变成确定性断言。 + */ + @Test + void choosePivotStaysInsideTheInterval() throws Exception { + Method m = TableLib.class.getDeclaredMethod("choosePivot", + int.class, int.class, int.class); + m.setAccessible(true); + int[] seeds = { + 0, 1, 7, 12345, Integer.MAX_VALUE, + -1, -12500, -24998, Integer.MIN_VALUE, + (int) 0xFFFFFFFFL, (int) 0xDEADBEEFL, (int) 0x80000000L, + }; + int[][] ranges = { + {1, 50000}, {1, 5000}, {1, 1000}, {2, 101}, {100, 500}, {3, 40000}, + }; + for (int[] r : ranges) { + int lo = r[0]; + int up = r[1]; + int r4 = (up - lo) / 4; + for (int rnd : seeds) { + int p = (Integer) m.invoke(null, lo, up, rnd); + assertTrue(p >= lo + r4 && p <= up - r4, + "choosePivot(" + lo + ", " + up + ", " + rnd + ") = " + p + + ",越出了 [" + (lo + r4) + ", " + (up - r4) + "]"); + } + } + } + + private static final String[] BASIC = { + + """ + local t = {10, 20, 30} + print(#t, table.concat(t, ","), table.concat(t, ",", 2), table.concat(t, ",", 2, 3)) + print(table.concat(t), table.concat({1, 2.5, "x"}, "-")) + print(table.concat({}, ","), table.concat({1, nil, 3}, ",")) + """, + + """ + local t = {1, 2, 3} + table.insert(t, 4) + print(table.concat(t, ",")) + table.insert(t, 2, 99) + print(table.concat(t, ",")) + print(table.remove(t), table.concat(t, ",")) + print(table.remove(t, 1), table.concat(t, ",")) + local e = {} + print(table.remove(e), table.remove(e, 1), table.remove(e, 0)) + """, + + """ + local a = {1, 2, 3, 4, 5} + print(table.unpack(a)) + print(table.unpack(a, 2)) + print(table.unpack(a, 2, 4)) + print(table.unpack(a, 5, 4)) + print(table.unpack(a, 1, 5)) + print(select("#", table.unpack(a))) + """, + + """ + local p = table.pack(1, nil, 3) + print(p.n, p[1], p[2], p[3]) + print(table.pack().n) + print(select("#", table.unpack({}, 1, 0))) + """, + + """ + local a = {1, 2, 3, 4, 5} + local b = {} + table.move(a, 1, 5, 1, b) + print(table.concat(b, ",")) + table.move(a, 2, 4, 1, a) + print(table.concat(a, ",")) + table.move(a, 1, 3, 3) + print(table.concat(a, ",")) + local c = {} + table.move(c, 1, 0, 1, c) + print(#c) + """, + + """ + local t = {3, 1, 2} + table.sort(t) + print(table.concat(t, ",")) + table.sort(t, function(a, b) return a > b end) + print(table.concat(t, ",")) + local s = {"bb", "a", "ccc"} + table.sort(s) + print(table.concat(s, ",")) + """, + + """ + local t = {} + for i = 1, 50 do t[i] = (i * 37) % 50 end + table.sort(t) + local ok = true + for i = 2, 50 do if t[i-1] > t[i] then ok = false end end + print(ok, #t, t[1], t[50]) + local n = 0 + for k in pairs(t) do n = n + 1 end + print(n) + """, + }; + + private static final String[] SORT = { + + """ + -- 相等元素排完仍是「全序正确」,但相对次序不确定,只能断言有序性 + local t = {} + for i = 1, 30 do t[i] = i % 3 end + table.sort(t) + local ok = true + for i = 2, 30 do if t[i-1] > t[i] then ok = false end end + print(ok, table.concat(t, "")) + """, + + """ + -- 越过 RANLIMIT(100) 才会走「随机枢轴」那条路;枢轴越界时 a[p] 是 nil, + -- sort_comp 会在调用比较函数之前就抛错,所以这里用 pcall 兜住。 + math.randomseed(42) + local t = {} + for i = 1, 5000 do t[i] = math.random() end + print(pcall(table.sort, t, function(a, b) return a < b end)) + local ok = true + for i = 2, 5000 do if t[i-1] > t[i] then ok = false end end + print(ok, #t) + """, + + """ + print(pcall(table.sort, {3,1,2}, function(a,b) return true end)) + print(pcall(table.sort, {1,2,3}, function(a,b) return false end)) + print(pcall(table.sort, {1,2,3}, function(a,b) return nil end)) + print(pcall(table.sort, {1,2,3}, 42)) + print(pcall(table.sort, 42)) + """, + + """ + local t = {5, -1, 0, 3.5, -2.5, 2} + table.sort(t, function(a,b) return math.abs(a) < math.abs(b) end) + print(table.concat(t, ",")) + local u = {"10", "9", "1"} + table.sort(u) + print(table.concat(u, ",")) + """, + + """ + -- 混合 integer/float:sort 不该改变子类型 + local t = {1.0, 2, 3.0} + table.sort(t) + print(math.type(t[1]), math.type(t[2]), math.type(t[3])) + """, + }; + + private static final String[] META = { + + """ + -- __index 生效:把 userdata 当表读 + local ud = setmetatable({}, {__index = function(t, k) return k * 2 end}) + print(table.concat({1,2}, ",")) + local mt = {__index = {1, 2, 3}} + local t = setmetatable({}, mt) + print(#t, table.concat(t, ",")) + print(table.unpack(t)) + """, + + """ + -- __newindex 生效:insert/remove 能写 + local store = {} + local t = setmetatable({}, { + __index = function(_, k) return store[k] end, + __newindex = function(_, k, v) store[k] = v end, + __len = function() return #store end, + }) + table.insert(t, "a") + table.insert(t, "b") + print(store[1], store[2]) + print(table.remove(t)) + print(store[1], store[2]) + """, + + """ + -- __len 生效:# 走元方法 + local t = setmetatable({}, {__len = function() return 7 end}) + print(#t) + print(pcall(table.insert, t, 1)) + local u = setmetatable({}, {__len = function() return "x" end}) + print(pcall(#u)) + """, + }; + + /** + * {@code ltablib.c} 里两处整数溢出的边界。 + *

+ * 两处 C 版都靠「无符号/有符号回绕」来判越界,而 Java 的 {@code long} + * 回绕的方向不一样,直译会漏判——漏了之后不是报错,是 + * {@code OutOfMemoryError}({@code unpack} 一路 push)或者报错的那一条 + * 参数({@code move})。上游 {@code sort.lua} 一上来就撞这两处。 + */ + private static final String[] OVERFLOW = { + + /* tunpack:n 是无符号的,e - i 溢出时按无符号比 */ + """ + local maxI, minI = math.maxinteger, math.mininteger + local maxi, mini = (1 << 31) - 1, -(1 << 31) + print(pcall(table.unpack, {}, 0, maxi)) + print(pcall(table.unpack, {}, 1, maxi)) + print(pcall(table.unpack, {}, 0, maxI)) + print(pcall(table.unpack, {}, 1, maxI)) + print(pcall(table.unpack, {}, mini, maxi)) + print(pcall(table.unpack, {}, -maxi, maxi)) + print(pcall(table.unpack, {}, minI, maxI)) + print(pcall(table.unpack, {}, 1, 2^31)) + print(pcall(table.unpack, {}, 1, 999999)) + print(pcall(table.unpack, {}, 1, 1000001)) + """, + + /* tmove:判据是 e < MAXINTEGER + f,化简成 e - f < MAXINTEGER 会漏 */ + """ + local maxI, minI = math.maxinteger, math.mininteger + print(pcall(table.move, {}, 0, maxI, 1)) + print(pcall(table.move, {}, -1, maxI - 1, 1)) + print(pcall(table.move, {}, minI, -1, 1)) + print(pcall(table.move, {}, minI, maxI, 1)) + print(pcall(table.move, {}, 1, maxI, 2)) + print(pcall(table.move, {}, 1, 2, maxI)) + print(pcall(table.move, {}, minI, -2, 2)) + print(pcall(table.move, {}, 1, 2, 3, 4)) + """, + }; + + private static final String[] ERRORS = { + + """ + print(pcall(table.insert, 42, 1)) + print(pcall(table.insert, "s", 1)) + print(pcall(table.insert, {}, 1, 2, 3)) + print(pcall(table.insert, {}, "x", 1)) + print(pcall(table.insert, {}, 0, 1)) + print(pcall(table.insert, {1}, 3, 1)) + """, + + """ + print(pcall(table.remove, 42)) + print(pcall(table.remove, {}, "x")) + print(pcall(table.concat, 42)) + print(pcall(table.concat, {1, {}}, ",")) + print(pcall(table.concat, {{}}, ",")) + print(pcall(table.concat, {1, 2}, {})) + """, + + """ + print(pcall(table.unpack, 42)) + print(pcall(table.unpack, {1,2,3}, "x")) + print(pcall(table.unpack, {1,2,3}, 1, "x")) + print(pcall(table.move, 42, 1, 2, 1)) + print(pcall(table.move, {1,2}, "x", 2, 1)) + """, + + """ + local f = function() return table.insert(nil, 1) end + print(pcall(f)) + print(pcall(table.insert, nil, 1)) + print(select(2, pcall(table.concat))) + """, + }; +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/UpstreamSmokeTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/UpstreamSmokeTest.java new file mode 100644 index 0000000..e9b2695 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/UpstreamSmokeTest.java @@ -0,0 +1,153 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.io.File; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * 上游套件里「输出不该逐字节比、但必须能跑完」的那些用例。 + *

+ * 与 {@link UpstreamSuiteTest} 的分工:那边断言与参考实现逐字节一致, + * 这边只断言能正常结束——跑完并打印 {@code OK}。理由写在 + * {@link UpstreamSuiteTest} 的类注释里({@code cstack.lua} 打印随实现而变的 + * 递归层数,{@code gc.lua} 依赖本实现没有的增量收集器, + * {@code literals.lua} 依赖 C 程序才设得上的 locale 小数点)。 + *

+ * {@code sort.lua} 也是同一个道理:它打印排序耗时与比较次数 + * ({@code sorting 50000 random elements in 13.71 msec.}),而待排数据是 + * {@code math.random} 生成的——参考实现每次运行的种子都不一样,两个数都不可能 + * 对上。但它是唯一一处把 {@code table.unpack} / {@code table.move} 的 + * 整数溢出边界整组走过来的用例,所以必须跑。 + *

+ * 为什么不能干脆不跑。这几个文件是全套里最容易把实现「跑死」的: + *

    + *
  • {@code cstack.lua} 整篇都在制造栈溢出与 C 栈溢出,没有 + * {@code LUAI_MAXSTACK} / {@code LUAI_MAXCCALLS} 这两道上限, + * 无限递归在 Java 这边就是 {@code OutOfMemoryError}——它真的把 + * surefire 的 forked JVM 撑爆过,{@code ./mvnw test} 因此整条红;
  • + *
  • {@code db.lua} 有一句 {@code repeat local a = {} until name}, + * 在等一个 {@code __gc} 终结器。终结器不跑就是死循环;
  • + *
  • {@code literals.lua} / {@code gc.lua} 大量分配,能兜住自动收集 + * 那一层的回归。
  • + *
+ * 所以这里宁可断言得松,也不能没有。 + */ +final class UpstreamSmokeTest { + + private static File dir; + + @BeforeAll + static void locate() { + String env = System.getenv("LUA54_TESTS"); + if (env != null && !env.isEmpty() && new File(env).isDirectory()) { + dir = new File(env); + } else if (new File("target/lua54-tests").isDirectory()) { + dir = new File("target/lua54-tests"); + } + assumeTrue(dir != null, "找不到上游用例目录,跳过;设 LUA54_TESTS," + + "或下载到 target/lua54-tests"); + } + + @ParameterizedTest + @ValueSource(strings = { + "cstack.lua", + "literals.lua", + "locals.lua", + "constructs.lua", + "sort.lua", + "math.lua", + }) + void runsToCompletion(String name) { + String out = Diff.mineFileOnly(dir, name); + String[] lines = out.trim().split("\r?\n"); + assertTrue(lines.length > 0, "用例没有任何输出,等于没跑。file: " + name); + /* + * 只认最后一行:这些文件都以 print'OK' 收尾,跑到那一行才说明 + * 中途既没死循环、也没被栈/堆上限打断。 + *

+ * 判定前要先抹掉 {@code tracegc} 的进度点。上游 {@code tracegc.lua} + * 的 {@code __gc} 是 {@code io.stderr:write'.'}——不带换行, + * 所以那个点会粘在相邻输出上({@code locals.lua} 里就成了 + * {@code ".OK"}),也可能在 {@code print'OK'} 之后单独占一行 + * ({@code cstack.lua})。它什么时候写,取决于终结器什么时候跑: + * 参考实现由 {@code lgc.c} 的步进决定,本实现不移植那一层 + * (回收全交给 JVM),于是位置对不上。这是噪声,不是回归。 + */ + String last = null; + for (String line : lines) { + String s = line.replace(".", "").trim(); + if (!s.isEmpty()) { + last = s; + } + } + assertTrue("OK".equals(last), + "用例没有跑到最后的 print'OK'。file: " + name + + "\n末几行:\n" + tail(lines)); + } + + /** + * {@code gc.lua}:专测收集器本身,单独一条断言,契约与上面那条不同。 + *

+ * 它到不了 {@code OK},而且这是设计使然——本实现不移植 + * {@code lgc.c}(弱表走 {@code WeakReference}、{@code __gc} 走 + * {@code Cleaner},回收动作全交给 JVM),所以「收集之后内存下降了多少」 + * 这个量根本不存在。{@code collectgarbage("count")} 恒报 0,于是 + * {@code gc.lua:194} 的 {@code assert(gcinfo() < x)} 是 {@code 0 < 0}, + * 恒假;再往后 {@code gc.lua:463} 还要求一个 2^22 字节的串让 + * {@code count} 正好涨 2^13 KB——那是逐字节的分配记账,更不可能。 + *

+ * 所以这里守的是另外两件事,它们才是真会回归的: + *

    + *
  • 得跑到那一步:前面「creating many objects / + * functions with errors / long strings / steps」四节靠的是 + * {@code __gc}、终结语、长串驻留,崩在其中任何一节都会被这里拦下;
  • + *
  • 必须是「断言失败」而不是别的:恒 0 的内存量会让 + * {@code repeat until gcinfo() > 3 * x} 变成死循环(真写错过一次), + * 而 {@code OutOfMemoryError} / {@code ArrayIndexOutOfBounds} + * 这类会直接从引擎里冒出来。所以末行必须是 Lua 的 + * {@code assertion failed!},也就是「脚本自己停下的」。
  • + *
+ */ + @org.junit.jupiter.api.Test + void gcRunsThroughFinalizers() { + String out = Diff.mineFileOnly(dir, "gc.lua"); + assertTrue(out.contains("creating many objects"), + "gc.lua 没跑到第一节。输出:\n" + out); + assertTrue(out.contains("long strings"), + "gc.lua 没跑到长串那一节。输出:\n" + out); + assertTrue(out.contains("steps (2)"), + "gc.lua 没跑到 steps 那一节。输出:\n" + out); + /* + * 不能只认末行:未捕获的错误会过 {@code lua.c} 那个 + * {@code msghandler},消息后面还挂着一段 {@code luaL_traceback}, + * 最后一行是 {@code [C]: in ?}。所以是「某一行」匹配。 + */ + String[] lines = out.trim().split("\r?\n"); + boolean onAssert = false; + StringBuilder tail = new StringBuilder(); + for (String line : lines) { + if (line.matches(".*gc\\.lua:\\d+: assertion failed!")) { + onAssert = true; + } + if (tail.length() == 0 && !line.isEmpty()) { + tail.append(line); + } + } + assertTrue(onAssert, + "gc.lua 应当停在一条 Lua 断言上(内存统计那一段)," + + "而不是崩溃或死循环。末几行:\n" + tail(lines)); + } + + private static String tail(String[] lines) { + StringBuilder sb = new StringBuilder(); + for (int i = Math.max(0, lines.length - 8); i < lines.length; i++) { + sb.append(" ").append(lines[i]).append('\n'); + } + return sb.toString(); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/UpstreamSuiteTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/UpstreamSuiteTest.java new file mode 100644 index 0000000..cc98a43 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/UpstreamSuiteTest.java @@ -0,0 +1,108 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.io.File; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * 上游自带的一致性套件 —— {@code lua-5.4.x/tests/*.lua} 原样跑一遍。 + *

+ * 这是 README 里四道闸门的第一道,也是最强的一道:上游那套用例比我们手写的任何 + * 一段脚本都刁钻,而且它是「Lua 到底是什么」的定义方写的。 + *

+ * 用例目录的查找顺序: + *

    + *
  1. 环境变量 {@code LUA54_TESTS};
  2. + *
  3. 仓库内 {@code target/lua54-tests}(CI 下载到那里)。
  4. + *
+ * 找不到就整类跳过——没有参考二进制时 {@link Diff} 已经会跳过,而这里多一个 + * 「连用例文件都没有」的跳过条件,是为了让本地 {@code ./mvnw clean test} 不需要 + * 先联网下载 136 KB 的测试包也能绿。 + *

+ * 不是所有用例都进了这张表。挑进来的是「独立可跑 + 输出确定」的那些: + *

    + *
  • {@code all.lua} 是总驱动,它要 {@code ltests.c} 编出来的 {@code T} 模块、 + * 还要写 {@code time.txt}、打印内存与耗时,天生不可比;
  • + *
  • {@code main.lua} / {@code files.lua} / {@code attrib.lua} / {@code big.lua} + * 依赖 {@code all.lua} 建的全局环境,单跑就失败;
  • + *
  • {@code math.lua} 与 {@code sort.lua} 打印随机种子、随机数范围与耗时, + * 两次运行都不一样;
  • + *
  • {@code bwcoercion.lua} 单跑没有输出(它被 {@code bitwise.lua} + * {@code require},内容在那儿被覆盖);
  • + *
  • {@code heavy.lua} 要跑好几分钟。
  • + *
+ * 剩下来的那些里,{@code api.lua} 与 {@code code.lua} 在没有 {@code T} 时会自己 + * 跳过 C API 那几段,但剩下部分照跑,所以仍然有价值。 + *

+ * 还有一类是「能跑但输出不该逐字节比」的,同样不进这张表,理由是 + * 它们打印的本来就是随实现/随机而变的东西: + *

    + *
  • {@code cstack.lua}:每个用例都 {@code print("final count: ", count)}。 + * 这个数取决于「一层嵌套吃掉多少 C 栈」,Java 与 C 必然不同,上游自己也 + * 只打印不断言。它仍然值得跑(能兜住栈上限回归),但由 + * {@link UpstreamSmokeTest} 只断言「跑完并打印 OK」。
  • + *
  • {@code gc.lua}:整个文件在测增量收集器的参数({@code setpause} / + * {@code setstepmul} / {@code count} / {@code step})、弱表、应急 GC。 + * 本实现把内存管理交给 JVM,没有这一层可对标的语义。
  • + *
  • {@code literals.lua}:末尾那段「decimal point locale」要 + * {@code os.setlocale("pt_BR")} 真的生效——参考实现所在的 C 程序能设上, + * 于是它会跑 {@code tonumber("3,4")} 那一整组;本实现设不上,走的是 + * 「跳过」分支,输出天然多一行。
  • + *
  • {@code constructs.lua}:第 319 行是 + * {@code _ENV.GLOB1 = math.random(0, 1)},紧跟着就 + * {@code print('testing short-circuit optimizations (' .. _ENV.GLOB1 .. ')')}。 + * Lua 5.4 的种子取自时间与地址,两个进程不可能一样,于是这一行 + * 每次都有约一半的概率对不上,后面几百条用例还要按 GLOB1 走不同的分支。 + * 它由 {@link UpstreamSmokeTest} 只断言「跑完并打印 OK」—— + * 这本来就是上游注释里写的「sometimes will be 0, sometimes will not」。
  • + *
+ */ +final class UpstreamSuiteTest { + + private static final String ENV_TESTS = "LUA54_TESTS"; + + /** 用例目录;找不到为 {@code null} */ + private static File dir; + + @BeforeAll + static void locate() { + String env = System.getenv(ENV_TESTS); + if (env != null && !env.isEmpty() && new File(env).isDirectory()) { + dir = new File(env); + } else if (new File("target/lua54-tests").isDirectory()) { + dir = new File("target/lua54-tests"); + } + assumeTrue(dir != null, "找不到上游用例目录,跳过;设 " + ENV_TESTS + + ",或下载到 target/lua54-tests"); + assumeTrue(ReferenceLua.find() != null, "参考实现二进制不存在,跳过一致性测试"); + } + + @ParameterizedTest + @ValueSource(strings = { + "api.lua", + "bitwise.lua", + "calls.lua", + "closure.lua", + "code.lua", + "coroutine.lua", + "db.lua", + "errors.lua", + "events.lua", + "gengc.lua", + "goto.lua", + "nextvar.lua", + "pm.lua", + "strings.lua", + "tpack.lua", + "utf8.lua", + "vararg.lua", + "verybig.lua", + }) + void suite(String name) { + Diff.assertFileMatches(dir, name); + } +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/Utf8LibTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/Utf8LibTest.java new file mode 100644 index 0000000..ef8c45b --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/Utf8LibTest.java @@ -0,0 +1,294 @@ +package com.github.esrrhs.fakescript.lua; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +/** + * utf8 库的差分用例 —— 阶段 7。 + *

+ * UTF-8 是盖在字节串之上的一层,不是字符串的另一种表示: + * {@code #s} 数的是字节,{@code utf8.len} 数的是字符,含 {@code \xFF} + * 的串照样能用(只是 {@code len} 返回 {@code nil + 出错位置})。 + *

+ * 几条容易写错的: + *

    + *
  • {@code utf8.len} 用两值返回失败:{@code nil, 出错字节下标}, + * 不是抛错也不是返回 -1;{@code lax} 模式下会跳过非法序列继续数。
  • + *
  • {@code offset} 默认往后走({@code n} 为正时找第 n 个 + * 字符的起始字节),{@code n} 为 0 时返回当前字符的起始; + * 越界返回 {@code nil}({@code i} 已越界)或报错({@code i} 越出串外)。
  • + *
  • {@code codepoint} 的默认只取一个:{@code utf8.codepoint(s)} + * 返回单个值,给了 {@code j} 才返回一串。
  • + *
  • {@code charpattern} 是一个固定的模式串,直接比对原样输出。
  • + *
+ */ +class Utf8LibTest { + + @TestFactory + List utf8ScriptsMatchReference() { + List tests = new ArrayList<>(); + add(tests, "len", LEN); + add(tests, "off", OFFSET); + add(tests, "code", CODE); + add(tests, "err", ERRORS); + add(tests, "char", CHAR); + return tests; + } + + private static void add(List tests, String tag, String[] srcs) { + for (int i = 0; i < srcs.length; i++) { + String src = srcs[i]; + int n = i; + tests.add(DynamicTest.dynamicTest(tag + "[" + n + "]", () -> Diff.assertMatches(src))); + } + } + + private static final String[] LEN = { + + """ + print(utf8.charpattern) + print(#"hello", utf8.len("hello")) + print(utf8.len(""), #"") + print(#"中文", utf8.len("中文")) + print(#"\\u{1F600}", utf8.len("\\u{1F600}")) + print(utf8.len("a\\u{4E2D}b")) + """, + + """ + -- 非法字节:返回 nil + 出错位置 + print(utf8.len("\\xff")) + print(utf8.len("a\\xffb")) + print(utf8.len("\\xc3")) -- 截断的两字节序列 + print(utf8.len("\\xe4\\xb8")) -- 截断的三字节序列 + print(utf8.len("\\xc3\\x28")) -- 第二个字节不是续字节 + print(utf8.len("ok"), utf8.len("ok") == 2) + """, + + """ + -- lax 模式:跳过非法序列继续数 + print(utf8.len("\\xff", 1, -1, true)) + print(utf8.len("a\\xffb", 1, -1, true)) + print(utf8.len("\\xc3", 1, -1, true)) + print(utf8.len("\\xed\\xa0\\x80", 1, -1, true)) -- surrogate + print(utf8.len("\\xf5\\x80\\x80\\x80", 1, -1, true)) -- > U+10FFFF + """, + + """ + -- 带起止区间 + print(utf8.len("中文", 2)) + print(utf8.len("中文", 1, 3)) + print(utf8.len("中文", 4)) + print(utf8.len("abc", -2)) + print(utf8.len("abc", 2, -1)) + """, + }; + + private static final String[] OFFSET = { + + """ + local s = "中文abc" + print(utf8.offset(s, 1), utf8.offset(s, 2), utf8.offset(s, 3)) + print(utf8.offset(s, 4), utf8.offset(s, 5), utf8.offset(s, 6)) + print(utf8.offset(s, 7)) + print(utf8.offset(s, -1), utf8.offset(s, -2), utf8.offset(s, -3)) + print(utf8.offset(s, -4), utf8.offset(s, -6)) + print(utf8.offset(s, -7)) + """, + + """ + local s = "abc" + print(utf8.offset(s, 0, 2)) + print(utf8.offset(s, 1, 2), utf8.offset(s, 2, 2)) + print(utf8.offset(s, -1, 2)) + print(utf8.offset(s, 1, 4)) + print(utf8.offset(s, 1, 5)) + """, + + """ + print(utf8.offset("abc", 1, 100)) + print(utf8.offset("abc", 1, 0)) + print(utf8.offset("", 1)) + print(utf8.offset("", 1, 1)) + print(utf8.offset("中", 2)) + """, + }; + + private static final String[] CODE = { + + """ + print(utf8.codepoint("a")) + print(utf8.codepoint("中")) + print(utf8.codepoint("\\u{1F600}")) + print(utf8.codepoint("abc", 2)) + print(utf8.codepoint("abc", 2, 3)) + print(utf8.codepoint("中文", 1, -1)) + print(select("#", utf8.codepoint("abc"))) + print(select("#", utf8.codepoint("abc", 1, 3))) + """, + + """ + print(utf8.char(65, 66, 67)) + print(#utf8.char(0x4E2D), utf8.codepoint(utf8.char(0x4E2D))) + print(#utf8.char()) + print(utf8.char(0x1F600) == "\\u{1F600}") + print(#utf8.char(0x10FFFF), utf8.codepoint(utf8.char(0x10FFFF))) + print(utf8.codepoint(utf8.char(0, 0x7F, 0x80, 0x7FF, 0x800, 0xFFFF))) + """, + + """ + -- codes 迭代器 + local t = {} + for p, c in utf8.codes("a中b") do t[#t+1] = p .. ":" .. c end + print(table.concat(t, " ")) + local u = {} + for p, c in utf8.codes("\\u{1F600}") do u[#u+1] = p .. ":" .. c end + print(table.concat(u, " ")) + local v = {} + for p, c in utf8.codes("") do v[#v+1] = p end + print(#v) + """, + }; + + private static final String[] ERRORS = { + + """ + print(pcall(utf8.char, -1)) + print(pcall(utf8.char, 0x110000)) + print(pcall(utf8.char, "x")) + print(pcall(utf8.char, 1.5)) + print(pcall(utf8.codepoint)) + print(pcall(utf8.codepoint, 42)) + """, + + """ + print(pcall(utf8.codepoint, "\\xff")) + print(pcall(utf8.codepoint, "a\\xffb", 1, 3)) + print(pcall(function() for p, c in utf8.codes("\\xff") do end end)) + print(pcall(function() for p, c in utf8.codes("a\\xff") do end end)) + """, + + """ + print(pcall(utf8.len, 42)) + print(pcall(utf8.len, "abc", "x")) + print(pcall(utf8.offset, 42, 1)) + print(pcall(utf8.offset, "abc", "x")) + print(pcall(utf8.len, nil)) + """, + }; + + /** + * {@code utf8.char} 与 {@code utf8.codepoint} 的边角。 + *

+ * 三条容易记反的,全都是实测(参考二进制)钉住的: + *

    + *
  • {@code MAXUTF} 是 {@code 0x7FFFFFFF},所以 {@code 0x110000} + * 与 {@code 0xD800}(代理区)在 5.4 里都是合法码点 —— + * 按 Unicode 的直觉写「越界」用例会得到一个正常的字符串,而不是报错。
  • + *
  • 越界是按无符号比的:{@code utf8.char(-1)} 转成 + * {@code 0xFFFF...} 于是报错;按有符号比 -1 会「通过」并吐出一个 + * {@code ¿} 开头的垃圾字节。
  • + *
  • 单参与多参走的是两条分支,各自都有「value out of range」。 + * 只测单参的那条覆盖不到多参的那一处 —— 覆盖率报告上表现为整段 nc。
  • + *
+ * 另外 {@code charpattern} 这一组是真被拿来切字符的:它是 + * {@code [\0-\x7F\xC2-\xFD][\x80-\xBF]*},首字节排除了 {@code \xC0} / + * {@code \xC1}(过长编码)与 {@code \xFE} / {@code \xFF}(非法),所以 + * {@code "\xff\xfe"} 上一个都匹配不到。只用 + * {@code print(utf8.charpattern)} 比对原样输出的话,这个性质一点都没验到。 + */ + private static final String[] CHAR = { + """ +-- utf8.char / utf8.codepoint 的边角 +local function p(tag, f, ...) + local ok, a, b = pcall(f, ...) + if not ok then + print(string.format("%s ERR %s", tag, tostring(a))) + else + local t = { tostring(a), tostring(b) } + print(string.format("%s %s | %s", tag, t[1], t[2])) + end +end +local function q(tag, s) + -- 结果按字节摊开,含 \\0 也看得见 + local t = {} + for i = 1, #s do + t[i] = string.format("%02X", string.byte(s, i)) + end + print(string.format("%s #%d [%s]", tag, #s, table.concat(t, " "))) +end + +-- A. utf8.char:单参 +q("A01", utf8.char(65)) +q("A02", utf8.char(0)) +q("A03", utf8.char(0x7F)) +q("A04", utf8.char(0x80)) +q("A05", utf8.char(0x7FF)) +q("A06", utf8.char(0x800)) +q("A07", utf8.char(0xFFFF)) +q("A08", utf8.char(0x10000)) +q("A09", utf8.char(0x10FFFF)) +q("A10", utf8.char(0xD800)) -- 代理区:5.4 允许 +p("A11", utf8.char, 0x110000) +p("A12", utf8.char, 0x7FFFFFFF) +p("A13", utf8.char, -1) +p("A14", utf8.char, -0x80000000) +p("A15", utf8.char, math.maxinteger) +p("A16", utf8.char, math.mininteger) + +-- B. utf8.char:多参(第二个之后越界走的是另一条分支) +q("B01", utf8.char()) +q("B02", utf8.char(65, 66, 67)) +q("B03", utf8.char(65, 0x4E2D, 66)) +p("B04", utf8.char, 65, 0x7FFFFFFF) +p("B05", utf8.char, 65, -1) +p("B06", utf8.char, 65, 0x110000) +p("B07", utf8.char, 0x110000, 65) +p("B08", utf8.char, 65, 66, -1, 67) +p("B09", utf8.char, 65, true) +p("B10", utf8.char, 65, 66.0) +p("B11", utf8.char, 65, 66.5) +p("B12", utf8.char, "65", 66) +p("B13", utf8.char, 65, nil) -- nil 收尾:算几个参数 +p("B14", utf8.char, 65, 66, 67, 68, 69, 70, 71, 72) + +-- C. utf8.codepoint 的越界形状 +local s = "a中b" +p("C01", utf8.codepoint, s) +p("C02", utf8.codepoint, s, 1) +p("C03", utf8.codepoint, s, 1, 2) +p("C04", utf8.codepoint, s, 2, 1) -- i > j:一个都不给 +p("C05", utf8.codepoint, s, 0) +p("C06", utf8.codepoint, s, 1, 6) +p("C07", utf8.codepoint, s, -6) +p("C08", utf8.codepoint, s, 1, -6) +p("C09", utf8.codepoint, "", 1) +p("C10", utf8.codepoint, "", 1, 0) +p("C11", utf8.codepoint, s, 1, 5, true) +p("C12", utf8.codepoint, "\\xff", 1, 1, true) +p("C13", utf8.codepoint, "\\xff", 1, 1, false) +p("C14", utf8.codepoint, s, "2") +p("C15", utf8.codepoint, s, 1, "3") +p("C16", utf8.codepoint, s, 1.0, 2.0) +p("C17", utf8.codepoint, s, 1.5) +p("C18", utf8.codepoint, s, true) + +-- D. charpattern 真被用过:能不重不漏地切字符 +local pat = utf8.charpattern +local n = 0 +for _ in ("a中b\\u{1F600}"):gmatch(pat) do + n = n + 1 +end +print("D01 " .. n) +local n2 = 0 +for _ in ("\\xff\\xfe"):gmatch(pat) do + n2 = n2 + 1 +end +print("D02 " .. n2) +print("D03 " .. ("a中b"):gsub(pat, "#")) +print("D04 " .. tostring(pat:byte(1, -1))) + """, + }; +} diff --git a/src/test/java/com/github/esrrhs/fakescript/lua/WeakTableTest.java b/src/test/java/com/github/esrrhs/fakescript/lua/WeakTableTest.java new file mode 100644 index 0000000..c2a6266 --- /dev/null +++ b/src/test/java/com/github/esrrhs/fakescript/lua/WeakTableTest.java @@ -0,0 +1,295 @@ +package com.github.esrrhs.fakescript.lua; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +/** + * 弱表的差分用例 —— 阶段 6 的闸门。 + *

+ * 这批用例与之前几批有个本质区别:它们依赖 JVM 真的把对象收掉。 + * 参考实现那边靠 {@code collectgarbage()} 触发一次完整的 Lua GC,这边靠 + * {@code System.gc()}。开工前先量过可靠性:本机 JDK 17 上 500/500 次 + * {@code System.gc()} 都能清掉不可达对象的 {@code WeakReference}, + * 带上 JaCoCo agent 也是 500/500,所以这批用例是可以进闸门的。 + *

+ * 反过来,如果哪天换了 JVM 参数(比如 {@code -XX:+DisableExplicitGC}) + * 让 {@code System.gc()} 变成空操作,这里会成片变红——那正是它该做的反应: + * 弱表语义已经不成立了。 + */ +class WeakTableTest { + + @TestFactory + List weakTableScriptsMatchReference() { + List tests = new ArrayList<>(); + for (int i = 0; i < SCRIPTS.length; i++) { + String src = SCRIPTS[i]; + int n = i; + tests.add(DynamicTest.dynamicTest("weak[" + n + "]", + () -> Diff.assertMatches(src))); + } + return tests; + } + + /** + * 不依赖收集时机的那一半:无论 JVM 收不收,这些规则都必须成立。 + *

+ * 单独列出来是有意义的——真正依赖收集的用例在环境异常时会成片变红, + * 而这一半应当永远绿着。它们红了说明是弱表逻辑写错了,不是环境的问题。 + */ + @TestFactory + List collectibleRulesWithoutCollection() { + List tests = new ArrayList<>(); + for (int i = 0; i < NO_GC.length; i++) { + String src = NO_GC[i]; + int n = i; + tests.add(DynamicTest.dynamicTest("weakNoGc[" + n + "]", + () -> Diff.assertMatches(src))); + } + return tests; + } + + private static final String[] NO_GC = { + + """ + -- 字符串 / 数字 / 布尔不会被弱表清掉(没有「显式构造」) + local t = setmetatable({}, {__mode = "kv"}) + t["abc"] = "def" + t[1] = true + t[2.5] = 7 + collectgarbage() + local n = 0 + for k in pairs(t) do n = n + 1 end + print(n, t["abc"], t[1], t[2.5]) + """, + + """ + -- 还活着的键不会被清掉 + local keep = {} + local t = setmetatable({}, {__mode = "k"}) + t[keep] = "v" + collectgarbage() + print(t[keep], (next(t)) ~= nil) + """, + + """ + -- 还活着的值不会被清掉 + local keep = {} + local t = setmetatable({}, {__mode = "v"}) + t[1] = keep + collectgarbage() + print(t[1] == keep) + """, + + """ + -- 非法的 __mode 一律静默接受 + print(pcall(setmetatable, {}, {__mode = "x"})) + print(pcall(setmetatable, {}, {__mode = 5})) + print(pcall(setmetatable, {}, {__mode = ""})) + print(pcall(setmetatable, {}, {__mode = "kvjk"})) + """, + + """ + -- 弱表照常能读写、能遍历 + local t = setmetatable({}, {__mode = "kv"}) + for i = 1, 20 do t[i] = i * i end + local n, sum = 0, 0 + for k, v in pairs(t) do n = n + 1 sum = sum + v end + print(n, sum, t[5]) + """, + + """ + -- 函数也是可收集对象 + local f = function() return 1 end + local t = setmetatable({}, {__mode = "v"}) + t.f = f + print(t.f()) + collectgarbage() + print(t.f()) + local g = function() return 2 end + t.f = g + print(t.f()) + """, + }; + + private static final String[] SCRIPTS = { + + """ + -- 弱键:键不再被引用 → 条目消失 + local t = setmetatable({}, {__mode = "k"}) + do + local k = {} + t[k] = "v" + end + print("before", next(t) ~= nil) + collectgarbage() + print("after", next(t)) + """, + + """ + -- 弱值:值不再被引用 → 条目消失 + local t = setmetatable({}, {__mode = "v"}) + do + local v = {} + t.x = v + end + collectgarbage() + print(next(t)) + """, + + """ + -- 先建普通表、后设 __mode:实测参考实现里照样生效 + local t = {} + local key = {} + t[key] = "alive" + print("before", t[key]) + setmetatable(t, {__mode = "k"}) + key = nil + collectgarbage() + print("after", next(t)) + """, + + """ + -- ephemeron:键只被自己的值引用时,仍然会被清掉 + local t = setmetatable({}, {__mode = "k"}) + do + local k = {} + t[k] = k + end + collectgarbage() + print("ephemeron", next(t)) + """, + + """ + -- 弱键表里,值引用了键 → 键活下来(JVM 可达性与 Lua 可达性在这里一致) + local t = setmetatable({}, {__mode = "k"}) + local holder + do + local k = {} + holder = {k} -- 值这一侧强引用 k + t[k] = holder + end + collectgarbage() + print("kept", next(t) ~= nil) + """, + + """ + -- kv 都弱:键或值任一被回收就摘掉 + local t = setmetatable({}, {__mode = "kv"}) + do + local k, v = {}, {} + t[k] = v + end + collectgarbage() + print(next(t)) + """, + + """ + -- 弱值表里的整数键(数组部分)也要能被回收 + local t = setmetatable({}, {__mode = "v"}) + for i = 1, 5 do t[i] = {i} end + print("before", #t) + collectgarbage() + local n = 0 + for k in pairs(t) do n = n + 1 end + print("after", n) + """, + + """ + -- 部分条目活着、部分被回收 + local keep = {} + local t = setmetatable({}, {__mode = "k"}) + t[keep] = "kept" + do + local dead = {} + t[dead] = "dead" + end + collectgarbage() + local n = 0 + for k in pairs(t) do n = n + 1 end + print(n, t[keep]) + """, + + """ + -- 弱表当缓存用:命中与失效 + local cache = setmetatable({}, {__mode = "k"}) + local function get(t) + local v = cache[t] + if v == nil then + v = "computed" + cache[t] = v + end + return v + end + local a = {} + print(get(a), get(a)) + local n = 0 + for k in pairs(cache) do n = n + 1 end + print(n) + a = nil + collectgarbage() + local m = 0 + for k in pairs(cache) do m = m + 1 end + print(m) + """, + + """ + -- 协程对象作为弱值 + local t = setmetatable({}, {__mode = "v"}) + do + local co = coroutine.create(function() end) + t.co = co + end + collectgarbage() + print(next(t)) + """, + + """ + -- 连续两轮收集:第二轮不该再有问题 + local t = setmetatable({}, {__mode = "kv"}) + for i = 1, 10 do + local k = {} + t[k] = {i} + end + collectgarbage() + collectgarbage() + local n = 0 + for k in pairs(t) do n = n + 1 end + print(n) + """, + + """ + -- 遍历中途条目仍然活着:pairs 不该崩 + local t = setmetatable({}, {__mode = "v"}) + local keep = {} + for i = 1, 10 do t[i] = keep end + local n = 0 + for k, v in pairs(t) do n = n + 1 end + print(n) + collectgarbage() + local m = 0 + for k, v in pairs(t) do m = m + 1 end + print(m) + """, + + """ + -- 弱表里放 nil 值:等于没有这一项 + local t = setmetatable({}, {__mode = "kv"}) + local k = {} + t[k] = 1 + t[k] = nil + print(next(t) == nil) + """, + + """ + -- collectgarbage 返回数字;弱表照常是表、照常能取地址串 + print(type(collectgarbage())) + local t = setmetatable({}, {__mode = "k"}) + print(type(t), type(tostring(t))) + """, + }; +} diff --git a/src/test/resources/fkdump.c b/src/test/resources/fkdump.c new file mode 100644 index 0000000..f5500da --- /dev/null +++ b/src/test/resources/fkdump.c @@ -0,0 +1,132 @@ +/* +** fkdump.c —— 给「Java 版 Lua」做差分比对用的规范转储器。 +** +** 这个文件要随仓库走:CI 的一致性 job 会先编出参考实现,再把它和 +** liblua.a 一起编译(见 .github/workflows/maven.yml)。它不是产品代码, +** 只是差分比对的另一半。 +** +** 为什么自己写而不用 luac -l:luac 的输出是给人看的(每条指令按 opcode 打不同 +** 的字段、还夹着注释),要反解回原始字段就得把那个大 switch 抄一遍,反而更脆。 +** 这里对所有 opcode 统一打印全部字段,格式固定,Java 侧照抄即可。 +** +** 用法:fkdump +*/ +#include +#include +#include + +#include "lua.h" +#include "lauxlib.h" +#include "lualib.h" + +#include "ldebug.h" +#include "lfunc.h" +#include "lobject.h" +#include "lopcodes.h" +#include "lstate.h" +#include "ltm.h" +#include "lopnames.h" + +static void printString(const char *s, int len) { + int i; + for (i = 0; i < len; i++) { + unsigned char c = (unsigned char) s[i]; + if (c == '\\' || c == '"') { + printf("\\%02x", c); + } else if (c >= 0x20 && c < 0x7f) { + putchar(c); + } else { + printf("\\%02x", c); + } + } +} + +static void printConstant(const TValue *v) { + switch (ttype(v)) { + case LUA_TNIL: + printf("nil"); + break; + case LUA_TBOOLEAN: + printf("bool:%d", !l_isfalse(v)); + break; + case LUA_TNUMBER: + if (ttisinteger(v)) + printf("int:%lld", (long long) ivalue(v)); + else { + lua_Number d = fltvalue(v); + unsigned long long bits = 0; + memcpy(&bits, &d, sizeof(bits)); + printf("flt:%016llx", bits); + } + break; + case LUA_TSTRING: { + TString *ts = tsvalue(v); + printf("str:"); + printString(getstr(ts), tsslen(ts)); + break; + } + default: + printf("other"); + break; + } +} + +static void dumpFunction(const Proto *f, int level, int idx); + +static void dumpFunction(const Proto *f, int level, int idx) { + int i; + printf("PROTO level=%d index=%d linedefined=%d lastlinedefined=%d\n", + level, idx, f->linedefined, f->lastlinedefined); + printf(" numparams=%d isvararg=%d maxstacksize=%d nups=%d nconsts=%d nsubs=%d\n", + f->numparams, f->is_vararg, f->maxstacksize, + f->sizeupvalues, f->sizek, f->sizep); + for (i = 0; i < f->sizecode; i++) { + Instruction inst = f->code[i]; + int o = GET_OPCODE(inst); + printf(" I %d %s a=%d b=%d c=%d k=%d bx=%d sbx=%d ax=%d sb=%d sc=%d sj=%d line=%d\n", + i, opnames[o], + GETARG_A(inst), GETARG_B(inst), GETARG_C(inst), GETARG_k(inst), + GETARG_Bx(inst), GETARG_sBx(inst), GETARG_Ax(inst), + GETARG_sB(inst), GETARG_sC(inst), GETARG_sJ(inst), + luaG_getfuncline(f, i)); + } + for (i = 0; i < f->sizek; i++) { + printf(" K %d ", i); + printConstant(&f->k[i]); + printf("\n"); + } + for (i = 0; i < f->sizeupvalues; i++) { + printf(" UP %d instack=%d idx=%d kind=%d name=", i, + f->upvalues[i].instack, f->upvalues[i].idx, f->upvalues[i].kind); + if (f->upvalues[i].name == NULL) { + printf("?"); + } else { + printString(getstr(f->upvalues[i].name), tsslen(f->upvalues[i].name)); + } + printf("\n"); + } + for (i = 0; i < f->sizep; i++) { + dumpFunction(f->p[i], level + 1, i); + } +} + +int main(int argc, char **argv) { + lua_State *L; + const TValue *o; + LClosure *cl; + if (argc < 2) { + fprintf(stderr, "usage: fkdump \n"); + return 2; + } + L = luaL_newstate(); + luaL_openlibs(L); + if (luaL_loadfilex(L, argv[1], "t") != LUA_OK) { + fprintf(stderr, "LOADERR %s\n", lua_tostring(L, -1)); + return 1; + } + o = s2v(L->top.p - 1); + cl = clLvalue(o); + dumpFunction(cl->p, 0, 0); + lua_close(L); + return 0; +}